Implement Subcommands; fix errors due to Settings <-> Plugins circular imports (#174)

This commit is contained in:
Ven 2022-10-29 20:45:31 +02:00 committed by GitHub
parent 95aa2d9d8d
commit d72542405a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 68 additions and 24 deletions

View file

@ -18,7 +18,7 @@
import { makeCodeblock } from "../../utils/misc"; import { makeCodeblock } from "../../utils/misc";
import { generateId, sendBotMessage } from "./commandHelpers"; 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 "./commandHelpers";
export * from "./types"; export * from "./types";
@ -79,7 +79,12 @@ export const _handleCommand = function (cmd: Command, args: Argument[], ctx: Com
} }
} as never; } as never;
function modifyOpt(opt: Option | Command) {
/**
* Prepare a Command Option for Discord by filling missing fields
* @param opt
*/
export function prepareOption<O extends Option | Command>(opt: O): O {
opt.displayName ||= opt.name; opt.displayName ||= opt.name;
opt.displayDescription ||= opt.description; opt.displayDescription ||= opt.description;
opt.options?.forEach((opt, i, opts) => { opt.options?.forEach((opt, i, opts) => {
@ -88,11 +93,36 @@ function modifyOpt(opt: Option | Command) {
else if (opt === ReqPlaceholder) opts[i] = RequiredMessageOption; else if (opt === ReqPlaceholder) opts[i] = RequiredMessageOption;
opt.choices?.forEach(x => x.displayName ||= x.name); 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<C extends Command>(command: C, plugin: string) {
if (!BUILT_IN) { if (!BUILT_IN) {
console.warn( console.warn(
"[CommandsAPI]", "[CommandsAPI]",
@ -112,7 +142,13 @@ export function registerCommand(command: Command, plugin: string) {
command.inputType ??= ApplicationCommandInputType.BUILT_IN_TEXT; command.inputType ??= ApplicationCommandInputType.BUILT_IN_TEXT;
command.plugin ||= plugin; command.plugin ||= plugin;
modifyOpt(command); prepareOption(command);
if (command.options?.[0]?.type === ApplicationCommandOptionType.SUB_COMMAND) {
registerSubCommands(command, plugin);
return;
}
commands[command.name] = command; commands[command.name] = command;
BUILT_IN.push(command); BUILT_IN.push(command);
} }

View file

@ -81,6 +81,7 @@ export interface Argument {
name: string; name: string;
value: string; value: string;
focused: undefined; focused: undefined;
options: Argument[];
} }
export interface Command { export interface Command {

View file

@ -42,12 +42,6 @@ const DefaultSettings: Settings = {
plugins: {} plugins: {}
}; };
for (const plugin in plugins) {
DefaultSettings.plugins[plugin] = {
enabled: plugins[plugin].required ?? false
};
}
try { try {
var settings = JSON.parse(VencordNative.ipc.sendSync(IpcEvents.GET_SETTINGS)) as Settings; var settings = JSON.parse(VencordNative.ipc.sendSync(IpcEvents.GET_SETTINGS)) as Settings;
mergeDefaults(settings, DefaultSettings); mergeDefaults(settings, DefaultSettings);
@ -60,13 +54,19 @@ type SubscriptionCallback = ((newValue: any, path: string) => void) & { _path?:
const subscriptions = new Set<SubscriptionCallback>(); const subscriptions = new Set<SubscriptionCallback>();
// Wraps the passed settings object in a Proxy to nicely handle change listeners and default values // 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, { return new Proxy(settings, {
get(target, p: string) { get(target, p: string) {
const v = target[p]; const v = target[p];
// using "in" is important in the following cases to properly handle falsy or nullish values // using "in" is important in the following cases to properly handle falsy or nullish values
if (!(p in target)) { 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 // Since the property is not set, check if this is a plugin's setting and if so, try to resolve
// the default value. // the default value.
if (path.startsWith("plugins.")) { if (path.startsWith("plugins.")) {

View file

@ -28,16 +28,23 @@ const logger = new Logger("PluginManager", "#a6d189");
export const plugins = Plugins; export const plugins = Plugins;
export const patches = [] as Patch[]; export const patches = [] as Patch[];
for (const plugin of Object.values(Plugins)) if (plugin.patches && Settings.plugins[plugin.name].enabled) { export function isPluginEnabled(p: string) {
for (const patch of plugin.patches) { return (Settings.plugins[p]?.enabled || Plugins[p]?.required) ?? false;
patch.plugin = plugin.name; }
if (!Array.isArray(patch.replacement)) patch.replacement = [patch.replacement];
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); patches.push(patch);
} }
} }
export function startAllPlugins() { export function startAllPlugins() {
for (const name in Plugins) if (Settings.plugins[name].enabled) { for (const name in Plugins)
if (isPluginEnabled(name)) {
startPlugin(Plugins[name]); startPlugin(Plugins[name]);
} }
} }
@ -45,7 +52,7 @@ export function startAllPlugins() {
export function startDependenciesRecursive(p: Plugin) { export function startDependenciesRecursive(p: Plugin) {
let restartNeeded = false; let restartNeeded = false;
const failures: string[] = []; const failures: string[] = [];
if (p.dependencies) for (const dep of p.dependencies) { p.dependencies?.forEach(dep => {
if (!Settings.plugins[dep].enabled) { if (!Settings.plugins[dep].enabled) {
startDependenciesRecursive(Plugins[dep]); startDependenciesRecursive(Plugins[dep]);
// If the plugin has patches, don't start the plugin, just enable it. // 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.`); logger.warn(`Enabling dependency ${dep} requires restart.`);
Settings.plugins[dep].enabled = true; Settings.plugins[dep].enabled = true;
restartNeeded = true; restartNeeded = true;
continue; return;
} }
const result = startPlugin(Plugins[dep]); const result = startPlugin(Plugins[dep]);
if (!result) failures.push(dep); if (!result) failures.push(dep);
} }
} });
return { restartNeeded, failures }; return { restartNeeded, failures };
} }