Blame Raw
Cohee · e3f41666 · · 1009 lines (43.0 KB)
1 contributor
1import { SlashCommand } from '../../../slash-commands/SlashCommand.js';
2import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js';
3import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../../slash-commands/SlashCommandArgument.js';
4import { SlashCommandClosure } from '../../../slash-commands/SlashCommandClosure.js';
5import { enumIcons } from '../../../slash-commands/SlashCommandCommonEnumsProvider.js';
6import { SlashCommandDebugController } from '../../../slash-commands/SlashCommandDebugController.js';
7import { SlashCommandEnumValue, enumTypes } from '../../../slash-commands/SlashCommandEnumValue.js';
8import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
9import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
10import { isTrueBoolean } from '../../../utils.js';
11import { QuickReplyApi } from '../api/QuickReplyApi.js';
12import { QuickReply } from './QuickReply.js';
13import { QuickReplySet } from './QuickReplySet.js';
14
15export class SlashCommandHandler {
16 /** @type {QuickReplyApi} */ api;
17
18
19 constructor(/** @type {QuickReplyApi} */api) {
20 this.api = api;
21 }
22
23
24 init() {
25 function getExecutionIcons(/** @type {QuickReply} */ qr) {
26 let icons = '';
27 if (qr.preventAutoExecute) icons += '🚫';
28 if (qr.isHidden) icons += '👁️';
29 if (qr.executeOnStartup) icons += '🚀';
30 if (qr.executeOnUser) icons += enumIcons.user;
31 if (qr.executeOnAi) icons += enumIcons.assistant;
32 if (qr.executeOnChatChange) icons += '💬';
33 if (qr.executeOnNewChat) icons += '🆕';
34 if (qr.executeOnGroupMemberDraft) icons += enumIcons.group;
35 if (qr.executeBeforeGeneration) icons += '✈️';
36 return icons;
37 }
38
39 const localEnumProviders = {
40 /** All quick reply sets, optionally filtering out sets that wer already used in the "set" named argument */
41 qrSets: (executor) => QuickReplySet.list.filter(qrSet => qrSet.name != String(executor.namedArgumentList.find(x => x.name == 'set')?.value))
42 .map(qrSet => new SlashCommandEnumValue(qrSet.name, null, enumTypes.enum, 'S')),
43
44 /** All QRs inside a set, utilizing the "set" named argument */
45 qrEntries: (executor) => QuickReplySet.get(String(executor.namedArgumentList.find(x => x.name == 'set')?.value))?.qrList.map(qr => {
46 const icons = getExecutionIcons(qr);
47 const message = `${qr.automationId ? `[${qr.automationId}]` : ''}${icons ? `[auto: ${icons}]` : ''} ${qr.title || qr.message}`.trim();
48 return new SlashCommandEnumValue(qr.label, message, enumTypes.enum, enumIcons.qr);
49 }) ?? [],
50
51 /** All QRs inside a set, utilizing the "set" named argument, returns the QR's ID */
52 qrIds: (executor) => QuickReplySet.get(String(executor.namedArgumentList.find(x => x.name == 'set')?.value))?.qrList.map(qr => {
53 const icons = getExecutionIcons(qr);
54 const message = `${qr.automationId ? `[${qr.automationId}]` : ''}${icons ? `[auto: ${icons}]` : ''} ${qr.title || qr.message}`.trim();
55 return new SlashCommandEnumValue(qr.label, message, enumTypes.enum, enumIcons.qr, null, () => qr.id.toString(), true);
56 }) ?? [],
57
58 /** All QRs as a set.name string, to be able to execute, for example via the /run command */
59 qrExecutables: () => {
60 const globalSetList = this.api.settings.config.setList;
61 const chatSetList = this.api.settings.chatConfig?.setList;
62
63 const globalQrs = globalSetList.map(link => link.set.qrList.map(qr => ({ set: link.set, qr }))).flat();
64 const chatQrs = chatSetList?.map(link => link.set.qrList.map(qr => ({ set: link.set, qr }))).flat() ?? [];
65 const otherQrs = QuickReplySet.list.filter(set => !globalSetList.some(link => link.set.name === set.name && !chatSetList?.some(link => link.set.name === set.name)))
66 .map(set => set.qrList.map(qr => ({ set, qr }))).flat();
67
68 return [
69 ...globalQrs.map(x => new SlashCommandEnumValue(`${x.set.name}.${x.qr.label}`, `[global] ${x.qr.title || x.qr.message}`, enumTypes.name, enumIcons.qr)),
70 ...chatQrs.map(x => new SlashCommandEnumValue(`${x.set.name}.${x.qr.label}`, `[chat] ${x.qr.title || x.qr.message}`, enumTypes.enum, enumIcons.qr)),
71 ...otherQrs.map(x => new SlashCommandEnumValue(`${x.set.name}.${x.qr.label}`, `${x.qr.title || x.qr.message}`, enumTypes.qr, enumIcons.qr)),
72 ];
73 },
74 };
75
76 globalThis.qrEnumProviderExecutables = localEnumProviders.qrExecutables;
77
78 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr',
79 callback: (_, value) => this.executeQuickReplyByIndex(Number(value)),
80 unnamedArgumentList: [
81 new SlashCommandArgument(
82 'number', [ARGUMENT_TYPE.NUMBER], true,
83 ),
84 ],
85 helpString: 'Activates the specified Quick Reply',
86 }));
87 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qrset',
88 callback: () => {
89 toastr.warning('The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.');
90 return '';
91 },
92 helpString: '<strong>DEPRECATED</strong> – The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.',
93 }));
94 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set',
95 callback: (args, value) => {
96 this.toggleGlobalSet(value, args);
97 return '';
98 },
99 namedArgumentList: [
100 new SlashCommandNamedArgument(
101 'visible', 'set visibility', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true',
102 ),
103 ],
104 unnamedArgumentList: [
105 SlashCommandArgument.fromProps({
106 description: 'QR set name',
107 typeList: [ARGUMENT_TYPE.STRING],
108 isRequired: true,
109 enumProvider: localEnumProviders.qrSets,
110 }),
111 ],
112 helpString: 'Toggle global QR set',
113 }));
114 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-on',
115 callback: (args, value) => {
116 this.addGlobalSet(value, args);
117 return '';
118 },
119 namedArgumentList: [
120 new SlashCommandNamedArgument(
121 'visible', 'set visibility', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true',
122 ),
123 ],
124 unnamedArgumentList: [
125 SlashCommandArgument.fromProps({
126 description: 'QR set name',
127 typeList: [ARGUMENT_TYPE.STRING],
128 isRequired: true,
129 enumProvider: localEnumProviders.qrSets,
130 }),
131 ],
132 helpString: 'Activate global QR set',
133 }));
134 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-off',
135 callback: (_, value) => {
136 this.removeGlobalSet(value);
137 return '';
138 },
139 unnamedArgumentList: [
140 SlashCommandArgument.fromProps({
141 description: 'QR set name',
142 typeList: [ARGUMENT_TYPE.STRING],
143 isRequired: true,
144 enumProvider: localEnumProviders.qrSets,
145 }),
146 ],
147 helpString: 'Deactivate global QR set',
148 }));
149 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-chat-set',
150 callback: (args, value) => {
151 this.toggleChatSet(value, args);
152 return '';
153 },
154 namedArgumentList: [
155 new SlashCommandNamedArgument(
156 'visible', 'set visibility', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true',
157 ),
158 ],
159 unnamedArgumentList: [
160 SlashCommandArgument.fromProps({
161 description: 'QR set name',
162 typeList: [ARGUMENT_TYPE.STRING],
163 isRequired: true,
164 enumProvider: localEnumProviders.qrSets,
165 }),
166 ],
167 helpString: 'Toggle chat QR set',
168 }));
169
170 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-chat-set-on',
171 callback: (args, value) => {
172 this.addChatSet(value, args);
173 return '';
174 },
175 namedArgumentList: [
176 new SlashCommandNamedArgument(
177 'visible', 'whether the QR set should be visible', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true',
178 ),
179 ],
180 unnamedArgumentList: [
181 SlashCommandArgument.fromProps({
182 description: 'QR set name',
183 typeList: [ARGUMENT_TYPE.STRING],
184 isRequired: true,
185 enumProvider: localEnumProviders.qrSets,
186 }),
187 ],
188 helpString: 'Activate chat QR set',
189 }));
190 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-chat-set-off',
191 callback: (_, value) => {
192 this.removeChatSet(value);
193 return '';
194 },
195 unnamedArgumentList: [
196 SlashCommandArgument.fromProps({
197 description: 'QR set name',
198 typeList: [ARGUMENT_TYPE.STRING],
199 isRequired: true,
200 enumProvider: localEnumProviders.qrSets,
201 }),
202 ],
203 helpString: 'Deactivate chat QR set',
204 }));
205 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-list',
206 callback: (_, value) => JSON.stringify(this.listSets(value ?? 'all')),
207 returns: 'list of QR sets',
208 namedArgumentList: [],
209 unnamedArgumentList: [
210 new SlashCommandArgument(
211 'set type', [ARGUMENT_TYPE.STRING], false, false, 'all', ['all', 'global', 'chat'],
212 ),
213 ],
214 helpString: 'Gets a list of the names of all quick reply sets.',
215 }));
216 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-list',
217 callback: (_, value) => {
218 return JSON.stringify(this.listQuickReplies(value));
219 },
220 returns: 'list of QRs',
221 namedArgumentList: [],
222 unnamedArgumentList: [
223 SlashCommandArgument.fromProps({
224 description: 'QR set name',
225 typeList: [ARGUMENT_TYPE.STRING],
226 isRequired: true,
227 enumProvider: localEnumProviders.qrSets,
228 }),
229 ],
230 helpString: 'Gets a list of the names of all quick replies in this quick reply set.',
231 }));
232
233 const qrArgs = [
234 SlashCommandNamedArgument.fromProps({
235 name: 'set',
236 description: 'name of the QR set, e.g., set=PresetName1',
237 typeList: [ARGUMENT_TYPE.STRING],
238 isRequired: true,
239 enumProvider: localEnumProviders.qrSets,
240 }),
241 SlashCommandNamedArgument.fromProps({
242 name: 'label',
243 description: 'text on the button, e.g., label=MyButton',
244 typeList: [ARGUMENT_TYPE.STRING],
245 isRequired: false,
246 enumProvider: localEnumProviders.qrEntries,
247 }),
248 SlashCommandNamedArgument.fromProps({
249 name: 'icon',
250 description: 'icon to show on the button, e.g., icon=fa-pencil',
251 typeList: [ARGUMENT_TYPE.STRING],
252 isRequired: false,
253 }),
254 SlashCommandNamedArgument.fromProps({
255 name: 'showlabel',
256 description: 'whether to show the label even when an icon is assigned, e.g., icon=fa-pencil showlabel=true',
257 typeList: [ARGUMENT_TYPE.BOOLEAN],
258 isRequired: false,
259 }),
260 new SlashCommandNamedArgument('hidden', 'whether the button should be hidden, e.g., hidden=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
261 new SlashCommandNamedArgument('startup', 'auto execute on app startup, e.g., startup=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
262 new SlashCommandNamedArgument('user', 'auto execute on user message, e.g., user=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
263 new SlashCommandNamedArgument('bot', 'auto execute on AI message, e.g., bot=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
264 new SlashCommandNamedArgument('load', 'auto execute on chat load, e.g., load=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
265 new SlashCommandNamedArgument('new', 'auto execute on new chat, e.g., new=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
266 new SlashCommandNamedArgument('group', 'auto execute on group member selection, e.g., group=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
267 new SlashCommandNamedArgument('generation', 'auto execute before message generation, e.g., generation=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
268 new SlashCommandNamedArgument('title', 'title / tooltip to be shown on button, e.g., title="My Fancy Button"', [ARGUMENT_TYPE.STRING], false),
269 ];
270 const qrUpdateArgs = [
271 new SlashCommandNamedArgument('newlabel', 'new text for the button', [ARGUMENT_TYPE.STRING], false),
272 SlashCommandNamedArgument.fromProps({
273 name: 'id',
274 description: 'numeric ID of the QR, e.g., id=42',
275 typeList: [ARGUMENT_TYPE.NUMBER],
276 isRequired: false,
277 enumProvider: localEnumProviders.qrIds,
278 }),
279 ];
280
281 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-create',
282 callback: (args, message) => {
283 this.createQuickReply(args, message);
284 return '';
285 },
286 namedArgumentList: qrArgs,
287 unnamedArgumentList: [
288 new SlashCommandArgument(
289 'command', [ARGUMENT_TYPE.STRING], true,
290 ),
291 ],
292 helpString: `
293 <div>Creates a new Quick Reply.</div>
294 <div>
295 <strong>Example:</strong>
296 <ul>
297 <li>
298 <pre><code>/qr-create set=MyPreset label=MyButton /echo 123</code></pre>
299 </li>
300 </ul>
301 </div>
302 `,
303 }));
304 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-get',
305 callback: (args, _) => {
306 return this.getQuickReply(args);
307 },
308 namedArgumentList: [
309 SlashCommandNamedArgument.fromProps({
310 name: 'set',
311 description: 'name of the QR set, e.g., set=PresetName1',
312 typeList: [ARGUMENT_TYPE.STRING],
313 isRequired: true,
314 enumProvider: localEnumProviders.qrSets,
315 }),
316 SlashCommandNamedArgument.fromProps({
317 name: 'label',
318 description: 'text on the button, e.g., label=MyButton',
319 typeList: [ARGUMENT_TYPE.STRING],
320 isRequired: false,
321 enumProvider: localEnumProviders.qrEntries,
322 }),
323 SlashCommandNamedArgument.fromProps({
324 name: 'id',
325 description: 'numeric ID of the QR, e.g., id=42',
326 typeList: [ARGUMENT_TYPE.NUMBER],
327 isRequired: false,
328 enumProvider: localEnumProviders.qrIds,
329 }),
330 ],
331 returns: 'a dictionary with all the QR\'s properties',
332 helpString: `
333 <div>Get a Quick Reply's properties.</div>
334 <div>
335 <strong>Examples:</strong>
336 <ul>
337 <li>
338 <pre><code>/qr-get set=MyPreset label=MyButton | /echo</code></pre>
339 <pre><code>/qr-get set=MyPreset id=42 | /echo</code></pre>
340 </li>
341 </ul>
342 </div>
343 `,
344 }));
345 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-update',
346 callback: (args, message) => {
347 this.updateQuickReply(args, message);
348 return '';
349 },
350 returns: 'updated quick reply',
351 namedArgumentList: [...qrUpdateArgs, ...qrArgs.map(it => {
352 if (it.name == 'label') {
353 const clone = SlashCommandNamedArgument.fromProps(it);
354 clone.isRequired = false;
355 return clone;
356 }
357 return it;
358 })],
359 unnamedArgumentList: [
360 new SlashCommandArgument('command', [ARGUMENT_TYPE.STRING]),
361 ],
362 helpString: `
363 <div>
364 Updates Quick Reply.
365 </div>
366 <div>
367 <strong>Example:</strong>
368 <ul>
369 <li>
370 <pre><code>/qr-update set=MyPreset label=MyButton newlabel=MyRenamedButton /echo 123</code></pre>
371 </li>
372 </ul>
373 </div>
374 `,
375 }));
376 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-delete',
377 callback: (args, name) => {
378 this.deleteQuickReply(args, name);
379 return '';
380 },
381 namedArgumentList: [
382 SlashCommandNamedArgument.fromProps({
383 name: 'set',
384 description: 'QR set name',
385 typeList: [ARGUMENT_TYPE.STRING],
386 isRequired: true,
387 enumProvider: localEnumProviders.qrSets,
388 }),
389 SlashCommandNamedArgument.fromProps({
390 name: 'label',
391 description: 'Quick Reply label',
392 typeList: [ARGUMENT_TYPE.STRING],
393 enumProvider: localEnumProviders.qrEntries,
394 }),
395 SlashCommandNamedArgument.fromProps({
396 name: 'id',
397 description: 'numeric ID of the QR, e.g., id=42',
398 typeList: [ARGUMENT_TYPE.NUMBER],
399 enumProvider: localEnumProviders.qrIds,
400 }),
401 ],
402 unnamedArgumentList: [
403 SlashCommandArgument.fromProps({
404 description: 'label',
405 typeList: [ARGUMENT_TYPE.STRING],
406 enumProvider: localEnumProviders.qrEntries,
407 }),
408 ],
409 helpString: 'Deletes a Quick Reply from the specified set. (Label must be provided via named or unnamed argument)',
410 }));
411 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-contextadd',
412 callback: (args, name) => {
413 this.createContextItem(args, name);
414 return '';
415 },
416 namedArgumentList: [
417 SlashCommandNamedArgument.fromProps({
418 name: 'set',
419 description: 'Name of QR set to add the context menu to',
420 typeList: [ARGUMENT_TYPE.STRING],
421 isRequired: true,
422 enumProvider: localEnumProviders.qrSets,
423 }),
424 SlashCommandNamedArgument.fromProps({
425 name: 'label',
426 description: 'Label of Quick Reply to add the context menu to',
427 typeList: [ARGUMENT_TYPE.STRING],
428 enumProvider: localEnumProviders.qrEntries,
429 }),
430 SlashCommandNamedArgument.fromProps({
431 name: 'id',
432 description: 'Numeric ID of Quick Reply to add the context menu to, e.g. id=42',
433 typeList: [ARGUMENT_TYPE.NUMBER],
434 enumProvider: localEnumProviders.qrIds,
435 }),
436 new SlashCommandNamedArgument(
437 'chain',
438 'If true, button QR is sent together with (before) the clicked QR from the context menu',
439 [ARGUMENT_TYPE.BOOLEAN],
440 false,
441 false,
442 'false',
443 ),
444 ],
445 unnamedArgumentList: [
446 SlashCommandArgument.fromProps({
447 description: 'Name of QR set to add as a context menu',
448 typeList: [ARGUMENT_TYPE.STRING],
449 isRequired: true,
450 enumProvider: localEnumProviders.qrSets,
451 }),
452 ],
453 helpString: `
454 <div>
455 Add a context menu preset to a QR.
456 </div>
457 <div>
458 If <code>id</code> and <code>label</code> are both provided, <code>id</code> will be used.
459 </div>
460 <div>
461 <strong>Example:</strong>
462 <ul>
463 <li>
464 <pre><code>/qr-contextadd set=MyQRSetWithTheButton label=MyButton chain=true MyQRSetWithContextItems</code></pre>
465 </li>
466 </ul>
467 </div>
468 `,
469 }));
470 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-contextdel',
471 callback: (args, name) => {
472 this.deleteContextItem(args, name);
473 return '';
474 },
475 namedArgumentList: [
476 SlashCommandNamedArgument.fromProps({
477 name: 'set',
478 description: 'Name of QR set to remove the context menu from',
479 typeList: [ARGUMENT_TYPE.STRING],
480 isRequired: true,
481 enumProvider: localEnumProviders.qrSets,
482 }),
483 SlashCommandNamedArgument.fromProps({
484 name: 'label',
485 description: 'Label of Quick Reply to remove the context menu from',
486 typeList: [ARGUMENT_TYPE.STRING],
487 enumProvider: localEnumProviders.qrEntries,
488 }),
489 SlashCommandNamedArgument.fromProps({
490 name: 'id',
491 description: 'Numeric ID of Quick Reply to remove the context menu from, e.g. id=42',
492 typeList: [ARGUMENT_TYPE.NUMBER],
493 enumProvider: localEnumProviders.qrIds,
494 }),
495 ],
496 unnamedArgumentList: [
497 SlashCommandArgument.fromProps({
498 description: 'Name of QR set to remove',
499 typeList: [ARGUMENT_TYPE.STRING],
500 isRequired: true,
501 enumProvider: localEnumProviders.qrSets,
502 }),
503 ],
504 helpString: `
505 <div>
506 Remove context menu preset from a QR.
507 </div>
508 <div>
509 If <code>id</code> and <code>label</code> are both provided, <code>id</code> will be used.
510 </div>
511 <div>
512 <strong>Example:</strong>
513 <ul>
514 <li>
515 <pre><code>/qr-contextdel set=MyPreset label=MyButton MyOtherPreset</code></pre>
516 </li>
517 </ul>
518 </div>
519 `,
520 }));
521 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-contextclear',
522 callback: (args, label) => {
523 this.clearContextMenu(args, label);
524 return '';
525 },
526 namedArgumentList: [
527 SlashCommandNamedArgument.fromProps({
528 name: 'set',
529 description: 'QR set name',
530 typeList: [ARGUMENT_TYPE.STRING],
531 isRequired: true,
532 enumProvider: localEnumProviders.qrSets,
533 }),
534 SlashCommandNamedArgument.fromProps({
535 name: 'id',
536 description: 'numeric ID of the QR, e.g., id=42',
537 typeList: [ARGUMENT_TYPE.NUMBER],
538 enumProvider: localEnumProviders.qrIds,
539 }),
540 ],
541 unnamedArgumentList: [
542 SlashCommandArgument.fromProps({
543 description: 'Quick Reply label',
544 typeList: [ARGUMENT_TYPE.STRING],
545 enumProvider: localEnumProviders.qrEntries,
546 }),
547 ],
548 helpString: `
549 <div>
550 Remove all context menu presets from a QR.
551 </div>
552 <div>
553 If <code>id</code> and a label are both provided, <code>id</code> will be used.
554 </div>
555 <div>
556 <strong>Example:</strong>
557 <ul>
558 <li>
559 <pre><code>/qr-contextclear set=MyPreset MyButton</code></pre>
560 </li>
561 </ul>
562 </div>
563 `,
564 }));
565
566 const presetArgs = [
567 new SlashCommandNamedArgument('nosend', 'disable send / insert in user input (invalid for slash commands)', [ARGUMENT_TYPE.BOOLEAN], false),
568 new SlashCommandNamedArgument('before', 'place QR before user input', [ARGUMENT_TYPE.BOOLEAN], false),
569 new SlashCommandNamedArgument('inject', 'inject user input automatically (if disabled use {{input}})', [ARGUMENT_TYPE.BOOLEAN], false),
570 ];
571 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-create',
572 callback: async (args, name) => {
573 await this.createSet(name, args);
574 return '';
575 },
576 aliases: ['qr-presetadd'],
577 namedArgumentList: presetArgs,
578 unnamedArgumentList: [
579 SlashCommandArgument.fromProps({
580 description: 'QR set name',
581 typeList: [ARGUMENT_TYPE.STRING],
582 isRequired: true,
583 enumProvider: localEnumProviders.qrSets,
584 forceEnum: false,
585 }),
586 ],
587 helpString: `
588 <div>
589 Create a new preset (overrides existing ones).
590 </div>
591 <div>
592 <strong>Example:</strong>
593 <ul>
594 <li>
595 <pre><code>/qr-set-add MyNewPreset</code></pre>
596 </li>
597 </ul>
598 </div>
599 `,
600 }));
601
602 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-update',
603 callback: async (args, name) => {
604 await this.updateSet(name, args);
605 return '';
606 },
607 aliases: ['qr-presetupdate'],
608 namedArgumentList: presetArgs,
609 unnamedArgumentList: [
610 SlashCommandArgument.fromProps({
611 description: 'QR set name',
612 typeList: [ARGUMENT_TYPE.STRING],
613 isRequired: true,
614 enumProvider: localEnumProviders.qrSets,
615 }),
616 ],
617 helpString: `
618 <div>
619 Update an existing preset.
620 </div>
621 <div>
622 <strong>Example:</strong>
623 <pre><code>/qr-set-update enabled=false MyPreset</code></pre>
624 </div>
625 `,
626 }));
627 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-delete',
628 callback: async (_, name) => {
629 await this.deleteSet(name);
630 return '';
631 },
632 aliases: ['qr-presetdelete'],
633 unnamedArgumentList: [
634 SlashCommandArgument.fromProps({
635 description: 'QR set name',
636 typeList: [ARGUMENT_TYPE.STRING],
637 isRequired: true,
638 enumProvider: localEnumProviders.qrSets,
639 }),
640 ],
641 helpString: `
642 <div>
643 Delete an existing preset.
644 </div>
645 <div>
646 <strong>Example:</strong>
647 <pre><code>/qr-set-delete MyPreset</code></pre>
648 </div>
649 `,
650 }));
651
652 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-arg',
653 callback: ({ _scope }, [key, value]) => {
654 _scope.setMacro(`arg::${key}`, value, key.includes('*'));
655 return '';
656 },
657 unnamedArgumentList: [
658 SlashCommandArgument.fromProps({ description: 'argument name',
659 typeList: ARGUMENT_TYPE.STRING,
660 isRequired: true,
661 }),
662 SlashCommandArgument.fromProps({ description: 'argument value',
663 typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY],
664 isRequired: true,
665 }),
666 ],
667 splitUnnamedArgument: true,
668 splitUnnamedArgumentCount: 2,
669 helpString: `
670 <div>
671 Set a fallback value for a Quick Reply argument.
672 </div>
673 <div>
674 <strong>Example:</strong>
675 <pre><code>/qr-arg x foo |\n/echo {{arg::x}}</code></pre>
676 </div>
677 `,
678 }));
679
680 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'import',
681 /**
682 *
683 * @param {{_scope:SlashCommandScope, _abortController:SlashCommandAbortController, _debugController:SlashCommandDebugController, from:string}} args
684 * @param {string} value
685 */
686 callback: (args, value) => {
687 if (!args.from) throw new Error('/import requires from= to be set.');
688 if (!value) throw new Error('/import requires the unnamed argument to be set.');
689 let qr = [...this.api.listGlobalSets(), ...this.api.listChatSets()]
690 .map(it => this.api.getSetByName(it)?.qrList ?? [])
691 .flat()
692 .find(it => it.label == args.from)
693 ;
694 if (!qr) {
695 let [setName, ...qrNameParts] = args.from.split('.');
696 let qrName = qrNameParts.join('.');
697 let qrs = QuickReplySet.get(setName);
698 if (qrs) {
699 qr = qrs.qrList.find(it => it.label == qrName);
700 }
701 }
702 if (qr) {
703 const parser = new SlashCommandParser();
704 const closure = parser.parse(qr.message, true, [], args._abortController, args._debugController);
705 if (args._debugController) {
706 closure.source = args.from;
707 }
708 const testCandidates = (executor) => {
709 return (
710 executor.namedArgumentList.find(arg => arg.name == 'key')
711 && executor.unnamedArgumentList.length > 0
712 && executor.unnamedArgumentList[0].value instanceof SlashCommandClosure
713 ) || (
714 !executor.namedArgumentList.find(arg => arg.name == 'key')
715 && executor.unnamedArgumentList.length > 1
716 && executor.unnamedArgumentList[1].value instanceof SlashCommandClosure
717 );
718 };
719 const candidates = closure.executorList
720 .filter(executor => ['let', 'var'].includes(executor.command.name))
721 .filter(testCandidates)
722 .map(executor => ({
723 key: executor.namedArgumentList.find(arg => arg.name == 'key')?.value ?? executor.unnamedArgumentList[0].value,
724 value: executor.unnamedArgumentList[executor.namedArgumentList.find(arg => arg.name == 'key') ? 0 : 1].value,
725 }))
726 ;
727 for (let i = 0; i < value.length; i++) {
728 const srcName = value[i];
729 let dstName = srcName;
730 if (i + 2 < value.length && value[i + 1] == 'as') {
731 dstName = value[i + 2];
732 i += 2;
733 }
734 const pick = candidates.find(it => it.key == srcName);
735 if (!pick) throw new Error(`No scoped closure named "${srcName}" found in "${args.from}"`);
736 if (args._scope.existsVariableInScope(dstName)) {
737 args._scope.setVariable(dstName, pick.value);
738 } else {
739 args._scope.letVariable(dstName, pick.value);
740 }
741 }
742 } else {
743 throw new Error(`No Quick Reply found for "${name}".`);
744 }
745 return '';
746 },
747 namedArgumentList: [
748 SlashCommandNamedArgument.fromProps({ name: 'from',
749 description: 'Quick Reply to import from (QRSet.QRLabel)',
750 typeList: ARGUMENT_TYPE.STRING,
751 isRequired: true,
752 }),
753 ],
754 unnamedArgumentList: [
755 SlashCommandArgument.fromProps({ description: 'what to import (x or x as y)',
756 acceptsMultiple: true,
757 typeList: ARGUMENT_TYPE.STRING,
758 isRequired: true,
759 }),
760 ],
761 splitUnnamedArgument: true,
762 helpString: `
763 <div>
764 Import one or more closures from another Quick Reply.
765 </div>
766 <div>
767 Only imports closures that are directly assigned a scoped variable via <code>/let</code> or <code>/var</code>.
768 </div>
769 <div>
770 <strong>Examples:</strong>
771 <ul>
772 <li><pre><code>/import from=LibraryQrSet.FooBar foo |\n/:foo</code></pre></li>
773 <li><pre><code>/import from=LibraryQrSet.FooBar\n\tfoo\n\tbar\n|\n/:foo |\n/:bar</code></pre></li>
774 <li><pre><code>/import from=LibraryQrSet.FooBar\n\tfoo as x\n\tbar as y\n|\n/:x |\n/:y</code></pre></li>
775 </ul>
776 </div>
777 `,
778 }));
779 }
780
781
782 getSetByName(name) {
783 const set = this.api.getSetByName(name);
784 if (!set) {
785 toastr.error(`No Quick Reply Set with the name "${name}" could be found.`);
786 }
787 return set;
788 }
789
790 getQrByLabel(setName, label) {
791 const qr = this.api.getQrByLabel(setName, label);
792 if (!qr) {
793 toastr.error(`No Quick Reply with the label "${label}" could be found in the set "${setName}"`);
794 }
795 return qr;
796 }
797
798
799 async executeQuickReplyByIndex(idx) {
800 try {
801 return await this.api.executeQuickReplyByIndex(idx);
802 } catch (ex) {
803 toastr.error(ex.message);
804 }
805 }
806
807
808 toggleGlobalSet(name, args = {}) {
809 try {
810 this.api.toggleGlobalSet(name, isTrueBoolean(args.visible ?? 'true'));
811 } catch (ex) {
812 toastr.error(ex.message);
813 }
814 }
815 addGlobalSet(name, args = {}) {
816 try {
817 this.api.addGlobalSet(name, isTrueBoolean(args.visible ?? 'true'));
818 } catch (ex) {
819 toastr.error(ex.message);
820 }
821 }
822 removeGlobalSet(name) {
823 try {
824 this.api.removeGlobalSet(name);
825 } catch (ex) {
826 toastr.error(ex.message);
827 }
828 }
829
830
831 toggleChatSet(name, args = {}) {
832 try {
833 this.api.toggleChatSet(name, isTrueBoolean(args.visible ?? 'true'));
834 } catch (ex) {
835 toastr.error(ex.message);
836 }
837 }
838 addChatSet(name, args = {}) {
839 try {
840 this.api.addChatSet(name, isTrueBoolean(args.visible ?? 'true'));
841 } catch (ex) {
842 toastr.error(ex.message);
843 }
844 }
845 removeChatSet(name) {
846 try {
847 this.api.removeChatSet(name);
848 } catch (ex) {
849 toastr.error(ex.message);
850 }
851 }
852
853
854 createQuickReply(args, message) {
855 try {
856 this.api.createQuickReply(
857 args.set ?? '',
858 args.label ?? '',
859 {
860 icon: args.icon,
861 showLabel: args.showlabel === undefined ? undefined : isTrueBoolean(args.showlabel),
862 message: message ?? '',
863 title: args.title,
864 isHidden: isTrueBoolean(args.hidden),
865 executeOnStartup: isTrueBoolean(args.startup),
866 executeOnUser: isTrueBoolean(args.user),
867 executeOnAi: isTrueBoolean(args.bot),
868 executeOnChatChange: isTrueBoolean(args.load),
869 executeOnNewChat: isTrueBoolean(args.new),
870 executeOnGroupMemberDraft: isTrueBoolean(args.group),
871 executeBeforeGeneration: isTrueBoolean(args.generation),
872 automationId: args.automationId ?? '',
873 },
874 );
875 } catch (ex) {
876 toastr.error(ex.message);
877 }
878 }
879 getQuickReply(args) {
880 if (!args.id && !args.label) {
881 toastr.error('Please provide a valid id or label.');
882 return '';
883 }
884 try {
885 return JSON.stringify(this.api.getQrByLabel(args.set, args.id !== undefined ? Number(args.id) : args.label));
886 } catch (ex) {
887 toastr.error(ex.message);
888 }
889 }
890 updateQuickReply(args, message) {
891 try {
892 this.api.updateQuickReply(
893 args.set ?? '',
894 args.id !== undefined ? Number(args.id) : (args.label ?? ''),
895 {
896 icon: args.icon,
897 showLabel: args.showlabel === undefined ? undefined : isTrueBoolean(args.showlabel),
898 newLabel: args.newlabel,
899 message: (message ?? '').trim().length > 0 ? message : undefined,
900 title: args.title,
901 isHidden: args.hidden === undefined ? undefined : isTrueBoolean(args.hidden),
902 executeOnStartup: args.startup === undefined ? undefined : isTrueBoolean(args.startup),
903 executeOnUser: args.user === undefined ? undefined : isTrueBoolean(args.user),
904 executeOnAi: args.bot === undefined ? undefined : isTrueBoolean(args.bot),
905 executeOnChatChange: args.load === undefined ? undefined : isTrueBoolean(args.load),
906 executeOnGroupMemberDraft: args.group === undefined ? undefined : isTrueBoolean(args.group),
907 executeOnNewChat: args.new === undefined ? undefined : isTrueBoolean(args.new),
908 executeBeforeGeneration: args.generation === undefined ? undefined : isTrueBoolean(args.generation),
909 automationId: args.automationId ?? '',
910 },
911 );
912 } catch (ex) {
913 toastr.error(ex.message);
914 }
915 }
916 deleteQuickReply(args, label) {
917 try {
918 this.api.deleteQuickReply(args.set, args.id !== undefined ? Number(args.id) : (args.label ?? label));
919 } catch (ex) {
920 toastr.error(ex.message);
921 }
922 }
923
924 createContextItem(args, name) {
925 try {
926 this.api.createContextItem(
927 args.set,
928 args.id !== undefined ? Number(args.id) : args.label,
929 name,
930 isTrueBoolean(args.chain),
931 );
932 } catch (ex) {
933 toastr.error(ex.message);
934 }
935 }
936 deleteContextItem(args, name) {
937 try {
938 this.api.deleteContextItem(args.set, args.id !== undefined ? Number(args.id) : args.label, name);
939 } catch (ex) {
940 toastr.error(ex.message);
941 }
942 }
943 clearContextMenu(args, label) {
944 try {
945 this.api.clearContextMenu(args.set, args.id !== undefined ? Number(args.id) : args.label ?? label);
946 } catch (ex) {
947 toastr.error(ex.message);
948 }
949 }
950
951
952 async createSet(name, args) {
953 try {
954 await this.api.createSet(
955 args.name ?? name ?? '',
956 {
957 disableSend: isTrueBoolean(args.nosend),
958 placeBeforeInput: isTrueBoolean(args.before),
959 injectInput: isTrueBoolean(args.inject),
960 },
961 );
962 } catch (ex) {
963 toastr.error(ex.message);
964 }
965 }
966 async updateSet(name, args) {
967 try {
968 await this.api.updateSet(
969 args.name ?? name ?? '',
970 {
971 disableSend: args.nosend !== undefined ? isTrueBoolean(args.nosend) : undefined,
972 placeBeforeInput: args.before !== undefined ? isTrueBoolean(args.before) : undefined,
973 injectInput: args.inject !== undefined ? isTrueBoolean(args.inject) : undefined,
974 },
975 );
976 } catch (ex) {
977 toastr.error(ex.message);
978 }
979 }
980 async deleteSet(name) {
981 try {
982 await this.api.deleteSet(name ?? '');
983 } catch (ex) {
984 toastr.error(ex.message);
985 }
986 }
987
988 listSets(source) {
989 try {
990 switch (source) {
991 case 'global':
992 return this.api.listGlobalSets();
993 case 'chat':
994 return this.api.listChatSets();
995 default:
996 return this.api.listSets();
997 }
998 } catch (ex) {
999 toastr.error(ex.message);
1000 }
1001 }
1002 listQuickReplies(name) {
1003 try {
1004 return this.api.listQuickReplies(name);
1005 } catch (ex) {
1006 toastr.error(ex.message);
1007 }
1008 }
1009}