diff options
Diffstat (limited to 'src/api')
| -rw-r--r-- | src/api/Commands.ts | 180 | ||||
| -rw-r--r-- | src/api/Commands/commandHelpers.ts | 42 | ||||
| -rw-r--r-- | src/api/Commands/index.ts | 100 | ||||
| -rw-r--r-- | src/api/Commands/types.ts | 77 | 
4 files changed, 219 insertions, 180 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>; +} | 
