Add commands API (#38)
This commit is contained in:
		
							parent
							
								
									a9e67aa340
								
							
						
					
					
						commit
						e563521416
					
				
					 8 changed files with 267 additions and 35 deletions
				
			
		
							
								
								
									
										157
									
								
								src/api/Commands.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								src/api/Commands.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,157 @@ | |||
| import { Channel, Guild } from "discord-types/general"; | ||||
| import { waitFor } from '../webpack'; | ||||
| 
 | ||||
| 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 extends string | undefined>(args: Argument[], name: string, fallbackValue?: T): T extends undefined ? T : string { | ||||
|     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); | ||||
| } | ||||
| 
 | ||||
| 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]; | ||||
| } | ||||
| 
 | ||||
| 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; | ||||
| } | ||||
|  | @ -1,2 +1,3 @@ | |||
| export * as MessageEvents from "./MessageEvents"; | ||||
| export * as Notices from "./Notices"; | ||||
| export * as Commands from "./Commands"; | ||||
|  |  | |||
|  | @ -129,23 +129,25 @@ export default ErrorBoundary.wrap(function Settings() { | |||
|                         value={settings.plugins[p.name].enabled || p.required || dependency} | ||||
|                         onChange={v => { | ||||
|                             settings.plugins[p.name].enabled = v; | ||||
|                             let needsRestart = Boolean(p.patches?.length); | ||||
|                             if (v) { | ||||
|                                 p.dependencies?.forEach(d => { | ||||
|                                     const dep = Plugins[d]; | ||||
|                                     needsRestart ||= Boolean(dep.patches?.length && !settings.plugins[d].enabled); | ||||
|                                     settings.plugins[d].enabled = true; | ||||
|                                     if (!Plugins[d].started && !stopPlugin) { | ||||
|                                         settings.plugins[p.name].enabled = false; | ||||
|                                     if (!needsRestart && !dep.started && !startPlugin(dep)) { | ||||
|                                         showErrorToast(`Failed to start dependency ${d}. Check the console for more info.`); | ||||
|                                     } | ||||
|                                 }); | ||||
|                                 if (!p.started && !startPlugin(p)) { | ||||
|                                 if (!needsRestart && !p.started && !startPlugin(p)) { | ||||
|                                     showErrorToast(`Failed to start plugin ${p.name}. Check the console for more info.`); | ||||
|                                 } | ||||
|                             } else { | ||||
|                                 if (p.started && !stopPlugin(p)) { | ||||
|                                 if ((p.started || !p.start && p.commands?.length) && !stopPlugin(p)) { | ||||
|                                     showErrorToast(`Failed to stop plugin ${p.name}. Check the console for more info.`); | ||||
|                                 } | ||||
|                             } | ||||
|                             if (p.patches) changes.handleChange(p.name); | ||||
|                             if (needsRestart) changes.handleChange(p.name); | ||||
|                         }} | ||||
|                         note={p.description} | ||||
|                         tooltipNote={ | ||||
|  |  | |||
							
								
								
									
										22
									
								
								src/plugins/apiCommands.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/plugins/apiCommands.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,22 @@ | |||
| import definePlugin from "../utils/types"; | ||||
| import { Devs } from "../utils/constants"; | ||||
| 
 | ||||
| export default definePlugin({ | ||||
|     name: "CommandsAPI", | ||||
|     authors: [Devs.Arjix], | ||||
|     description: "Api required by anything that uses commands", | ||||
|     patches: [ | ||||
|         { | ||||
|             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(...)
 | ||||
|                     match: /(?<=\w=)(\w)(\.filter\(.{0,30}giphy)/, | ||||
|                     replace: "Vencord.Api.Commands._init($1)$2", | ||||
|                 } | ||||
|             ], | ||||
|         } | ||||
|     ], | ||||
| }); | ||||
|  | @ -1,4 +1,5 @@ | |||
| import Plugins from "plugins"; | ||||
| import { registerCommand, unregisterCommand } from "../api/Commands"; | ||||
| import { Settings } from "../api/settings"; | ||||
| import Logger from "../utils/logger"; | ||||
| import { Patch, Plugin } from "../utils/types"; | ||||
|  | @ -17,44 +18,70 @@ for (const plugin of Object.values(Plugins)) if (plugin.patches && Settings.plug | |||
| } | ||||
| 
 | ||||
| export function startAllPlugins() { | ||||
|     for (const plugin in Plugins) if (Settings.plugins[plugin].enabled) { | ||||
|         startPlugin(Plugins[plugin]); | ||||
|     for (const name in Plugins) if (Settings.plugins[name].enabled) { | ||||
|         startPlugin(Plugins[name]); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export function startPlugin(p: Plugin) { | ||||
|     if (!p.start) return true; | ||||
| 
 | ||||
|     logger.info("Starting plugin", p.name); | ||||
|     if (p.started) { | ||||
|         logger.warn(`${p.name} already started`); | ||||
|         return false; | ||||
|     if (p.start) { | ||||
|         logger.info("Starting plugin", p.name); | ||||
|         if (p.started) { | ||||
|             logger.warn(`${p.name} already started`); | ||||
|             return false; | ||||
|         } | ||||
|         try { | ||||
|             p.start(); | ||||
|             p.started = true; | ||||
|         } catch (e) { | ||||
|             logger.error(`Failed to start ${p.name}\n`, e); | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     try { | ||||
|         p.start(); | ||||
|         p.started = true; | ||||
|         return true; | ||||
|     } catch (err: any) { | ||||
|         logger.error(`Failed to start ${p.name}\n`, err); | ||||
|         return false; | ||||
|     if (p.commands?.length) { | ||||
|         logger.info("Registering commands of plugin", p.name); | ||||
|         for (const cmd of p.commands) { | ||||
|             try { | ||||
|                 registerCommand(cmd, p.name); | ||||
|             } catch (e) { | ||||
|                 logger.error(`Failed to register command ${cmd.name}\n`, e); | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     return true; | ||||
| } | ||||
| 
 | ||||
| export function stopPlugin(p: Plugin) { | ||||
|     if (!p.stop) return true; | ||||
|     if (p.stop) { | ||||
|         logger.info("Stopping plugin", p.name); | ||||
|         if (!p.started) { | ||||
|             logger.warn(`${p.name} already stopped`); | ||||
|             return false; | ||||
|         } | ||||
|         try { | ||||
|             p.stop(); | ||||
|             p.started = false; | ||||
|         } catch (e) { | ||||
|             logger.error(`Failed to stop ${p.name}\n`, e); | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     logger.info("Stopping plugin", p.name); | ||||
|     if (!p.started) { | ||||
|         logger.warn(`${p.name} already stopped / never started`); | ||||
|         return false; | ||||
|     } | ||||
|     try { | ||||
|         p.stop(); | ||||
|         p.started = false; | ||||
|         return true; | ||||
|     } catch (err: any) { | ||||
|         logger.error(`Failed to stop ${p.name}\n`, err); | ||||
|         return false; | ||||
|     if (p.commands?.length) { | ||||
|         logger.info("Unregistering commands of plugin", p.name); | ||||
|         for (const cmd of p.commands) { | ||||
|             try { | ||||
|                 unregisterCommand(cmd.name); | ||||
|             } catch (e) { | ||||
|                 logger.error(`Failed to unregister command ${cmd.name}\n`, e); | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return true; | ||||
| } | ||||
|  |  | |||
							
								
								
									
										20
									
								
								src/plugins/lenny.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/plugins/lenny.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | |||
| import definePlugin from "../utils/types"; | ||||
| import { Devs } from "../utils/constants"; | ||||
| import { findOption, OptionalMessageOption } from "../api/Commands"; | ||||
| 
 | ||||
| export default definePlugin({ | ||||
|     name: "lenny", | ||||
|     description: "( ͡° ͜ʖ ͡°)", | ||||
|     authors: [Devs.Arjix], | ||||
|     dependencies: ["CommandsAPI"], | ||||
|     commands: [ | ||||
|         { | ||||
|             name: "lenny", | ||||
|             description: "Sends a lenny face", | ||||
|             options: [OptionalMessageOption], | ||||
|             execute: (opts) => ({ | ||||
|                 content: findOption(opts, "message", "") + " ( ͡° ͜ʖ ͡°)" | ||||
|             }), | ||||
|         }, | ||||
|     ] | ||||
| }); | ||||
|  | @ -61,7 +61,7 @@ export default definePlugin({ | |||
|                 const emojiString = `<${emoji.animated ? 'a' : ''}:${emoji.originalName || emoji.name}:${emoji.id}>`; | ||||
|                 const url = emoji.url.replace(/\?size=[0-9]+/, `?size=48`); | ||||
|                 messageObj.content = messageObj.content.replace(emojiString, (match, offset, origStr) => { | ||||
|                     return `${getWordBoundary(origStr, offset-1)}${url}${getWordBoundary(origStr, offset+match.length)}`; | ||||
|                     return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`; | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
|  | @ -76,7 +76,7 @@ export default definePlugin({ | |||
| 
 | ||||
|                 const url = emoji.url.replace(/\?size=[0-9]+/, `?size=48`); | ||||
|                 messageObj.content = messageObj.content.replace(emojiStr, (match, offset, origStr) => { | ||||
|                     return `${getWordBoundary(origStr, offset-1)}${url}${getWordBoundary(origStr, offset+match.length)}`; | ||||
|                     return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`; | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
|  |  | |||
|  | @ -1,3 +1,5 @@ | |||
| import { Command } from "../api/Commands"; | ||||
| 
 | ||||
| // exists to export default definePlugin({...})
 | ||||
| export default function definePlugin(p: PluginDef & Record<string, any>) { | ||||
|     return p; | ||||
|  | @ -31,6 +33,7 @@ interface PluginDef { | |||
|     start?(): void; | ||||
|     stop?(): void; | ||||
|     patches?: Omit<Patch, "plugin">[]; | ||||
|     commands?: Command[]; | ||||
|     dependencies?: string[], | ||||
|     required?: boolean; | ||||
|     /** | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue