feat(bots/discord): framework changes and new features

- Migrated to a new command framework which looks better and works better
- Fixed commands not being bundled correctly
- Added message (prefix) commands with argument validation
- Added a new CommandErrorType, for invalid arguments
- `/eval` is now a bit safer
- Corrected colors for the coinflip embed
- `/stop` now works even when the bot is not connected to the API
This commit is contained in:
PalmDevs
2024-07-30 21:05:12 +07:00
parent a848a9c896
commit 646ec8da87
36 changed files with 1153 additions and 616 deletions

View File

@@ -1,78 +1,22 @@
import CommandError from '$/classes/CommandError'
import { isAdmin } from '$/utils/discord/permissions'
import { createErrorEmbed, createStackTraceEmbed } from '$utils/discord/embeds'
import { createStackTraceEmbed } from '$utils/discord/embeds'
import { on, withContext } from '$utils/discord/events'
withContext(on, 'interactionCreate', async (context, interaction) => {
if (!interaction.isChatInputCommand()) return
const { logger, discord, config } = context
const { logger, discord } = context
const command = discord.commands[interaction.commandName]
logger.debug(`Command ${interaction.commandName} being invoked by ${interaction.user.tag}`)
if (!command) return void logger.error(`Command ${interaction.commandName} not implemented but registered!!!`)
const isExecutorBotAdmin = isAdmin(await interaction.guild?.members.fetch(interaction.user.id) || interaction.user, config.admin)
/**
* Admin check
*/
if (command.adminOnly && !isExecutorBotAdmin)
return void (await interaction.reply({
embeds: [createErrorEmbed('Massive skill issue', 'This command can only be used by the bot admins.')],
ephemeral: true,
}))
/**
* Sanity check
*/
if (!command.global && !interaction.inGuild()) {
logger.error(`Command ${interaction.commandName} cannot be run in DMs, but was registered as global`)
await interaction.reply({
embeds: [createErrorEmbed('Cannot run that here', 'This command can only be used in a server.')],
ephemeral: true,
})
return
}
/**
* Permission checks
*/
if (interaction.inGuild()) {
// Bot owners get bypass
if (command.memberRequirements && !isExecutorBotAdmin) {
const { permissions = 0n, roles = [], mode } = command.memberRequirements
const member = await interaction.guild!.members.fetch(interaction.user.id)
const [missingPermissions, missingRoles] = [
// This command is an owner-only command (the user is not an owner, checked above)
permissions <= 0n ||
// or the user doesn't have the required permissions
(permissions > 0n && !interaction.memberPermissions.has(permissions)),
// If not:
!roles.some(x => member.roles.cache.has(x)),
]
if ((mode === 'any' && missingPermissions && missingRoles) || missingPermissions || missingRoles)
return void interaction.reply({
embeds: [
createErrorEmbed(
'Missing roles or permissions',
"You don't have the required roles or permissions to use this command.",
),
],
ephemeral: true,
})
}
}
try {
logger.debug(`Command ${interaction.commandName} being executed`)
await command.execute(context, interaction, { isExecutorBotAdmin })
await command.onInteraction(context, interaction)
} catch (err) {
logger.error(`Error while executing command ${interaction.commandName}:`, err)
if (!(err instanceof CommandError))
logger.error(`Error while executing command ${interaction.commandName}:`, err)
await interaction[interaction.replied ? 'followUp' : 'reply']({
embeds: [err instanceof CommandError ? err.toEmbed() : createStackTraceEmbed(err)],
ephemeral: true,

View File

@@ -0,0 +1,53 @@
import { type CommandArguments, CommandSpecialArgumentType } from '$/classes/Command'
import CommandError from '$/classes/CommandError'
import { createStackTraceEmbed } from '$utils/discord/embeds'
import { on, withContext } from '$utils/discord/events'
withContext(on, 'messageCreate', async (context, msg) => {
const { logger, discord, config } = context
if (msg.author.bot) return
const regex = new RegExp(`^(?:${config.prefix}|${msg.client.user.toString()}\\s*)([a-zA-Z-_]+)(?:\\s+)?(.+)?`)
const matches = msg.content.match(regex)
if (!matches) return
const [, commandName, argsString] = matches
if (!commandName) return
const command = discord.commands[commandName]
logger.debug(`Command ${commandName} being invoked by ${msg.author.id}`)
if (!command) return void logger.error(`Command ${commandName} not implemented`)
const argsRegex: RegExp = /[^\s"]+|"([^"]*)"/g
const args: CommandArguments = []
let match: RegExpExecArray | null
// biome-ignore lint/suspicious/noAssignInExpressions: nuh uh
while ((match = argsRegex.exec(argsString ?? '')) !== null) {
const arg = match[1] ? match[1] : match[0]
const mentionMatch = arg.match(/<(@(?:!|&)?|#)(.+?)>/)
if (mentionMatch) {
const [, prefix, id] = mentionMatch
if (!id || !prefix) {
args.push('')
continue
}
args.push({
type: CommandSpecialArgumentType[prefix[1] === '&' ? 'Role' : prefix[0] === '#' ? 'Channel' : 'User'],
id,
})
} else args.push(arg)
}
try {
logger.debug(`Command ${commandName} being executed`)
await command.onMessage(context, msg, args)
} catch (err) {
if (!(err instanceof CommandError)) logger.error(`Error while executing command ${commandName}:`, err)
await msg.reply({ embeds: [err instanceof CommandError ? err.toEmbed() : createStackTraceEmbed(err)] })
}
})

View File

@@ -13,7 +13,7 @@ withContext(on, 'messageCreate', async (context, msg) => {
} = context
if (!config || !config.responses) return
if (msg.author.bot && !config.scanBots)
if (msg.author.bot && !config.scanBots) return
if (!msg.inGuild() && !config.scanOutsideGuilds) return
if (msg.inGuild() && msg.member?.partial) await msg.member.fetch()
@@ -24,7 +24,11 @@ withContext(on, 'messageCreate', async (context, msg) => {
try {
logger.debug(`Classifying message ${msg.id}`)
const { response, label, replyToReplied } = await getResponseFromText(msg.content, filteredResponses, context)
const { response, label, replyToReplied } = await getResponseFromText(
msg.content,
filteredResponses,
context,
)
if (response) {
logger.debug('Response found')

View File

@@ -13,8 +13,8 @@ import {
import type { ConfigMessageScanResponseLabelConfig } from '$/../config.schema'
import { responses } from '$/database/schemas'
import { handleUserResponseCorrection } from '$/utils/discord/messageScan'
import { eq } from 'drizzle-orm'
import { isAdmin } from '$/utils/discord/permissions'
import { eq } from 'drizzle-orm'
const PossibleReactions = Object.values(Reactions) as string[]

View File

@@ -7,9 +7,7 @@ import { on, withContext } from 'src/utils/discord/events'
export default withContext(on, 'ready', ({ config, logger }, client) => {
logger.info(`Connected to Discord API, logged in as ${client.user.displayName} (@${client.user.tag})!`)
logger.info(
`Bot is in ${client.guilds.cache.size} guilds, if this is not expected, please run the /leave-unknowns command`,
)
logger.info(`Bot is in ${client.guilds.cache.size} guilds`)
if (config.rolePresets) {
removeExpiredPresets(client)