Blame Raw
· · · 306 lines (13.0 KB)
0 contributors
1import { disableExtension, enableExtension, extensionNames, findExtension } from './extensions.js';
2import { SlashCommand } from './slash-commands/SlashCommand.js';
3import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
4import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
5import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
6import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
7import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
8import { isFalseBoolean, isTrueBoolean } from './utils.js';
9
10/**
11 * @param {'enable' | 'disable' | 'toggle'} action - The action to perform on the extension
12 * @typedef {import('./slash-commands/SlashCommand.js').NamedArguments | import('./slash-commands/SlashCommand.js').NamedArgumentsCapture} NamedArgumentsAssignment
13 * @returns {(args: NamedArgumentsAssignment, extensionName: string | SlashCommandClosure) => Promise<string>}
14 */
15function getExtensionActionCallback(action) {
16 return async (args, extensionName) => {
17 if (args?.reload instanceof SlashCommandClosure) throw new Error('\'reload\' argument cannot be a closure.');
18 if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
19 if (!extensionName) {
20 toastr.warning(`Extension name must be provided as an argument to ${action} this extension.`);
21 return '';
22 }
23
24 const reload = !isFalseBoolean(args?.reload?.toString());
25 const extension = findExtension(extensionName);
26 if (!extension) {
27 toastr.warning(`Extension ${extensionName} does not exist.`);
28 return '';
29 }
30
31 if (action === 'enable' && extension.enabled) {
32 toastr.info(`Extension ${extension.name} is already enabled.`);
33 return extension.name;
34 }
35
36 if (action === 'disable' && !extension.enabled) {
37 toastr.info(`Extension ${extension.name} is already disabled.`);
38 return extension.name;
39 }
40
41 if (action === 'toggle') {
42 action = extension.enabled ? 'disable' : 'enable';
43 }
44
45 if (reload) {
46 toastr.info(`${action.charAt(0).toUpperCase() + action.slice(1)}ing extension ${extension.name} and reloading...`);
47
48 // Clear input, so it doesn't stay because the command didn't "finish",
49 // and wait for a bit to both show the toast and let the clear bubble through.
50 $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
51 await new Promise(resolve => setTimeout(resolve, 100));
52 }
53
54 if (action === 'enable') {
55 await enableExtension(extension.name, reload);
56 } else {
57 await disableExtension(extension.name, reload);
58 }
59
60 toastr.success(`Extension ${extension.name} ${action}d.`);
61
62
63 console.info(`Extension ${action}ed: ${extension.name}`);
64 if (!reload) {
65 console.info('Reload not requested, so page needs to be reloaded manually for changes to take effect.');
66 }
67
68 return extension.name;
69 };
70}
71
72/**
73 * Provides an array of SlashCommandEnumValue objects based on the extension names.
74 * Each object contains the name of the extension and a description indicating if it is a third-party extension.
75 *
76 * @returns {SlashCommandEnumValue[]} An array of SlashCommandEnumValue objects
77 */
78const extensionNamesEnumProvider = () => extensionNames.map(name => {
79 const isThirdParty = name.startsWith('third-party/');
80 if (isThirdParty) name = name.slice('third-party/'.length);
81
82 const description = isThirdParty ? 'third party extension' : null;
83
84 return new SlashCommandEnumValue(name, description, !isThirdParty ? enumTypes.name : enumTypes.enum);
85});
86
87export function registerExtensionSlashCommands() {
88 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
89 name: 'extension-enable',
90 callback: getExtensionActionCallback('enable'),
91 returns: 'The internal extension name',
92 namedArgumentList: [
93 SlashCommandNamedArgument.fromProps({
94 name: 'reload',
95 description: 'Whether to reload the page after enabling the extension',
96 typeList: [ARGUMENT_TYPE.BOOLEAN],
97 defaultValue: 'true',
98 enumList: commonEnumProviders.boolean('trueFalse')(),
99 }),
100 ],
101 unnamedArgumentList: [
102 SlashCommandArgument.fromProps({
103 description: 'Extension name',
104 typeList: [ARGUMENT_TYPE.STRING],
105 isRequired: true,
106 enumProvider: extensionNamesEnumProvider,
107 forceEnum: true,
108 }),
109 ],
110 helpString: `
111 <div>
112 Enables a specified extension.
113 </div>
114 <div>
115 By default, the page will be reloaded automatically, stopping any further commands.<br />
116 If <code>reload=false</code> named argument is passed, the page will not be reloaded, and the extension will stay disabled until refreshed.
117 The page either needs to be refreshed, or <code>/reload-page</code> has to be called.
118 </div>
119 <div>
120 <strong>Example:</strong>
121 <ul>
122 <li>
123 <pre><code class="language-stscript">/extension-enable Summarize</code></pre>
124 </li>
125 </ul>
126 </div>
127 `,
128 }));
129 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
130 name: 'extension-disable',
131 callback: getExtensionActionCallback('disable'),
132 returns: 'The internal extension name',
133 namedArgumentList: [
134 SlashCommandNamedArgument.fromProps({
135 name: 'reload',
136 description: 'Whether to reload the page after disabling the extension',
137 typeList: [ARGUMENT_TYPE.BOOLEAN],
138 defaultValue: 'true',
139 enumList: commonEnumProviders.boolean('trueFalse')(),
140 }),
141 ],
142 unnamedArgumentList: [
143 SlashCommandArgument.fromProps({
144 description: 'Extension name',
145 typeList: [ARGUMENT_TYPE.STRING],
146 isRequired: true,
147 enumProvider: extensionNamesEnumProvider,
148 forceEnum: true,
149 }),
150 ],
151 helpString: `
152 <div>
153 Disables a specified extension.
154 </div>
155 <div>
156 By default, the page will be reloaded automatically, stopping any further commands.<br />
157 If <code>reload=false</code> named argument is passed, the page will not be reloaded, and the extension will stay enabled until refreshed.
158 The page either needs to be refreshed, or <code>/reload-page</code> has to be called.
159 </div>
160 <div>
161 <strong>Example:</strong>
162 <ul>
163 <li>
164 <pre><code class="language-stscript">/extension-disable Summarize</code></pre>
165 </li>
166 </ul>
167 </div>
168 `,
169 }));
170 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
171 name: 'extension-toggle',
172 callback: async (args, extensionName) => {
173 if (args?.state instanceof SlashCommandClosure) throw new Error('\'state\' argument cannot be a closure.');
174 if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
175
176 const action = isTrueBoolean(args?.state?.toString()) ? 'enable' :
177 isFalseBoolean(args?.state?.toString()) ? 'disable' :
178 'toggle';
179
180 return await getExtensionActionCallback(action)(args, extensionName);
181 },
182 returns: 'The internal extension name',
183 namedArgumentList: [
184 SlashCommandNamedArgument.fromProps({
185 name: 'reload',
186 description: 'Whether to reload the page after toggling the extension',
187 typeList: [ARGUMENT_TYPE.BOOLEAN],
188 defaultValue: 'true',
189 enumList: commonEnumProviders.boolean('trueFalse')(),
190 }),
191 SlashCommandNamedArgument.fromProps({
192 name: 'state',
193 description: 'Explicitly set the state of the extension (true to enable, false to disable). If not provided, the state will be toggled to the opposite of the current state.',
194 typeList: [ARGUMENT_TYPE.BOOLEAN],
195 enumList: commonEnumProviders.boolean('trueFalse')(),
196 }),
197 ],
198 unnamedArgumentList: [
199 SlashCommandArgument.fromProps({
200 description: 'Extension name',
201 typeList: [ARGUMENT_TYPE.STRING],
202 isRequired: true,
203 enumProvider: extensionNamesEnumProvider,
204 forceEnum: true,
205 }),
206 ],
207 helpString: `
208 <div>
209 Toggles the state of a specified extension.
210 </div>
211 <div>
212 By default, the page will be reloaded automatically, stopping any further commands.<br />
213 If <code>reload=false</code> named argument is passed, the page will not be reloaded, and the extension will stay in its current state until refreshed.
214 The page either needs to be refreshed, or <code>/reload-page</code> has to be called.
215 </div>
216 <div>
217 <strong>Example:</strong>
218 <ul>
219 <li>
220 <pre><code class="language-stscript">/extension-toggle Summarize</code></pre>
221 </li>
222 <li>
223 <pre><code class="language-stscript">/extension-toggle Summarize state=true</code></pre>
224 </li>
225 </ul>
226 </div>
227 `,
228 }));
229 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
230 name: 'extension-state',
231 callback: async (_, extensionName) => {
232 if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
233 const extension = findExtension(extensionName);
234 if (!extension) {
235 toastr.warning(`Extension ${extensionName} does not exist.`);
236 return '';
237 }
238
239 return String(extension.enabled);
240 },
241 returns: 'The state of the extension, whether it is enabled.',
242 unnamedArgumentList: [
243 SlashCommandArgument.fromProps({
244 description: 'Extension name',
245 typeList: [ARGUMENT_TYPE.STRING],
246 isRequired: true,
247 enumProvider: extensionNamesEnumProvider,
248 forceEnum: true,
249 }),
250 ],
251 helpString: `
252 <div>
253 Returns the state of a specified extension (true if enabled, false if disabled).
254 </div>
255 <div>
256 <strong>Example:</strong>
257 <ul>
258 <li>
259 <pre><code class="language-stscript">/extension-state Summarize</code></pre>
260 </li>
261 </ul>
262 </div>
263 `,
264 }));
265 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
266 name: 'extension-exists',
267 aliases: ['extension-installed'],
268 callback: async (_, extensionName) => {
269 if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
270 const extension = findExtension(extensionName);
271 return extension !== null ? 'true' : 'false';
272 },
273 returns: 'Whether the extension exists and is installed.',
274 unnamedArgumentList: [
275 SlashCommandArgument.fromProps({
276 description: 'Extension name',
277 typeList: [ARGUMENT_TYPE.STRING],
278 isRequired: true,
279 enumProvider: extensionNamesEnumProvider,
280 }),
281 ],
282 helpString: `
283 <div>
284 Checks if a specified extension exists.
285 </div>
286 <div>
287 <strong>Example:</strong>
288 <ul>
289 <li>
290 <pre><code class="language-stscript">/extension-exists SillyTavern-LALib</code></pre>
291 </li>
292 </ul>
293 </div>
294 `,
295 }));
296
297 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
298 name: 'reload-page',
299 callback: async () => {
300 toastr.info('Reloading the page...');
301 location.reload();
302 return '';
303 },
304 helpString: 'Reloads the current page. All further commands will not be processed.',
305 }));
306}