import { MessageFlags, type EditInteractionContent } from 'oceanic.js' import { deletePanel, panels, setPanel } from '../store.ts' import { normalizeEmoji } from '../emoji.ts' import { guard, slash, slashSub } from '../rosepack.ts' const ephemeral = (content: string): EditInteractionContent => ({ content, flags: MessageFlags.EPHEMERAL }) const ROLE_INPUT = /^(?:<@&)?(\d+)>?$/ const MESSAGE_INPUT = /^(?:https:\/\/discord(?:app)?\.com\/channels\/\d+\/\d+\/)?(\d+)$/ // declared on the leaves rather than the root so the leaf contexts get // guild-narrowed interaction types const adminOnly = [ guard.guild({ message: 'this command only works in a server.' }), guard.userPermissions('MANAGE_ROLES', { message: 'you need manage roles to do that.' }) ] export default slash({ name: 'reactrole', description: 'manage reaction-role panels', subcommands: { create: slashSub({ description: 'post a new reaction-role message in this channel', guards: adminOnly, options: { message: { description: 'what the panel message should say', kind: 'string', maxLength: 2_000, required: true }, emoji: { description: 'the emoji users react with', kind: 'string', required: true }, role: { description: 'the role granted by reacting (mention or id)', kind: 'string', required: true } }, async execute(context) { const { client } = context const guildID = context.interaction.guildID const channelID = context.interaction.channelID const emoji = normalizeEmoji(context.options.emoji) if (emoji === undefined) { await context.reply(ephemeral('that is not a valid emoji. use a single unicode or custom emoji.')) return } const roleID = ROLE_INPUT.exec(context.options.role.trim())?.[1] if (roleID === undefined) { await context.reply(ephemeral('pass the role as a mention or an id.')) return } const guild = client.guilds.get(guildID) const role = guild?.roles.get(roleID) ?? (await client.rest.guilds.getRole(guildID, roleID).catch(() => undefined)) if (role === undefined) { await context.reply(ephemeral('that role does not exist in this server.')) return } // startup reconciliation revokes the role from anyone who has not // reacted, so two panels sharing one role would fight each other for (const [existingID, existing] of panels) { if (existing.guildID === guildID && existing.roleID === roleID) { await context.reply( ephemeral( `that role already has a panel: https://discord.com/channels/${guildID}/${existing.channelID}/${existingID}` ) ) return } } if (guild !== undefined) { const botMember = guild.members.get(client.user.id) if (botMember !== undefined) { const top = Math.max(0, ...botMember.roles.map((id) => guild.roles.get(id)?.position ?? 0)) if (role.position >= top) { await context.reply( ephemeral('the target role must sit below my highest role before i can grant it.') ) return } } } let message try { message = await client.rest.channels.createMessage(channelID, { content: context.options.message }) } catch { await context.reply( ephemeral( 'i could not post the panel here. check that i have view channel and send messages in this channel.' ) ) return } try { await client.rest.channels.createReaction(channelID, message.id, emoji) } catch { await client.rest.channels.deleteMessage(channelID, message.id).catch(() => {}) await context.reply( ephemeral( 'i could not react to the new message. check that i have add reactions and read message history here.' ) ) return } setPanel(message.id, { channelID, emoji, guildID, reactors: new Set(), roleID }) await context.reply( ephemeral(`panel created: https://discord.com/channels/${guildID}/${channelID}/${message.id}`) ) } }), remove: slashSub({ description: 'stop a reaction-role message from granting roles', guards: adminOnly, options: { message: { description: 'the panel message, as an id or link', kind: 'string', required: true } }, async execute(context) { const messageID = MESSAGE_INPUT.exec(context.options.message.trim())?.[1] if (messageID === undefined) { await context.reply(ephemeral('pass the panel message as an id or a message link.')) return } await context.reply( ephemeral( deletePanel(messageID) ? 'that message no longer grants roles.' : 'there is no panel registered for that message.' ) ) } }) } })