From 56a469a999774c8b8683adeb59b243fdf85b14c9 Mon Sep 17 00:00:00 2001 From: Max Isom Date: Sat, 5 Feb 2022 16:16:17 -0600 Subject: Migrate to slash commands (#431) Co-authored-by: Federico fuji97 Rapetti --- src/bot.ts | 204 +++++++++++++++++++++++++++++++++---------------------------- 1 file changed, 112 insertions(+), 92 deletions(-) (limited to 'src/bot.ts') diff --git a/src/bot.ts b/src/bot.ts index 4a97f6e..0a041ec 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,147 +1,167 @@ -import {Client, Message, Collection} from 'discord.js'; +import {Client, Collection, User} from 'discord.js'; import {inject, injectable} from 'inversify'; import ora from 'ora'; import {TYPES} from './types.js'; -import {prisma} from './utils/db.js'; import container from './inversify.config.js'; import Command from './commands/index.js'; import debug from './utils/debug.js'; -import NaturalLanguage from './services/natural-language-commands.js'; import handleGuildCreate from './events/guild-create.js'; import handleVoiceStateUpdate from './events/voice-state-update.js'; +import handleGuildUpdate from './events/guild-update.js'; import errorMsg from './utils/error-msg.js'; import {isUserInVoice} from './utils/channels.js'; import Config from './services/config.js'; import {generateDependencyReport} from '@discordjs/voice'; +import {REST} from '@discordjs/rest'; +import {Routes} from 'discord-api-types/v9'; +import updatePermissionsForGuild from './utils/update-permissions-for-guild.js'; @injectable() export default class { private readonly client: Client; - private readonly naturalLanguage: NaturalLanguage; private readonly token: string; - private readonly commands!: Collection; + private readonly shouldRegisterCommandsOnBot: boolean; + private readonly commandsByName!: Collection; + private readonly commandsByButtonId!: Collection; constructor( @inject(TYPES.Client) client: Client, - @inject(TYPES.Services.NaturalLanguage) naturalLanguage: NaturalLanguage, @inject(TYPES.Config) config: Config, ) { this.client = client; - this.naturalLanguage = naturalLanguage; this.token = config.DISCORD_TOKEN; - this.commands = new Collection(); + this.shouldRegisterCommandsOnBot = config.REGISTER_COMMANDS_ON_BOT; + this.commandsByName = new Collection(); + this.commandsByButtonId = new Collection(); } - public async listen(): Promise { + public async register(): Promise { // Load in commands - container.getAll(TYPES.Command).forEach(command => { - const commandNames = [command.name, ...command.aliases]; - - commandNames.forEach(commandName => - this.commands.set(commandName, command), - ); - }); - - this.client.on('messageCreate', async (msg: Message) => { - // Get guild settings - if (!msg.guild) { - return; - } - - const settings = await prisma.setting.findUnique({ - where: { - guildId: msg.guild.id, - }, - }); - - if (!settings) { - // Got into a bad state, send owner welcome message - this.client.emit('guildCreate', msg.guild); - return; - } - - const {prefix, channel} = settings; - - if ( - !msg.content.startsWith(prefix) - && !msg.author.bot - && msg.channel.id === channel - && (await this.naturalLanguage.execute(msg)) - ) { - // Natural language command handled message - return; + for (const command of container.getAll(TYPES.Command)) { + // Make sure we can serialize to JSON without errors + try { + command.slashCommand.toJSON(); + } catch (error) { + console.error(error); + throw new Error(`Could not serialize /${command.slashCommand.name ?? ''} to JSON`); } - if ( - !msg.content.startsWith(prefix) - || msg.author.bot - || msg.channel.id !== channel - ) { - return; + if (command.slashCommand.name) { + this.commandsByName.set(command.slashCommand.name, command); } - let args = msg.content.slice(prefix.length).split(/ +/); - const command = args.shift()!.toLowerCase(); - - const shortcut = await prisma.shortcut.findFirst({ - where: { - guildId: msg.guild.id, - shortcut: command, - }, - }); - - let handler: Command; - - if (this.commands.has(command)) { - handler = this.commands.get(command)!; - } else if (shortcut) { - const possibleHandler = this.commands.get( - shortcut.command.split(' ')[0], - ); - - if (possibleHandler) { - handler = possibleHandler; - args = shortcut.command.split(/ +/).slice(1); - } else { - return; + if (command.handledButtonIds) { + for (const buttonId of command.handledButtonIds) { + this.commandsByButtonId.set(buttonId, command); } - } else { - return; } + } + // Register event handlers + this.client.on('interactionCreate', async interaction => { try { - if (handler.requiresVC && !isUserInVoice(msg.guild, msg.author)) { - await msg.channel.send(errorMsg('gotta be in a voice channel')); - return; + if (interaction.isCommand()) { + const command = this.commandsByName.get(interaction.commandName); + + if (!command) { + return; + } + + if (!interaction.guild) { + await interaction.reply(errorMsg('you can\'t use this bot in a DM')); + return; + } + + const requiresVC = command.requiresVC instanceof Function ? command.requiresVC(interaction) : command.requiresVC; + + if (requiresVC && interaction.member && !isUserInVoice(interaction.guild, interaction.member.user as User)) { + await interaction.reply({content: errorMsg('gotta be in a voice channel'), ephemeral: true}); + return; + } + + if (command.execute) { + await command.execute(interaction); + } + } else if (interaction.isButton()) { + const command = this.commandsByButtonId.get(interaction.customId); + + if (!command) { + return; + } + + if (command.handleButtonInteraction) { + await command.handleButtonInteraction(interaction); + } + } else if (interaction.isAutocomplete()) { + const command = this.commandsByName.get(interaction.commandName); + + if (!command) { + return; + } + + if (command.handleAutocompleteInteraction) { + await command.handleAutocompleteInteraction(interaction); + } } - - await handler.execute(msg, args); } catch (error: unknown) { debug(error); - await msg.channel.send( - errorMsg((error as Error).message.toLowerCase()), - ); + + // This can fail if the message was deleted, and we don't want to crash the whole bot + try { + if ((interaction.isApplicationCommand() || interaction.isButton()) && (interaction.replied || interaction.deferred)) { + await interaction.editReply(errorMsg(error as Error)); + } else if (interaction.isApplicationCommand() || interaction.isButton()) { + await interaction.reply({content: errorMsg(error as Error), ephemeral: true}); + } + } catch {} } }); const spinner = ora('📡 connecting to Discord...').start(); - this.client.on('ready', () => { + this.client.once('ready', async () => { debug(generateDependencyReport()); - spinner.succeed( - `Ready! Invite the bot with https://discordapp.com/oauth2/authorize?client_id=${this.client.user?.id ?? '' - }&scope=bot&permissions=36752448`, - ); + // Update commands + const rest = new REST({version: '9'}).setToken(this.token); + + if (this.shouldRegisterCommandsOnBot) { + spinner.text = '📡 updating commands on bot...'; + + await rest.put( + Routes.applicationCommands(this.client.user!.id), + {body: this.commandsByName.map(command => command.slashCommand.toJSON())}, + ); + } else { + spinner.text = '📡 updating commands in all guilds...'; + + await Promise.all([ + ...this.client.guilds.cache.map(async guild => { + await rest.put( + Routes.applicationGuildCommands(this.client.user!.id, guild.id), + {body: this.commandsByName.map(command => command.slashCommand.toJSON())}, + ); + }), + // Remove commands registered on bot (if they exist) + rest.put(Routes.applicationCommands(this.client.user!.id), {body: []}), + ], + ); + } + + // Update permissions + spinner.text = '📡 updating permissions...'; + await Promise.all(this.client.guilds.cache.map(async guild => updatePermissionsForGuild(guild))); + + spinner.succeed(`Ready! Invite the bot with https://discordapp.com/oauth2/authorize?client_id=${this.client.user?.id ?? ''}&scope=bot%20applications.commands&permissions=36700160`); }); this.client.on('error', console.error); this.client.on('debug', debug); - // Register event handlers this.client.on('guildCreate', handleGuildCreate); this.client.on('voiceStateUpdate', handleVoiceStateUpdate); + this.client.on('guildUpdate', handleGuildUpdate); - return this.client.login(this.token); + await this.client.login(this.token); } } -- cgit v1.2.3