/transfer: validate target via isStaff() — covers ADDITIONAL_STAFF_ROLES

The transfer-target check previously matched only against
CONFIG.ROLE_TO_PING_ID, so a member with one of
CONFIG.ADDITIONAL_STAFF_ROLES (a recognized staff role everywhere else
in the bot, including requireStaffRole and the messages.js claimer-DM
path) was rejected as a transfer target. Switch to isStaff() so the
transfer-target gate matches the rest of the codebase's staff
definition.

Also:
- Reject bots as transfer targets (guildMember.user.bot).
- Reject self-transfer (transferring to interaction.user.id) — the
  rename + DB write would no-op but the log line claimed a transfer
  that didn't happen.
- Resolve the target member cache-first to avoid an unnecessary REST
  round-trip when the GuildMembers intent has the user cached.
This commit is contained in:
2026-05-24 05:04:40 +00:00
parent 3212004fc9
commit a388d99fdf

View File

@@ -17,6 +17,7 @@
const { EmbedBuilder, MessageFlags } = require('discord.js'); const { EmbedBuilder, MessageFlags } = require('discord.js');
const { mongoose } = require('../../db-connection'); const { mongoose } = require('../../db-connection');
const { CONFIG } = require('../../config'); const { CONFIG } = require('../../config');
const { isStaff } = require('../../utils');
const { setNotifyDm } = require('../../services/staffSettings'); const { setNotifyDm } = require('../../services/staffSettings');
const { makeTicketName, resolveCreatorNickname } = require('../../services/tickets'); const { makeTicketName, resolveCreatorNickname } = require('../../services/tickets');
const { enqueueRename, enqueueMove, enqueueOverwrite, enqueueTopic, enqueueSend } = require('../../services/channelQueue'); const { enqueueRename, enqueueMove, enqueueOverwrite, enqueueTopic, enqueueSend } = require('../../services/channelQueue');
@@ -95,11 +96,26 @@ async function handleTransfer(interaction) {
const ticket = await findTicketForChannel(interaction); const ticket = await findTicketForChannel(interaction);
if (!ticket) return; if (!ticket) return;
const staffRoleId = CONFIG.ROLE_TO_PING_ID; // Cache-first member resolution; falls back to a fetch if not in cache.
const guildMember = await interaction.guild.members.fetch(member.id).catch(() => null); // GuildMembers intent keeps the cache warm in normal operation.
const guildMember = interaction.guild.members.cache.get(member.id)
|| await interaction.guild.members.fetch(member.id).catch(() => null);
if (!guildMember || !guildMember.roles.cache.has(staffRoleId)) { // Reject self-transfers and bots; require the target to satisfy isStaff(),
return interaction.reply({ content: 'The target member must have the staff role.', flags: MessageFlags.Ephemeral }); // which covers ROLE_ID_TO_PING + ADDITIONAL_STAFF_ROLES — the same staff
// definition used by every other gate in the bot. The previous check only
// looked at ROLE_TO_PING_ID, missing additional staff roles.
if (!guildMember || guildMember.user.bot || !isStaff(guildMember)) {
return interaction.reply({
content: 'The target member must have the staff role.',
flags: MessageFlags.Ephemeral
});
}
if (guildMember.id === interaction.user.id) {
return interaction.reply({
content: 'You cannot transfer the ticket to yourself.',
flags: MessageFlags.Ephemeral
});
} }
// Defer before the DB write + rename so the interaction token survives. // Defer before the DB write + rename so the interaction token survives.