From d72542405af8873a542b55128d8b0c6311183235 Mon Sep 17 00:00:00 2001 From: Ven Date: Sat, 29 Oct 2022 20:45:31 +0200 Subject: [PATCH] Implement Subcommands; fix errors due to Settings <-> Plugins circular imports (#174) --- src/api/Commands/index.ts | 46 ++++++++++++++++++++++++++++++++++----- src/api/Commands/types.ts | 1 + src/api/settings.ts | 14 ++++++------ src/plugins/index.ts | 31 ++++++++++++++++---------- 4 files changed, 68 insertions(+), 24 deletions(-) diff --git a/src/api/Commands/index.ts b/src/api/Commands/index.ts index 3b42379d..a20ac500 100644 --- a/src/api/Commands/index.ts +++ b/src/api/Commands/index.ts @@ -18,7 +18,7 @@ import { makeCodeblock } from "../../utils/misc"; import { generateId, sendBotMessage } from "./commandHelpers"; -import { ApplicationCommandInputType, ApplicationCommandType, Argument, Command, CommandContext, Option } from "./types"; +import { ApplicationCommandInputType, ApplicationCommandOptionType, ApplicationCommandType, Argument, Command, CommandContext, Option } from "./types"; export * from "./commandHelpers"; export * from "./types"; @@ -79,7 +79,12 @@ export const _handleCommand = function (cmd: Command, args: Argument[], ctx: Com } } as never; -function modifyOpt(opt: Option | Command) { + +/** + * Prepare a Command Option for Discord by filling missing fields + * @param opt + */ +export function prepareOption(opt: O): O { opt.displayName ||= opt.name; opt.displayDescription ||= opt.description; opt.options?.forEach((opt, i, opts) => { @@ -88,11 +93,36 @@ function modifyOpt(opt: Option | Command) { else if (opt === ReqPlaceholder) opts[i] = RequiredMessageOption; opt.choices?.forEach(x => x.displayName ||= x.name); - modifyOpt(opts[i]); + prepareOption(opts[i]); + }); + return opt; +} + +// Yes, Discord registers individual commands for each subcommand +// TODO: This probably doesn't support nested subcommands. If that is ever needed, +// investigate +function registerSubCommands(cmd: Command, plugin: string) { + cmd.options?.forEach(o => { + if (o.type !== ApplicationCommandOptionType.SUB_COMMAND) + throw new Error("When specifying sub-command options, all options must be sub-commands."); + const subCmd = { + ...cmd, + ...o, + type: ApplicationCommandType.CHAT_INPUT, + name: `${cmd.name} ${o.name}`, + displayName: `${cmd.name} ${o.name}`, + subCommandPath: [{ + name: o.name, + type: o.type, + displayName: o.name + }], + rootCommand: cmd + }; + registerCommand(subCmd as any, plugin); }); } -export function registerCommand(command: Command, plugin: string) { +export function registerCommand(command: C, plugin: string) { if (!BUILT_IN) { console.warn( "[CommandsAPI]", @@ -112,7 +142,13 @@ export function registerCommand(command: Command, plugin: string) { command.inputType ??= ApplicationCommandInputType.BUILT_IN_TEXT; command.plugin ||= plugin; - modifyOpt(command); + prepareOption(command); + + if (command.options?.[0]?.type === ApplicationCommandOptionType.SUB_COMMAND) { + registerSubCommands(command, plugin); + return; + } + commands[command.name] = command; BUILT_IN.push(command); } diff --git a/src/api/Commands/types.ts b/src/api/Commands/types.ts index a40353f3..9acab664 100644 --- a/src/api/Commands/types.ts +++ b/src/api/Commands/types.ts @@ -81,6 +81,7 @@ export interface Argument { name: string; value: string; focused: undefined; + options: Argument[]; } export interface Command { diff --git a/src/api/settings.ts b/src/api/settings.ts index e25572f3..9e518c67 100644 --- a/src/api/settings.ts +++ b/src/api/settings.ts @@ -42,12 +42,6 @@ const DefaultSettings: Settings = { plugins: {} }; -for (const plugin in plugins) { - DefaultSettings.plugins[plugin] = { - enabled: plugins[plugin].required ?? false - }; -} - try { var settings = JSON.parse(VencordNative.ipc.sendSync(IpcEvents.GET_SETTINGS)) as Settings; mergeDefaults(settings, DefaultSettings); @@ -60,13 +54,19 @@ type SubscriptionCallback = ((newValue: any, path: string) => void) & { _path?: const subscriptions = new Set(); // Wraps the passed settings object in a Proxy to nicely handle change listeners and default values -function makeProxy(settings: Settings, root = settings, path = ""): Settings { +function makeProxy(settings: any, root = settings, path = ""): Settings { return new Proxy(settings, { get(target, p: string) { const v = target[p]; // using "in" is important in the following cases to properly handle falsy or nullish values if (!(p in target)) { + // Return empty for plugins with no settings + if (path === "plugins" && p in plugins) + return target[p] = makeProxy({ + enabled: plugins[p].required ?? false + }, root, `plugins/${p}`); + // Since the property is not set, check if this is a plugin's setting and if so, try to resolve // the default value. if (path.startsWith("plugins.")) { diff --git a/src/plugins/index.ts b/src/plugins/index.ts index fdb256c5..be6fae34 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -28,24 +28,31 @@ const logger = new Logger("PluginManager", "#a6d189"); export const plugins = Plugins; export const patches = [] as Patch[]; -for (const plugin of Object.values(Plugins)) if (plugin.patches && Settings.plugins[plugin.name].enabled) { - for (const patch of plugin.patches) { - patch.plugin = plugin.name; - if (!Array.isArray(patch.replacement)) patch.replacement = [patch.replacement]; - patches.push(patch); - } +export function isPluginEnabled(p: string) { + return (Settings.plugins[p]?.enabled || Plugins[p]?.required) ?? false; } -export function startAllPlugins() { - for (const name in Plugins) if (Settings.plugins[name].enabled) { - startPlugin(Plugins[name]); +for (const p of Object.values(Plugins)) + if (p.patches && isPluginEnabled(p.name)) { + for (const patch of p.patches) { + patch.plugin = p.name; + if (!Array.isArray(patch.replacement)) + patch.replacement = [patch.replacement]; + patches.push(patch); + } } + +export function startAllPlugins() { + for (const name in Plugins) + if (isPluginEnabled(name)) { + startPlugin(Plugins[name]); + } } export function startDependenciesRecursive(p: Plugin) { let restartNeeded = false; const failures: string[] = []; - if (p.dependencies) for (const dep of p.dependencies) { + p.dependencies?.forEach(dep => { if (!Settings.plugins[dep].enabled) { startDependenciesRecursive(Plugins[dep]); // If the plugin has patches, don't start the plugin, just enable it. @@ -53,12 +60,12 @@ export function startDependenciesRecursive(p: Plugin) { logger.warn(`Enabling dependency ${dep} requires restart.`); Settings.plugins[dep].enabled = true; restartNeeded = true; - continue; + return; } const result = startPlugin(Plugins[dep]); if (!result) failures.push(dep); } - } + }); return { restartNeeded, failures }; }