diff --git a/commands/register.js b/commands/register.js index cf75c7f..da4ec13 100644 --- a/commands/register.js +++ b/commands/register.js @@ -365,13 +365,6 @@ async function registerCommands() { .setIntegrationTypes([ApplicationIntegrationType.GuildInstall]) .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), - new SlashCommandBuilder() - .setName('ids') - .setDescription('List all channel and role IDs in this server') - .setContexts([InteractionContextType.Guild]) - .setIntegrationTypes([ApplicationIntegrationType.GuildInstall]) - .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages), - new SlashCommandBuilder() .setName('accountinfo') .setDescription('Look up website account info by email or Discord user') diff --git a/handlers/commands.js b/handlers/commands.js index c116166..ef2a2ea 100644 --- a/handlers/commands.js +++ b/handlers/commands.js @@ -967,49 +967,6 @@ async function handleCommand(interaction) { await interaction.editReply('❌ An error occurred while fetching statistics.'); } } - - // /ids – list channel and role IDs (like discord.py guild.channels / guild.roles) - if (interaction.commandName === 'ids') { - trackInteraction('commands', 'ids', interaction.user.tag); - await interaction.deferReply({ ephemeral: true }); - const guild = interaction.guild; - if (!guild) { - await interaction.editReply('This command can only be used in a server.'); - return; - } - try { - await guild.channels.fetch().catch(() => {}); - - const channelTypeName = type => - Object.entries(ChannelType).find(([, v]) => v === type)?.[0] ?? String(type); - - const lines = ['**Channels:**']; - const channels = [...guild.channels.cache.values()].sort((a, b) => a.rawPosition - b.rawPosition); - for (const ch of channels) { - lines.push(`\`${ch.id}\` — ${ch.name} (${channelTypeName(ch.type)})`); - } - lines.push(''); - lines.push('**Roles:**'); - const roles = [...guild.roles.cache.values()].sort((a, b) => b.position - a.position); - for (const role of roles) { - lines.push(`\`${role.id}\` — ${role.name}`); - } - - const text = lines.join('\n'); - if (text.length <= 1900) { - await interaction.editReply(text); - } else { - const buf = Buffer.from(text, 'utf8'); - await interaction.editReply({ - content: 'List is long; sent as a file.', - files: [new AttachmentBuilder(buf, { name: `guild-ids-${guild.id}.txt` })] - }); - } - } catch (err) { - trackError('ids-command', err, interaction); - await interaction.editReply('Failed to build ID list.'); - } - } } /** @@ -1182,4 +1139,11 @@ async function handleAutocomplete(interaction) { } } -module.exports = { handleCommand, handleContextMenu, handleAutocomplete, runEscalation, runDeescalation }; +module.exports = { + handleCommand, + handleContextMenu, + handleAutocomplete, + runEscalation, + runDeescalation, + hasStaffRole +}; diff --git a/handlers/messages.js b/handlers/messages.js index 95af410..2f43855 100644 --- a/handlers/messages.js +++ b/handlers/messages.js @@ -1,20 +1,91 @@ /** - * Discord messageCreate handler – forwards staff replies to Gmail. + * Discord messageCreate handler – prefix commands and forwards staff replies to Gmail. */ +const { ChannelType, AttachmentBuilder } = require('discord.js'); const { mongoose } = require('../db-connection'); const { CONFIG } = require('../config'); const { extractRawEmail } = require('../utils'); const { getGmailClient, sendGmailReply } = require('../services/gmail'); const { updateTicketActivity } = require('../services/tickets'); +const { hasStaffRole } = require('./commands'); +const { trackInteraction, trackError } = require('./analytics'); const Ticket = mongoose.model('Ticket'); +/** + * `!ids` — list channel and role IDs (same rules as slash staff-gated commands). + * @returns {Promise} true if the message was consumed + */ +async function tryHandleIdsCommand(m) { + if (m.content?.trim().toLowerCase() !== '!ids') return false; + + if (!m.guild) { + await m.reply('This command can only be used in a server.').catch(() => {}); + return true; + } + + const staffConfigured = + CONFIG.ROLE_ID_TO_PING || (CONFIG.ADDITIONAL_STAFF_ROLES && CONFIG.ADDITIONAL_STAFF_ROLES.length > 0); + if (staffConfigured && !hasStaffRole(m.member)) { + const roleMention = CONFIG.ROLE_ID_TO_PING ? `<@&${CONFIG.ROLE_ID_TO_PING}>` : 'support'; + await m.reply({ + content: `This command is only available to the support team (${roleMention}).`, + allowedMentions: { users: [], roles: [], repliedUser: false } + }).catch(() => {}); + return true; + } + + trackInteraction('commands', 'ids-prefix', m.author.tag); + + try { + await m.guild.channels.fetch().catch(() => {}); + + const channelTypeName = type => + Object.entries(ChannelType).find(([, v]) => v === type)?.[0] ?? String(type); + + const lines = ['**Channels:**']; + const channels = [...m.guild.channels.cache.values()].sort((a, b) => a.rawPosition - b.rawPosition); + for (const ch of channels) { + lines.push(`\`${ch.id}\` — ${ch.name} (${channelTypeName(ch.type)})`); + } + lines.push(''); + lines.push('**Roles:**'); + const roles = [...m.guild.roles.cache.values()].sort((a, b) => b.position - a.position); + for (const role of roles) { + lines.push(`\`${role.id}\` — ${role.name}`); + } + + const text = lines.join('\n'); + const baseReply = { allowedMentions: { users: [], roles: [], repliedUser: false } }; + if (text.length <= 1900) { + await m.reply({ content: text, ...baseReply }).catch(() => {}); + } else { + const buf = Buffer.from(text, 'utf8'); + await m.reply({ + content: 'List is long; sent as a file.', + files: [new AttachmentBuilder(buf, { name: `guild-ids-${m.guild.id}.txt` })], + ...baseReply + }).catch(() => {}); + } + } catch (err) { + trackError('ids-prefix-command', err, null); + await m.reply({ + content: 'Failed to build ID list.', + allowedMentions: { repliedUser: false } + }).catch(() => {}); + } + + return true; +} + /** * Handle a Discord message in a ticket channel → relay to Gmail (email tickets only). */ async function handleDiscordReply(m) { if (m.author.bot || m.interaction) return; + if (await tryHandleIdsCommand(m)) return; + const ticket = await Ticket.findOne({ discordThreadId: m.channel.id }).lean(); if (!ticket) return;