mirror of
https://github.com/ReVanced/revanced-bots.git
synced 2026-01-11 13:56:15 +00:00
feat(bots/discord): improve admin commands
- The reload command now properly reloads configuration changes by skipping the configuration cache - The eval command now sends a file if the output is too long - The eval command now restricts access to the bot token by removing it and sandboxing the input code execution
This commit is contained in:
@@ -1,8 +1,12 @@
|
|||||||
|
import { unlinkSync, writeFileSync } from 'fs'
|
||||||
|
import { join } from 'path'
|
||||||
import { inspect } from 'util'
|
import { inspect } from 'util'
|
||||||
|
import { runInNewContext } from 'vm'
|
||||||
import { ApplicationCommandOptionType } from 'discord.js'
|
import { ApplicationCommandOptionType } from 'discord.js'
|
||||||
|
|
||||||
import { AdminCommand } from '$/classes/Command'
|
import { AdminCommand } from '$/classes/Command'
|
||||||
import { createSuccessEmbed } from '$/utils/discord/embeds'
|
import { createSuccessEmbed } from '$/utils/discord/embeds'
|
||||||
|
import { parseDuration } from '$/utils/duration'
|
||||||
|
|
||||||
export default new AdminCommand({
|
export default new AdminCommand({
|
||||||
name: 'eval',
|
name: 'eval',
|
||||||
@@ -18,26 +22,73 @@ export default new AdminCommand({
|
|||||||
type: ApplicationCommandOptionType.Boolean,
|
type: ApplicationCommandOptionType.Boolean,
|
||||||
required: false,
|
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 }) {
|
async execute(context, trigger, { code, 'show-hidden': showHidden, timeout, ['inspect-depth']: inspectDepth }) {
|
||||||
// So it doesn't show up as unused, and we can use it in `code`
|
const currentToken = context.discord.client.token
|
||||||
context
|
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({
|
await trigger.reply({
|
||||||
ephemeral: true,
|
ephemeral: true,
|
||||||
embeds: [
|
embeds: [embed],
|
||||||
createSuccessEmbed('Evaluate', `\`\`\`js\n${code}\`\`\``).addFields({
|
files,
|
||||||
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,
|
|
||||||
})}\`\`\``,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (files.length) unlinkSync(filepath)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ export default new AdminCommand({
|
|||||||
description: 'The type of exception to throw',
|
description: 'The type of exception to throw',
|
||||||
type: ApplicationCommandOptionType.String,
|
type: ApplicationCommandOptionType.String,
|
||||||
required: true,
|
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 }) {
|
async execute(_, __, { type }) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { dirname, join } from 'path'
|
||||||
import { AdminCommand } from '$/classes/Command'
|
import { AdminCommand } from '$/classes/Command'
|
||||||
import { join, dirname } from 'path'
|
|
||||||
|
|
||||||
import type { Config } from 'config.schema'
|
import type { Config } from 'config.schema'
|
||||||
|
|
||||||
@@ -8,7 +8,12 @@ export default new AdminCommand({
|
|||||||
description: 'Reload configuration',
|
description: 'Reload configuration',
|
||||||
async execute(context, trigger) {
|
async execute(context, trigger) {
|
||||||
const { api, logger, discord } = context
|
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 })
|
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'])
|
await discord.client.login(process.env['DISCORD_TOKEN'])
|
||||||
|
|
||||||
// @ts-expect-error: TypeScript dum
|
// @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' })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user