From a388d99fdf0f061f35b3efecc2484e7fa04f3d0d Mon Sep 17 00:00:00 2001 From: indifferentketchup Date: Sun, 24 May 2026 05:04:40 +0000 Subject: [PATCH] =?UTF-8?q?/transfer:=20validate=20target=20via=20isStaff(?= =?UTF-8?q?)=20=E2=80=94=20covers=20ADDITIONAL=5FSTAFF=5FROLES?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- handlers/commands/index.js | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/handlers/commands/index.js b/handlers/commands/index.js index de7fe2e..bcc849e 100644 --- a/handlers/commands/index.js +++ b/handlers/commands/index.js @@ -17,6 +17,7 @@ const { EmbedBuilder, MessageFlags } = require('discord.js'); const { mongoose } = require('../../db-connection'); const { CONFIG } = require('../../config'); +const { isStaff } = require('../../utils'); const { setNotifyDm } = require('../../services/staffSettings'); const { makeTicketName, resolveCreatorNickname } = require('../../services/tickets'); const { enqueueRename, enqueueMove, enqueueOverwrite, enqueueTopic, enqueueSend } = require('../../services/channelQueue'); @@ -95,11 +96,26 @@ async function handleTransfer(interaction) { const ticket = await findTicketForChannel(interaction); if (!ticket) return; - const staffRoleId = CONFIG.ROLE_TO_PING_ID; - const guildMember = await interaction.guild.members.fetch(member.id).catch(() => null); + // Cache-first member resolution; falls back to a fetch if not in cache. + // 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)) { - return interaction.reply({ content: 'The target member must have the staff role.', flags: MessageFlags.Ephemeral }); + // Reject self-transfers and bots; require the target to satisfy isStaff(), + // 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.