diff --git a/bots/discord/src/commands/admin/eval.ts b/bots/discord/src/commands/admin/eval.ts index fc67146..9a13bcc 100644 --- a/bots/discord/src/commands/admin/eval.ts +++ b/bots/discord/src/commands/admin/eval.ts @@ -1,8 +1,12 @@ +import { unlinkSync, writeFileSync } from 'fs' +import { join } from 'path' import { inspect } from 'util' +import { runInNewContext } from 'vm' import { ApplicationCommandOptionType } from 'discord.js' import { AdminCommand } from '$/classes/Command' import { createSuccessEmbed } from '$/utils/discord/embeds' +import { parseDuration } from '$/utils/duration' export default new AdminCommand({ name: 'eval', @@ -18,26 +22,73 @@ export default new AdminCommand({ type: ApplicationCommandOptionType.Boolean, required: false, }, + ['inspect-depth']: { + description: 'How many times to recurse while formatting the object (default: 1)', + type: ApplicationCommandOptionType.Integer, + required: false, + }, + timeout: { + description: 'Timeout for the evaluation (default: 10s)', + type: ApplicationCommandOptionType.String, + required: false, + }, }, - async execute(context, trigger, { code, 'show-hidden': showHidden }) { - // So it doesn't show up as unused, and we can use it in `code` - context + async execute(context, trigger, { code, 'show-hidden': showHidden, timeout, ['inspect-depth']: inspectDepth }) { + const currentToken = context.discord.client.token + const currentEnvToken = process.env['DISCORD_TOKEN'] + context.discord.client.token = null + process.env['DISCORD_TOKEN'] = undefined + + // This allows developers to access and modify the context object to apply changes + // to the bot while the bot is running, minus malicious actors getting the token to perform malicious actions + const output = await runInNewContext( + code, + { + ...globalThis, + context, + }, + { + timeout: parseDuration(timeout ?? '10s'), + filename: 'eval', + displayErrors: true, + }, + ) + + context.discord.client.token = currentToken + process.env['DISCORD_TOKEN'] = currentEnvToken + + const inspectedOutput = inspect(output, { + depth: inspectDepth ?? 1, + showHidden, + getters: showHidden, + numericSeparator: true, + showProxy: showHidden, + }) + + const embed = createSuccessEmbed('Evaluate', `\`\`\`js\n${code}\`\`\``) + const files: string[] = [] + const filepath = join(Bun.main, '..', `output-eval-${Date.now()}.js`) + + if (inspectedOutput.length > 1000) { + writeFileSync(filepath, inspectedOutput) + files.push(filepath) + + embed.addFields({ + name: 'Result', + value: '```js\n// (output too long, file uploaded)```', + }) + } else + embed.addFields({ + name: 'Result', + value: `\`\`\`js\n${inspectedOutput}\`\`\``, + }) await trigger.reply({ ephemeral: true, - embeds: [ - createSuccessEmbed('Evaluate', `\`\`\`js\n${code}\`\`\``).addFields({ - name: 'Result', - // biome-ignore lint/security/noGlobalEval: This is fine as it's an admin command - value: `\`\`\`js\n${inspect(await eval(code), { - depth: 1, - showHidden, - getters: true, - numericSeparator: true, - showProxy: true, - })}\`\`\``, - }), - ], + embeds: [embed], + files, }) + + if (files.length) unlinkSync(filepath) }, }) diff --git a/bots/discord/src/commands/admin/exception-test.ts b/bots/discord/src/commands/admin/exception-test.ts index 68ac9c8..a9017ca 100644 --- a/bots/discord/src/commands/admin/exception-test.ts +++ b/bots/discord/src/commands/admin/exception-test.ts @@ -11,7 +11,10 @@ export default new AdminCommand({ description: 'The type of exception to throw', type: ApplicationCommandOptionType.String, required: true, - choices: Object.keys(CommandErrorType).map(k => ({ name: k, value: k })), + choices: [ + { name: 'Process', value: 'Process' }, + ...Object.keys(CommandErrorType).map(k => ({ name: k, value: k })), + ], }, }, async execute(_, __, { type }) { diff --git a/bots/discord/src/commands/admin/reload.ts b/bots/discord/src/commands/admin/reload.ts index ada207c..6fcc71d 100644 --- a/bots/discord/src/commands/admin/reload.ts +++ b/bots/discord/src/commands/admin/reload.ts @@ -1,5 +1,5 @@ +import { dirname, join } from 'path' import { AdminCommand } from '$/classes/Command' -import { join, dirname } from 'path' import type { Config } from 'config.schema' @@ -8,7 +8,12 @@ export default new AdminCommand({ description: 'Reload configuration', async execute(context, trigger) { const { api, logger, discord } = context - context.config = ((await import(join(dirname(Bun.main), '..', 'config.js'))) as { default: Config }).default + logger.info(`Reload triggered by ${context.executor.tag} (${context.executor.id})`) + + // Apparently the query strings only work with non-Windows "URLs", otherwise it'd just infinitely hang + const path = `${Bun.pathToFileURL(join(dirname(Bun.main), '..', 'config.js')).toString()}?cache=${Date.now()}` + logger.debug(`Reloading configuration from: ${path}`) + context.config = ((await import(path)) as { default: Config }).default if ('deferReply' in trigger) await trigger.deferReply({ ephemeral: true }) @@ -32,6 +37,6 @@ export default new AdminCommand({ await discord.client.login(process.env['DISCORD_TOKEN']) // @ts-expect-error: TypeScript dum - await trigger[('deferReply' in trigger ? 'editReply' : 'reply')]({ content: 'Reloaded configuration' }) + await trigger['deferReply' in trigger ? 'editReply' : 'reply']({ content: 'Reloaded configuration' }) }, })