aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVendicated <vendicated@riseup.net>2022-10-12 22:22:21 +0200
committerVendicated <vendicated@riseup.net>2022-10-12 22:22:21 +0200
commit267b2b1a0703313311da01addafcee28100347ea (patch)
tree8e074ea220ba10d168f69b92ebc40ea608476ae7
parent83d480a68cfed2adee18b9a6b74634644cefaacc (diff)
downloadVencord-267b2b1a0703313311da01addafcee28100347ea.tar.gz
Vencord-267b2b1a0703313311da01addafcee28100347ea.tar.bz2
Vencord-267b2b1a0703313311da01addafcee28100347ea.zip
Commands: basic error handling
-rw-r--r--src/api/Commands.ts180
-rw-r--r--src/api/Commands/commandHelpers.ts42
-rw-r--r--src/api/Commands/index.ts100
-rw-r--r--src/api/Commands/types.ts77
-rw-r--r--src/plugins/apiCommands.ts14
-rw-r--r--src/utils/misc.tsx8
6 files changed, 240 insertions, 181 deletions
diff --git a/src/api/Commands.ts b/src/api/Commands.ts
deleted file mode 100644
index ba6c7fa..0000000
--- a/src/api/Commands.ts
+++ /dev/null
@@ -1,180 +0,0 @@
-import { Channel, Guild, Embed, Message } from "discord-types/general";
-import { lazyWebpack, mergeDefaults } from "../utils/misc";
-import { waitFor, findByProps, find, filters } from "../webpack";
-import type { PartialDeep } from "type-fest";
-
-const createBotMessage = lazyWebpack(filters.byCode('username:"Clyde"'));
-const MessageSender = lazyWebpack(filters.byProps([ "receiveMessage" ]));
-
-export function _init(cmds: Command[]) {
- try {
- BUILT_IN = cmds;
- OptionalMessageOption = cmds.find(c => c.name === "shrug")!.options![0];
- RequiredMessageOption = cmds.find(c => c.name === "me")!.options![0];
- } catch (e) {
- console.error("Failed to load CommandsApi");
- }
- return cmds;
-}
-
-export let BUILT_IN: Command[];
-export const commands = {} as Record<string, Command>;
-
-// hack for plugins being evaluated before we can grab these from webpack
-const OptPlaceholder = Symbol("OptionalMessageOption") as any as Option;
-const ReqPlaceholder = Symbol("RequiredMessageOption") as any as Option;
-/**
- * Optional message option named "message" you can use in commands.
- * Used in "tableflip" or "shrug"
- * @see {@link RequiredMessageOption}
- */
-export let OptionalMessageOption: Option = OptPlaceholder;
-/**
- * Required message option named "message" you can use in commands.
- * Used in "me"
- * @see {@link OptionalMessageOption}
- */
-export let RequiredMessageOption: Option = ReqPlaceholder;
-
-let SnowflakeUtils: any;
-waitFor("fromTimestamp", m => SnowflakeUtils = m);
-
-export function generateId() {
- return `-${SnowflakeUtils.fromTimestamp(Date.now())}`;
-}
-
-/**
- * Get the value of an option by name
- * @param args Arguments array (first argument passed to execute)
- * @param name Name of the argument
- * @param fallbackValue Fallback value in case this option wasn't passed
- * @returns Value
- */
-export function findOption<T>(args: Argument[], name: string): T & {} | undefined;
-export function findOption<T>(args: Argument[], name: string, fallbackValue: T): T & {};
-export function findOption(args: Argument[], name: string, fallbackValue?: any) {
- return (args.find(a => a.name === name)?.value || fallbackValue) as any;
-}
-
-function modifyOpt(opt: Option | Command) {
- opt.displayName ||= opt.name;
- opt.displayDescription ||= opt.description;
- opt.options?.forEach((opt, i, opts) => {
- // See comment above Placeholders
- if (opt === OptPlaceholder) opts[i] = OptionalMessageOption;
- else if (opt === ReqPlaceholder) opts[i] = RequiredMessageOption;
- modifyOpt(opts[i]);
- });
-}
-
-export function registerCommand(command: Command, plugin: string) {
- if (BUILT_IN.some(c => c.name === command.name))
- throw new Error(`Command '${command.name}' already exists.`);
-
- command.id ??= generateId();
- command.applicationId ??= "-1"; // BUILT_IN;
- command.type ??= ApplicationCommandType.CHAT_INPUT;
- command.inputType ??= ApplicationCommandInputType.BUILT_IN_TEXT;
- command.plugin ||= plugin;
-
- modifyOpt(command);
- commands[command.name] = command;
- BUILT_IN.push(command);
-}
-
-/**
- * Send a message as Clyde
- * @param {string} channelId ID of channel to send message to
- * @param {Message} message Message to send
- * @returns {Message}
- */
-export function sendBotMessage(channelId: string, message: PartialDeep<Message>) {
- const botMessage = createBotMessage({ channelId, content: "", embeds: [] });
-
- MessageSender.receiveMessage(channelId, mergeDefaults(message, botMessage));
-
- return message;
-}
-
-export function unregisterCommand(name: string) { 1;
- const idx = BUILT_IN.findIndex(c => c.name === name);
- if (idx === -1)
- return false;
-
- BUILT_IN.splice(idx, 1);
- delete commands[name];
-
- return true;
-}
-
-export interface CommandContext {
- channel: Channel;
- guild?: Guild;
-}
-
-export enum ApplicationCommandOptionType {
- SUB_COMMAND = 1,
- SUB_COMMAND_GROUP = 2,
- STRING = 3,
- INTEGER = 4,
- BOOLEAN = 5,
- USER = 6,
- CHANNEL = 7,
- ROLE = 8,
- MENTIONABLE = 9,
- NUMBER = 10,
- ATTACHMENT = 11,
-}
-
-export enum ApplicationCommandInputType {
- BUILT_IN = 0,
- BUILT_IN_TEXT = 1,
- BUILT_IN_INTEGRATION = 2,
- BOT = 3,
- PLACEHOLDER = 4,
-}
-
-export interface Option {
- name: string;
- displayName?: string;
- type: ApplicationCommandOptionType;
- description: string;
- displayDescription?: string;
- required?: boolean;
- options?: Option[];
-}
-
-export enum ApplicationCommandType {
- CHAT_INPUT = 1,
- USER = 2,
- MESSAGE = 3,
-}
-
-export interface CommandReturnValue {
- content: string;
-}
-
-export interface Argument {
- type: ApplicationCommandOptionType;
- name: string;
- value: string;
- focused: undefined;
-}
-
-export interface Command {
- id?: string;
- applicationId?: string;
- type?: ApplicationCommandType;
- inputType?: ApplicationCommandInputType;
- plugin?: string;
-
- name: string;
- displayName?: string;
- description: string;
- displayDescription?: string;
-
- options?: Option[];
- predicate?(ctx: CommandContext): boolean;
-
- execute(args: Argument[], ctx: CommandContext): CommandReturnValue | void | Promise<CommandReturnValue | void>;
-}
diff --git a/src/api/Commands/commandHelpers.ts b/src/api/Commands/commandHelpers.ts
new file mode 100644
index 0000000..544445b
--- /dev/null
+++ b/src/api/Commands/commandHelpers.ts
@@ -0,0 +1,42 @@
+import { filters, waitFor } from "../../webpack";
+import type { PartialDeep } from "type-fest";
+import { Message } from "discord-types/general";
+import { lazyWebpack, mergeDefaults } from "../../utils/misc";
+import { Argument } from "./types";
+
+const createBotMessage = lazyWebpack(filters.byCode('username:"Clyde"'));
+const MessageSender = lazyWebpack(filters.byProps(["receiveMessage"]));
+
+let SnowflakeUtils: any;
+waitFor("fromTimestamp", m => SnowflakeUtils = m);
+
+export function generateId() {
+ return `-${SnowflakeUtils.fromTimestamp(Date.now())}`;
+}
+
+/**
+ * Send a message as Clyde
+ * @param {string} channelId ID of channel to send message to
+ * @param {Message} message Message to send
+ * @returns {Message}
+ */
+export function sendBotMessage(channelId: string, message: PartialDeep<Message>): Message {
+ const botMessage = createBotMessage({ channelId, content: "", embeds: [] });
+
+ MessageSender.receiveMessage(channelId, mergeDefaults(message, botMessage));
+
+ return message as Message;
+}
+
+/**
+ * Get the value of an option by name
+ * @param args Arguments array (first argument passed to execute)
+ * @param name Name of the argument
+ * @param fallbackValue Fallback value in case this option wasn't passed
+ * @returns Value
+ */
+export function findOption<T>(args: Argument[], name: string): T & {} | undefined;
+export function findOption<T>(args: Argument[], name: string, fallbackValue: T): T & {};
+export function findOption(args: Argument[], name: string, fallbackValue?: any) {
+ return (args.find(a => a.name === name)?.value || fallbackValue) as any;
+}
diff --git a/src/api/Commands/index.ts b/src/api/Commands/index.ts
new file mode 100644
index 0000000..be65646
--- /dev/null
+++ b/src/api/Commands/index.ts
@@ -0,0 +1,100 @@
+import { makeCodeblock } from "../../utils/misc";
+import { generateId, sendBotMessage } from "./commandHelpers";
+import { ApplicationCommandInputType, ApplicationCommandType, Argument, Command, CommandContext, Option, CommandReturnValue } from "./types";
+
+export * from "./types";
+export * from "./commandHelpers";
+
+export let BUILT_IN: Command[];
+export const commands = {} as Record<string, Command>;
+
+// hack for plugins being evaluated before we can grab these from webpack
+const OptPlaceholder = Symbol("OptionalMessageOption") as any as Option;
+const ReqPlaceholder = Symbol("RequiredMessageOption") as any as Option;
+/**
+ * Optional message option named "message" you can use in commands.
+ * Used in "tableflip" or "shrug"
+ * @see {@link RequiredMessageOption}
+ */
+export let OptionalMessageOption: Option = OptPlaceholder;
+/**
+ * Required message option named "message" you can use in commands.
+ * Used in "me"
+ * @see {@link OptionalMessageOption}
+ */
+export let RequiredMessageOption: Option = ReqPlaceholder;
+
+export const _init = function (cmds: Command[]) {
+ try {
+ BUILT_IN = cmds;
+ OptionalMessageOption = cmds.find(c => c.name === "shrug")!.options![0];
+ RequiredMessageOption = cmds.find(c => c.name === "me")!.options![0];
+ } catch (e) {
+ console.error("Failed to load CommandsApi");
+ }
+ return cmds;
+} as never;
+
+export const _handleCommand = function (cmd: Command, args: Argument[], ctx: CommandContext) {
+ if (!cmd.isVencordCommand)
+ return cmd.execute(args, ctx);
+
+ const handleError = (err: any) => {
+ // TODO: cancel send if cmd.inputType === BUILT_IN_TEXT
+ const msg = `An Error occurred while executing command "${cmd.name}"`;
+ const reason = err instanceof Error ? err.stack || err.message : String(err);
+
+ console.error(msg, err);
+ sendBotMessage(ctx.channel.id, {
+ content: `${msg}:\n${makeCodeblock(reason)}`,
+ author: {
+ username: "Vencord"
+ }
+ });
+ };
+
+ try {
+ const res = cmd.execute(args, ctx);
+ return res instanceof Promise ? res.catch(handleError) : res;
+ } catch (err) {
+ return handleError(err);
+ }
+} as never;
+
+function modifyOpt(opt: Option | Command) {
+ opt.displayName ||= opt.name;
+ opt.displayDescription ||= opt.description;
+ opt.options?.forEach((opt, i, opts) => {
+ // See comment above Placeholders
+ if (opt === OptPlaceholder) opts[i] = OptionalMessageOption;
+ else if (opt === ReqPlaceholder) opts[i] = RequiredMessageOption;
+ modifyOpt(opts[i]);
+ });
+}
+
+export function registerCommand(command: Command, plugin: string) {
+ if (BUILT_IN.some(c => c.name === command.name))
+ throw new Error(`Command '${command.name}' already exists.`);
+
+ command.isVencordCommand = true;
+ command.id ??= generateId();
+ command.applicationId ??= "-1"; // BUILT_IN;
+ command.type ??= ApplicationCommandType.CHAT_INPUT;
+ command.inputType ??= ApplicationCommandInputType.BUILT_IN_TEXT;
+ command.plugin ||= plugin;
+
+ modifyOpt(command);
+ commands[command.name] = command;
+ BUILT_IN.push(command);
+}
+
+export function unregisterCommand(name: string) {
+ const idx = BUILT_IN.findIndex(c => c.name === name);
+ if (idx === -1)
+ return false;
+
+ BUILT_IN.splice(idx, 1);
+ delete commands[name];
+
+ return true;
+}
diff --git a/src/api/Commands/types.ts b/src/api/Commands/types.ts
new file mode 100644
index 0000000..d50db3c
--- /dev/null
+++ b/src/api/Commands/types.ts
@@ -0,0 +1,77 @@
+import { Channel, Guild } from "discord-types/general";
+import { Promisable } from "type-fest";
+
+export interface CommandContext {
+ channel: Channel;
+ guild?: Guild;
+}
+
+export enum ApplicationCommandOptionType {
+ SUB_COMMAND = 1,
+ SUB_COMMAND_GROUP = 2,
+ STRING = 3,
+ INTEGER = 4,
+ BOOLEAN = 5,
+ USER = 6,
+ CHANNEL = 7,
+ ROLE = 8,
+ MENTIONABLE = 9,
+ NUMBER = 10,
+ ATTACHMENT = 11,
+}
+
+export enum ApplicationCommandInputType {
+ BUILT_IN = 0,
+ BUILT_IN_TEXT = 1,
+ BUILT_IN_INTEGRATION = 2,
+ BOT = 3,
+ PLACEHOLDER = 4,
+}
+
+export interface Option {
+ name: string;
+ displayName?: string;
+ type: ApplicationCommandOptionType;
+ description: string;
+ displayDescription?: string;
+ required?: boolean;
+ options?: Option[];
+}
+
+export enum ApplicationCommandType {
+ CHAT_INPUT = 1,
+ USER = 2,
+ MESSAGE = 3,
+}
+
+export interface CommandReturnValue {
+ content: string;
+ /** TODO: implement */
+ cancel?: boolean;
+}
+
+export interface Argument {
+ type: ApplicationCommandOptionType;
+ name: string;
+ value: string;
+ focused: undefined;
+}
+
+export interface Command {
+ id?: string;
+ applicationId?: string;
+ type?: ApplicationCommandType;
+ inputType?: ApplicationCommandInputType;
+ plugin?: string;
+ isVencordCommand?: boolean;
+
+ name: string;
+ displayName?: string;
+ description: string;
+ displayDescription?: string;
+
+ options?: Option[];
+ predicate?(ctx: CommandContext): boolean;
+
+ execute(args: Argument[], ctx: CommandContext): Promisable<void | CommandReturnValue>;
+}
diff --git a/src/plugins/apiCommands.ts b/src/plugins/apiCommands.ts
index 449f804..08fcec9 100644
--- a/src/plugins/apiCommands.ts
+++ b/src/plugins/apiCommands.ts
@@ -6,17 +6,29 @@ export default definePlugin({
authors: [Devs.Arjix],
description: "Api required by anything that uses commands",
patches: [
+ // obtain BUILT_IN_COMMANDS instance
{
find: '"giphy","tenor"',
replacement: [
{
// Matches BUILT_IN_COMMANDS. This is not exported so this is
// the only way. _init() just returns the same object to make the
- // patch simpler, the resulting code is x=Vencord.Api.Commands._init(y).filter(...)
+ // patch simpler
+
+ // textCommands = builtInCommands.filter(...)
match: /(?<=\w=)(\w)(\.filter\(.{0,30}giphy)/,
replace: "Vencord.Api.Commands._init($1)$2",
}
],
+ },
+ // command error handling
+ {
+ find: "Unexpected value for option",
+ replacement: {
+ // return [2, cmd.execute(args, ctx)]
+ match: /,(.{1,2})\.execute\((.{1,2}),(.{1,2})\)]/,
+ replace: (_, cmd, args, ctx) => `,Vencord.Api.Commands._handleCommand(${cmd}, ${args}, ${ctx})]`
+ }
}
],
});
diff --git a/src/utils/misc.tsx b/src/utils/misc.tsx
index dfeb330..b646ec1 100644
--- a/src/utils/misc.tsx
+++ b/src/utils/misc.tsx
@@ -157,3 +157,11 @@ export function suppressErrors<F extends Function>(name: string, func: F, thisOb
}
}) as any as F;
}
+
+/**
+ * Wrap the text in ``` with an optional language
+ */
+export function makeCodeblock(text: string, language?: string) {
+ const chars = "```";
+ return `${chars}${language || ""}\n${text}\n${chars}`;
+}