From c8cd4967de1609cc30750986be811e017a69b878 Mon Sep 17 00:00:00 2001 From: amsyarasyiq <82711525+amsyarasyiq@users.noreply.github.com> Date: Tue, 27 Feb 2024 23:35:55 +0800 Subject: [PATCH] Implement core commands --- src/lib/commands.ts | 92 -------------------------------- src/lib/commands/core/debug.ts | 34 ++++++++++++ src/lib/commands/core/eval.ts | 41 ++++++++++++++ src/lib/commands/core/plugins.ts | 41 ++++++++++++++ src/lib/commands/index.ts | 53 ++++++++++++++++++ src/lib/commands/types.ts | 68 +++++++++++++++++++++++ src/lib/i18n/default.json | 9 +++- src/lib/metro/common.ts | 1 + 8 files changed, 246 insertions(+), 93 deletions(-) delete mode 100644 src/lib/commands.ts create mode 100644 src/lib/commands/core/debug.ts create mode 100644 src/lib/commands/core/eval.ts create mode 100644 src/lib/commands/core/plugins.ts create mode 100644 src/lib/commands/index.ts create mode 100644 src/lib/commands/types.ts diff --git a/src/lib/commands.ts b/src/lib/commands.ts deleted file mode 100644 index 9fa7be0..0000000 --- a/src/lib/commands.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { commands as commandsModule } from "@metro/common"; -import { after } from "@lib/patcher"; - -let commands: ApplicationCommand[] = []; - -export interface ApplicationCommand { - description: string; - name: string; - options: ApplicationCommandOption[]; - execute: (args: any[], ctx: CommandContext) => CommandResult | void | Promise | Promise; - id?: string; - applicationId: string; - displayName: string; - displayDescription: string; - inputType: ApplicationCommandInputType; - type: ApplicationCommandType; -} - -export enum ApplicationCommandInputType { - BUILT_IN, - BUILT_IN_TEXT, - BUILT_IN_INTEGRATION, - BOT, - PLACEHOLDER, -} - -export interface ApplicationCommandOption { - name: string; - description: string; - required?: boolean; - type: ApplicationCommandOptionType; - displayName: string; - displayDescription: string; -} - -export enum ApplicationCommandOptionType { - SUB_COMMAND = 1, - SUB_COMMAND_GROUP, - STRING, - INTEGER, - BOOLEAN, - USER, - CHANNEL, - ROLE, - MENTIONABLE, - NUMBER, - ATTACHMENT, -} - -export enum ApplicationCommandType { - CHAT = 1, - USER, - MESSAGE, -} - -export interface CommandContext { - channel: any; - guild: any; -} - -export interface CommandResult { - content: string; - tts?: boolean; -} - -export function patchCommands() { - const unpatch = after("getBuiltInCommands", commandsModule, ([type], res: ApplicationCommand[]) => { - if (type === ApplicationCommandType.CHAT) return res.concat(commands); - }); - - return () => { - commands = []; - unpatch(); - }; -} - -export function registerCommand(command: ApplicationCommand): () => void { - // Get built in commands - const builtInCommands = commandsModule.getBuiltInCommands(ApplicationCommandType.CHAT, true, false); - builtInCommands.sort((a: ApplicationCommand, b: ApplicationCommand) => parseInt(b.id!) - parseInt(a.id!)); - - const lastCommand = builtInCommands[builtInCommands.length - 1]; - - // Override the new command's id to the last command id - 1 - command.id = (parseInt(lastCommand.id, 10) - 1).toString(); - - // Add it to the commands array - commands.push(command); - - // Return command id so it can be unregistered - return () => (commands = commands.filter(({ id }) => id !== command.id)); -} diff --git a/src/lib/commands/core/debug.ts b/src/lib/commands/core/debug.ts new file mode 100644 index 0000000..538f5d4 --- /dev/null +++ b/src/lib/commands/core/debug.ts @@ -0,0 +1,34 @@ +import { Strings } from "@/lib/i18n"; +import { getDebugInfo } from "@lib/debug"; +import { messageUtil } from "@metro/common"; +import { ApplicationCommand, ApplicationCommandOptionType } from "../types"; + +export default () => { + name: "debug", + description: Strings.COMMAND_DEBUG_DESC, + options: [ + { + name: "ephemeral", + type: ApplicationCommandOptionType.BOOLEAN, + description: Strings.COMMAND_DEBUG_OPT_EPHEMERALLY, + } + ], + execute([ephemeral], ctx) { + const info = getDebugInfo(); + const content = [ + "**Bunny Debug Info**", + `> Bunny: ${info.vendetta.version} (${info.vendetta.loader})`, + `> Discord: ${info.discord.version} (${info.discord.build})`, + `> React: ${info.react.version} (RN ${info.react.nativeVersion})`, + `> Hermes: ${info.hermes.version} (bcv${info.hermes.bytecodeVersion})`, + `> System: ${info.os.name} ${info.os.version} (SDK ${info.os.sdk})`, + `> Device: ${info.device.model} (${info.device.codename})`, + ].join("\n"); + + if (ephemeral?.value) { + messageUtil.sendBotMessage(ctx.channel.id, content); + } else { + messageUtil.sendMessage(ctx.channel.id, { content }); + } + } +} \ No newline at end of file diff --git a/src/lib/commands/core/eval.ts b/src/lib/commands/core/eval.ts new file mode 100644 index 0000000..5128581 --- /dev/null +++ b/src/lib/commands/core/eval.ts @@ -0,0 +1,41 @@ +import { findByProps } from "@metro/filters"; +import { messageUtil } from "@metro/common"; +import { Strings } from "@/lib/i18n"; +import { ApplicationCommand, ApplicationCommandOptionType } from "../types"; + +const util = findByProps("inspect"); +const AsyncFunction = (async () => void 0).constructor; + +const ZERO_WIDTH_SPACE_CHARACTER = "\u200B"; + +function wrapInJSCodeblock(resString: string) { + return "```js\n" + resString.replaceAll("`", "`" + ZERO_WIDTH_SPACE_CHARACTER) + "\n```"; +} + +export default () => { + name: "eval", + description: Strings.COMMAND_EVAL_DESC, + options: [ + { + name: "code", + type: ApplicationCommandOptionType.STRING, + description: Strings.COMMAND_EVAL_OPT_CODE, + required: true + }, + { + name: "async", + type: ApplicationCommandOptionType.BOOLEAN, + description: Strings.COMMAND_EVAL_OPT_ASYNC, + } + ], + async execute([code, async], ctx) { + try { + const res = util.inspect(async?.value ? await AsyncFunction(code.value)() : eval?.(code.value)); + const trimmedRes = res.length > 2000 ? res.slice(0, 2000) + "..." : res; + + messageUtil.sendBotMessage(ctx.channel.id, wrapInJSCodeblock(trimmedRes)); + } catch (err: any) { + messageUtil.sendBotMessage(ctx.channel.id, wrapInJSCodeblock(err?.stack ?? err)); + } + } +} \ No newline at end of file diff --git a/src/lib/commands/core/plugins.ts b/src/lib/commands/core/plugins.ts new file mode 100644 index 0000000..01fc038 --- /dev/null +++ b/src/lib/commands/core/plugins.ts @@ -0,0 +1,41 @@ +import { messageUtil } from "@metro/common"; +import { plugins as pluginStorage } from "@lib/plugins"; +import { Strings } from "@/lib/i18n"; +import { ApplicationCommand, ApplicationCommandOptionType } from "../types"; + +export default () => { + name: "plugins", + description: Strings.COMMAND_DEBUG_DESC, + options: [ + { + name: "ephemeral", + displayName: "ephemeral", + type: ApplicationCommandOptionType.BOOLEAN, + description: Strings.COMMAND_DEBUG_OPT_EPHEMERALLY, + } + ], + execute([ephemeral], ctx) { + const plugins = Object.values(pluginStorage).sort((a, b) => a.manifest.name.localeCompare(b.manifest.name)); + + const enabled = plugins.filter(p => p.enabled).map(p => p.manifest.name); + const disabled = plugins.filter(p => !p.enabled).map(p => p.manifest.name); + + const content = [ + `**Installed Plugins (${plugins.length}):**`, + ...(enabled.length > 0 ? [ + `Enabled (${enabled.length}):`, + "> " + enabled.join(", "), + ] : []), + ...(disabled.length > 0 ? [ + `Disabled (${disabled.length}):`, + "> " + disabled.join(", "), + ] : []), + ].join("\n"); + + if (ephemeral?.value) { + messageUtil.sendBotMessage(ctx.channel.id, content); + } else { + messageUtil.sendMessage(ctx.channel.id, { content }); + } + } +} \ No newline at end of file diff --git a/src/lib/commands/index.ts b/src/lib/commands/index.ts new file mode 100644 index 0000000..61566d7 --- /dev/null +++ b/src/lib/commands/index.ts @@ -0,0 +1,53 @@ +import { commands as commandsModule } from "@metro/common"; +import { after } from "@lib/patcher"; +import { ApplicationCommand, ApplicationCommandType, ApplicationCommandInputType } from "./types"; + +let commands: ApplicationCommand[] = []; + +export function patchCommands() { + const unpatch = after("getBuiltInCommands", commandsModule, ([type], res: ApplicationCommand[]) => { + if (type === ApplicationCommandType.CHAT) return res.concat(commands); + }); + + // Register core commands + [ + require("@lib/commands/core/eval"), + require("@lib/commands/core/debug"), + require("@lib/commands/core/plugins") + ].forEach(r => registerCommand(r.default())); + + return () => { + commands = []; + unpatch(); + }; +} + +export function registerCommand(command: ApplicationCommand): () => void { + // Get built in commands + const builtInCommands = commandsModule.getBuiltInCommands(ApplicationCommandType.CHAT, true, false); + builtInCommands.sort((a: ApplicationCommand, b: ApplicationCommand) => parseInt(b.id!) - parseInt(a.id!)); + + const lastCommand = builtInCommands[builtInCommands.length - 1]; + + // Override the new command's id to the last command id - 1 + command.id = (parseInt(lastCommand.id, 10) - 1).toString(); + + // Fill optional args + command.__isBunny = true; + command.applicationId ??= "-1"; + command.type ??= ApplicationCommandType.CHAT; + command.inputType = ApplicationCommandInputType.BUILT_IN; + command.displayName ??= command.name; + command.displayDescription ??= command.description; + + if (command.options) for (const opt of command.options) { + opt.displayName ??= opt.name; + opt.displayDescription ??= opt.description; + } + + // Add it to the commands array + commands.push(command); + + // Return command id so it can be unregistered + return () => (commands = commands.filter(({ id }) => id !== command.id)); +} diff --git a/src/lib/commands/types.ts b/src/lib/commands/types.ts new file mode 100644 index 0000000..4029d3d --- /dev/null +++ b/src/lib/commands/types.ts @@ -0,0 +1,68 @@ +export interface Argument { + type: ApplicationCommandOptionType, + name: string, + value: string, + focused: undefined; + options: Argument[]; +} + +export interface ApplicationCommand { + name: string; + description: string; + execute: (args: Argument[], ctx: CommandContext) => CommandResult | void | Promise | Promise; + options: ApplicationCommandOption[]; + id?: string; + applicationId?: string; + displayName?: string; + displayDescription?: string; + inputType?: ApplicationCommandInputType; + type?: ApplicationCommandType; + __isBunny?: true; +} + +export enum ApplicationCommandInputType { + BUILT_IN, + BUILT_IN_TEXT, + BUILT_IN_INTEGRATION, + BOT, + PLACEHOLDER, +} + +export interface ApplicationCommandOption { + name: string; + description: string; + required?: boolean; + type: ApplicationCommandOptionType; + displayName?: string; + displayDescription?: string; +} + +export enum ApplicationCommandOptionType { + SUB_COMMAND = 1, + SUB_COMMAND_GROUP, + STRING, + INTEGER, + BOOLEAN, + USER, + CHANNEL, + ROLE, + MENTIONABLE, + NUMBER, + ATTACHMENT, +} + +export enum ApplicationCommandType { + CHAT = 1, + USER, + MESSAGE, +} + +export interface CommandContext { + channel: any; + guild: any; +} + +export interface CommandResult { + content: string; + tts?: boolean; +} \ No newline at end of file diff --git a/src/lib/i18n/default.json b/src/lib/i18n/default.json index 44f0071..2ebc63e 100644 --- a/src/lib/i18n/default.json +++ b/src/lib/i18n/default.json @@ -66,5 +66,12 @@ "UH_OH": "Uh oh.", "UNINSTALL": "Uninstall", "UNINSTALL_TITLE": "Uninstall {title}", - "VERSIONS": "Versions" + "VERSIONS": "Versions", + "COMMAND_DEBUG_DESC": "Send Vendetta debug info.", + "COMMAND_DEBUG_OPT_EPHEMERALLY": "Send debug info ephemerally.", + "COMMAND_EVAL_DESC": "Evaluate JavaScript code.", + "COMMAND_EVAL_OPT_CODE": "The code to evaluate.", + "COMMAND_EVAL_OPT_ASYNC": "Whether to support 'await' in code. Must explicitly return for result (default: false)", + "COMMAND_PLUGINS_DESC": "Send list of installed plugins.", + "COMMAND_PLUGINS_OPT_EPHEMERALLY": "Send plugins list ephemerally." } \ No newline at end of file diff --git a/src/lib/metro/common.ts b/src/lib/metro/common.ts index 1f86f77..7740b1a 100644 --- a/src/lib/metro/common.ts +++ b/src/lib/metro/common.ts @@ -50,6 +50,7 @@ export const assets = findByProps("registerAsset"); export const invites = findByProps("acceptInviteAndTransitionToInviteChannel"); export const commands = findByProps("getBuiltInCommands"); export const navigation = findByProps("pushLazy"); +export const messageUtil = findByProps("sendBotMessage"); export const navigationStack = findByProps("createStackNavigator"); export const NavigationNative = findByProps("NavigationContainer");