diff options
Diffstat (limited to 'src/commands/dev/eval.ts')
-rw-r--r-- | src/commands/dev/eval.ts | 206 |
1 files changed, 118 insertions, 88 deletions
diff --git a/src/commands/dev/eval.ts b/src/commands/dev/eval.ts index 3c9b54c..fe8703e 100644 --- a/src/commands/dev/eval.ts +++ b/src/commands/dev/eval.ts @@ -2,8 +2,10 @@ import { ActivePunishment, BushCommand, + BushInspectOptions, BushMessage, BushSlashMessage, + CodeBlockLang, Global, Guild, Level, @@ -13,6 +15,7 @@ import { type ArgType } from '#lib'; import { Snowflake as Snowflake_ } from '@sapphire/snowflake'; +import assert from 'assert'; import { Canvas } from 'canvas'; import { exec } from 'child_process'; import { @@ -37,6 +40,7 @@ import { SelectMenuComponent, Util } from 'discord.js'; +import got from 'got'; import path from 'path'; import ts from 'typescript'; import { fileURLToPath } from 'url'; @@ -49,13 +53,18 @@ const { transpile } = ts, __dirname = path.dirname(fileURLToPath(import.meta.url)); /* eslint-enable @typescript-eslint/no-unused-vars */ +// prettier-ignore +util.assertAll(ActivePunishment, BushCommand, BushMessage, BushSlashMessage, Global, Guild, Level, ModLog, Shared, StickyRole, Snowflake_, Canvas, exec, ActionRow, ButtonComponent, ButtonInteraction, Collection, Collector, CommandInteraction, ContextMenuCommandInteraction, DMChannel, Embed, Emoji, Interaction, InteractionCollector, Message, MessageAttachment, MessageCollector, PermissionsBitField, ReactionCollector, SelectMenuComponent, Util, path, ts, fileURLToPath, promisify, assert, got, transpile, emojis, colors, sh, SnowflakeUtil, __dirname); + export default class EvalCommand extends BushCommand { public constructor() { super('eval', { aliases: ['eval', 'ev', 'evaluate'], category: 'dev', description: 'Evaluate code.', - usage: ['eval <code> [--depth #] [--sudo] [--silent] [--delete] [--proto] [--hidden] [--ts]'], + usage: [ + 'eval <code> [--depth #] [--sudo] [--delete] [--silent] [--ts] [--hidden] [--proto] [--methods] [--async] [--strings]' + ], examples: ['eval message.channel.delete()'], args: [ { @@ -109,7 +118,7 @@ export default class EvalCommand extends BushCommand { id: 'typescript', description: 'Whether or not to treat the code as typescript and transpile it.', match: 'flag', - flag: '--ts', + flag: ['--ts', '--typescript'], prompt: 'Is this code written in typescript?', slashType: ApplicationCommandOptionType.Boolean, optional: true @@ -169,12 +178,24 @@ export default class EvalCommand extends BushCommand { public override async exec( message: BushMessage | BushSlashMessage, - args: { + { + code: argCode, + sel_depth: selDepth, + sudo, + silent, + delete_msg: deleteMsg, + typescript, + hidden, + show_proto: showProto, + show_methods: showMethods, + async, + no_inspect_strings: noInspectStrings + }: { code: ArgType<'string'>; sel_depth: ArgType<'integer'>; sudo: ArgType<'boolean'>; silent: ArgType<'boolean'>; - deleteMSG: ArgType<'boolean'>; + delete_msg: ArgType<'boolean'>; typescript: ArgType<'boolean'>; hidden: ArgType<'boolean'>; show_proto: ArgType<'boolean'>; @@ -185,105 +206,114 @@ export default class EvalCommand extends BushCommand { ) { if (!message.author.isOwner()) return await message.util.reply(`${util.emojis.error} Only my developers can run this command.`); - if (message.util.isSlashMessage(message)) { - await message.interaction.deferReply({ ephemeral: args.silent }); + if (message.util.isSlashMessage(message)) await message.interaction.deferReply({ ephemeral: silent }); + + if (!sudo && ['delete', 'destroy'].some((p) => argCode.includes(p))) { + return await message.util.send(`${util.emojis.error} This eval was blocked by smooth brain protection™.`); } - const isTypescript = args.typescript || args.code.includes('```ts'); - let rawCode = args.code.replace(/[“”]/g, '"').replace(/```*(?:js|ts)?/g, ''); - if (args.async) rawCode = `(async () => {${rawCode}})()`; - const code: { ts: string | null; js: string; lang: 'ts' | 'js' } = { + const isTypescript = typescript || argCode.includes('```ts'); + let rawCode = argCode.replace(/[“”]/g, '"').replace(/```*(?:js|ts)?/g, ''); + if (async) { + if (rawCode.includes(';') && (!rawCode.endsWith(';') || rawCode.includes('\n') || rawCode.split(';').length > 2)) + rawCode = `(async () => {${rawCode}})()`; + else rawCode = `(async () => { return ${rawCode} })()`; + } + + const code = { ts: isTypescript ? rawCode : null, - js: isTypescript - ? transpile(rawCode, { - module: ts.ModuleKind.ESNext, - target: ts.ScriptTarget.ESNext, - moduleResolution: ts.ModuleResolutionKind.NodeNext, - lib: ['esnext'], - sourceMap: true, - incremental: true, - experimentalDecorators: true, - emitDecoratorMetadata: true, - resolveJsonModule: true, - noImplicitOverride: true, - noErrorTruncation: true, - strict: true - }) - : rawCode, - lang: isTypescript ? 'ts' : 'js' + js: isTypescript ? this.transpile(rawCode) : rawCode, + lang: isTypescript ? ('ts' as const) : ('js' as const) }; - const embed = new Embed(); - const badPhrases = ['delete', 'destroy']; - - if (badPhrases.some((p) => code[code.lang]!.includes(p)) && !args.sudo) { - return await message.util.send(`${util.emojis.error} This eval was blocked by smooth brain protection™.`); - } + const embed = new Embed().setFooter({ text: message.author.tag, iconURL: message.author.displayAvatarURL() ?? undefined }); - /* eslint-disable @typescript-eslint/no-unused-vars */ - const me = message.member, - member = message.member, - bot = client, - guild = message.guild, - channel = message.channel, - config = client.config, - members = message.guild?.members, - roles = message.guild?.roles; - /* eslint-enable @typescript-eslint/no-unused-vars */ + let err = false; + let rawResult: any; - const inputJS = await util.inspectCleanRedactCodeblock(code.js, 'js'); - const inputTS = code.lang === 'ts' ? await util.inspectCleanRedactCodeblock(code.ts, 'ts') : undefined; try { - const rawOutput = /^(9\s*?\+\s*?10)|(10\s*?\+\s*?9)$/.test(code[code.lang]!) ? '21' : await eval(code.js); - const output = await util.inspectCleanRedactCodeblock(rawOutput, 'js', { - depth: args.sel_depth ?? 0, - showHidden: args.hidden, - getters: true, - showProxy: true, - inspectStrings: !args.no_inspect_strings - }); - const methods = args.show_methods ? await util.inspectCleanRedactCodeblock(util.getMethods(rawOutput), 'js') : undefined; - const proto = args.show_proto - ? await util.inspectCleanRedactCodeblock(Object.getPrototypeOf(rawOutput), 'js', { - depth: 1, - getters: true, - showHidden: true - }) - : undefined; + /* eslint-disable @typescript-eslint/no-unused-vars */ + const me = message.member, + member = message.member, + bot = client, + guild = message.guild, + channel = message.channel, + config = client.config, + members = message.guild?.members, + roles = message.guild?.roles; + /* eslint-enable @typescript-eslint/no-unused-vars */ - embed.setTitle(`${emojis.successFull} Successfully Evaluated Expression`).setColor(colors.success); - if (inputTS) - embed - .addField({ name: '📥 Input (typescript)', value: inputTS }) - .addField({ name: '📥 Input (transpiled javascript)', value: inputJS }); - else embed.addField({ name: '📥 Input', value: inputJS }); - embed.addField({ name: '📤 Output', value: output }); - if (methods) embed.addField({ name: '🔧 Methods', value: methods }); - if (proto) embed.addField({ name: '⚙️ Proto', value: proto }); + rawResult = /^(9\s*?\+\s*?10)|(10\s*?\+\s*?9)$/.test(code[code.lang]!) ? '21' : await eval(code.js); } catch (e) { - embed.setTitle(`${emojis.errorFull} Unable to Evaluate Expression`).setColor(colors.error); - if (inputTS) - embed - .addField({ name: '📥 Input (typescript)', value: inputTS }) - .addField({ name: '📥 Input (transpiled javascript)', value: inputJS }); - else embed.addField({ name: '📥 Input', value: inputJS }); - embed.addField({ name: '📤 Error', value: await util.inspectCleanRedactCodeblock(e, 'js') }); + err = true; + rawResult = e; } - embed.setTimestamp().setFooter({ text: message.author.tag, iconURL: message.author.displayAvatarURL() ?? undefined }); + const inputJS = await this.codeblock(code.js, 'js'); + const inputTS = code.lang === 'ts' ? await this.codeblock(code.ts, 'ts') : undefined; + + embed.setTimestamp(); + + if (inputTS) embed.addField({ name: ':inbox_tray: Input (typescript)', value: inputTS }); + else embed.addField({ name: `:inbox_tray: Input${inputTS ? ' (transpiled javascript)' : ''}`, value: inputJS }); + + const output = await this.codeblock(rawResult, 'js', { + depth: selDepth ?? 0, + showHidden: hidden, + showProxy: true, + inspectStrings: !noInspectStrings + }); + + const methods = !err && showMethods ? await this.codeblock(rawResult, 'ts', { methods: true }) : undefined; + const proto = !err && showProto ? await this.codeblock(rawResult, 'js', { showHidden: true, prototype: true }) : undefined; - if (!args.silent || message.util.isSlashMessage(message)) { - await message.util.reply({ embeds: [embed] }); + embed + .setTitle(`${emojis[err ? 'errorFull' : 'successFull']} ${err ? 'Uns' : 'S'}uccessfully Evaluated Expression`) + .setColor(colors[err ? 'error' : 'success']) + .addField({ name: `:outbox_tray: ${err ? 'Error' : 'Output'}`, value: output }); + + if (!err && methods) embed.addField({ name: ':wrench: Methods', value: methods }); + if (!err && proto) embed.addField({ name: ':gear: Proto', value: proto }); + + if (!silent || message.util.isSlashMessage(message)) { + await message.util.reply({ content: null, embeds: [embed] }); } else { - try { - await message.author.send({ embeds: [embed] }); - if (!args.deleteMSG) await message.react(emojis.successFull); - } catch { - if (!args.deleteMSG) await message.react(emojis.errorFull); - } + const success = await message.author.send({ embeds: [embed] }).catch(() => false); + if (!deleteMsg) await message.react(success ? emojis.successFull : emojis.errorFull).catch(() => {}); } - if (args.deleteMSG && 'deletable' in message && message.deletable) await message.delete(); + + if (deleteMsg && 'deletable' in message && message.deletable) await message.delete().catch(() => {}); + } + + private transpile(code: string): string { + const transpileOptions = { + module: ts.ModuleKind.ESNext, + target: ts.ScriptTarget.ESNext, + moduleResolution: ts.ModuleResolutionKind.NodeNext, + lib: ['esnext'], + experimentalDecorators: true, + emitDecoratorMetadata: true, + resolveJsonModule: true + }; + + return transpile(code, transpileOptions); } + + private async codeblock(obj: any, language: CodeBlockLang, options: CodeBlockCustomOptions = {}) { + if (options.prototype) obj = Object.getPrototypeOf(obj); + if (options.methods) obj = util.getMethods(obj); + + options.depth ??= 1; + options.getters ??= true; + + return util.inspectCleanRedactCodeblock(obj, language, options); + } +} + +type CodeBlockOptions = BushInspectOptions & { inspectStrings?: boolean }; +interface CodeBlockCustomOptions extends CodeBlockOptions { + prototype?: boolean; + methods?: boolean; } -/** @typedef {ActivePunishment|Global|Guild|Level|ModLog|StickyRole|ButtonInteraction|Collection|Collector|CommandInteraction|ContextMenuCommandInteraction|DMChannel|Emoji|Interaction|InteractionCollector|Message|ActionRow|MessageAttachment|ButtonComponent|MessageCollector|SelectMenuComponent|ReactionCollector|Util|Canvas|Shared|PermissionsBitField} VSCodePleaseDontRemove */ +/** @typedef {ActivePunishment|Global|Guild|Level|ModLog|StickyRole|ButtonInteraction|Collection|Collector|CommandInteraction|ContextMenuCommandInteraction|DMChannel|Emoji|Interaction|InteractionCollector|Message|ActionRow|MessageAttachment|ButtonComponent|MessageCollector|SelectMenuComponent|ReactionCollector|Util|Canvas|Shared|PermissionsBitField|got} VSCodePleaseDontRemove */ |