Implement core commands

This commit is contained in:
amsyarasyiq 2024-02-27 23:35:55 +08:00
parent 2ff46796bd
commit c8cd4967de
8 changed files with 246 additions and 93 deletions

View file

@ -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<CommandResult> | Promise<void>;
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));
}

View file

@ -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 () => <ApplicationCommand>{
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 });
}
}
}

View file

@ -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 () => <ApplicationCommand>{
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));
}
}
}

View file

@ -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 () => <ApplicationCommand>{
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 });
}
}
}

53
src/lib/commands/index.ts Normal file
View file

@ -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));
}

68
src/lib/commands/types.ts Normal file
View file

@ -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<CommandResult> | Promise<void>;
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;
}

View file

@ -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."
}

View file

@ -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");