/[sudobot]/branches/8.x/src/commands/settings/ConfigCommand.ts
ViewVC logotype

Annotation of /branches/8.x/src/commands/settings/ConfigCommand.ts

Parent Directory Parent Directory | Revision Log Revision Log


Revision 577 - (hide annotations)
Mon Jul 29 18:52:37 2024 UTC (8 months ago) by rakinar2
File MIME type: application/typescript
File size: 18640 byte(s)
chore: add old version archive branches (2.x to 9.x-dev)
1 rakinar2 577 /**
2     * This file is part of SudoBot.
3     *
4     * Copyright (C) 2021-2024 OSN Developers.
5     *
6     * SudoBot is free software; you can redistribute it and/or modify it
7     * under the terms of the GNU Affero General Public License as published by
8     * the Free Software Foundation, either version 3 of the License, or
9     * (at your option) any later version.
10     *
11     * SudoBot is distributed in the hope that it will be useful, but
12     * WITHOUT ANY WARRANTY; without even the implied warranty of
13     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14     * GNU Affero General Public License for more details.
15     *
16     * You should have received a copy of the GNU Affero General Public License
17     * along with SudoBot. If not, see <https://www.gnu.org/licenses/>.
18     */
19    
20     import {
21     ChatInputCommandInteraction,
22     Colors,
23     EmbedBuilder,
24     Interaction,
25     Message,
26     PermissionsBitField,
27     SlashCommandBuilder,
28     codeBlock,
29     escapeInlineCode,
30     inlineCode
31     } from "discord.js";
32     import JSON5 from "json5";
33     import Client from "../../core/Client";
34     import Command, {
35     ArgumentType,
36     BasicCommandContext,
37     CommandMessage,
38     CommandReturn,
39     ValidationRule
40     } from "../../core/Command";
41     import { GatewayEventListener } from "../../decorators/GatewayEventListener";
42     import { HasEventListeners } from "../../types/HasEventListeners";
43     import { get, has, set, toDotted } from "../../utils/objects";
44     import { isSystemAdmin } from "../../utils/utils";
45    
46     export default class ConfigCommand extends Command implements HasEventListeners {
47     public readonly name = "config";
48     public readonly subcommands = ["get", "set", "save", "restore"];
49     public readonly validationRules: ValidationRule[] = [
50     {
51     types: [ArgumentType.String],
52     name: "subcommand",
53     optional: false,
54     errors: {
55     required: `You must provide a subcommand. The valid subcommands are \`${this.subcommands.join(
56     "`, `"
57     )}\`.`
58     }
59     }
60     ];
61     public readonly permissions = [PermissionsBitField.Flags.ManageGuild];
62     public readonly aliases = ["setting", "settings"];
63     public readonly description = "View or change a configuration setting.";
64     public readonly argumentSyntaxes = ["<subcommand> <key> [value]"];
65     public readonly subcommandsMeta = {
66     get: {
67     description: "Get the value of a configuration key",
68     argumentSyntaxes: ["<key>"]
69     },
70     set: {
71     description: "Set the value of a configuration key",
72     argumentSyntaxes: ["<key> <value>"]
73     },
74     save: {
75     description: "Save the current configuration."
76     },
77     restore: {
78     description: "Restore the previously saved configuration."
79     }
80     };
81     public readonly slashCommandBuilder = new SlashCommandBuilder()
82     .addSubcommand(subcommand =>
83     subcommand
84     .setName("get")
85     .setDescription("Get the value of a configuration key")
86     .addStringOption(option =>
87     option
88     .setName("key")
89     .setDescription("The configuration key to view or change.")
90     .setAutocomplete(true)
91     .setRequired(true)
92     )
93     .addStringOption(option =>
94     option
95     .setName("config_type")
96     .setDescription("The configuration type")
97     .setChoices(
98     {
99     name: "Guild",
100     value: "guild"
101     },
102     {
103     name: "System",
104     value: "system"
105     }
106     )
107     )
108     )
109     .addSubcommand(subcommand =>
110     subcommand
111     .setName("set")
112     .setDescription("Set the value of a configuration key")
113     .addStringOption(option =>
114     option
115     .setName("key")
116     .setDescription("The configuration key to view or change.")
117     .setAutocomplete(true)
118     .setRequired(true)
119     )
120     .addStringOption(option =>
121     option
122     .setName("value")
123     .setDescription("The new value to set the configuration key to.")
124     .setRequired(true)
125     )
126     .addStringOption(option =>
127     option
128     .setName("cast")
129     .setDescription("The type to cast the value to.")
130     .setChoices(
131     {
132     name: "String",
133     value: "string"
134     },
135     {
136     name: "Number",
137     value: "number"
138     },
139     {
140     name: "Boolean",
141     value: "boolean"
142     },
143     {
144     name: "JSON",
145     value: "json"
146     }
147     )
148     )
149     .addBooleanOption(option =>
150     option
151     .setName("save")
152     .setDescription("Save the current configuration immediately.")
153     )
154     .addBooleanOption(option =>
155     option
156     .setName("no_create")
157     .setDescription("Do not create the key if it does not exist.")
158     )
159     .addStringOption(option =>
160     option
161     .setName("config_type")
162     .setDescription("The configuration type")
163     .setChoices(
164     {
165     name: "Guild",
166     value: "guild"
167     },
168     {
169     name: "System",
170     value: "system"
171     }
172     )
173     )
174     )
175     .addSubcommand(subcommand =>
176     subcommand.setName("save").setDescription("Save the current configuration.")
177     )
178     .addSubcommand(subcommand =>
179     subcommand
180     .setName("restore")
181     .setDescription("Restore the previously saved configuration.")
182     );
183     protected readonly dottedConfig = {
184     guild: {} as Record<string, string[]>,
185     system: [] as string[]
186     };
187    
188     constructor(client: Client<true>) {
189     super(client);
190     this.reloadDottedConfig();
191     }
192    
193     reloadDottedConfig(configType: "guild" | "system" | null = null) {
194     if (!configType || configType === "guild") {
195     const guildConfig: Record<string, string[]> = {};
196    
197     for (const key in this.client.configManager.config) {
198     guildConfig[key] = Object.keys(toDotted(this.client.configManager.config[key]!));
199     }
200    
201     this.dottedConfig.guild = guildConfig;
202     }
203    
204     if (!configType || configType === "system") {
205     this.dottedConfig.system = Object.keys(
206     toDotted(this.client.configManager.systemConfig)
207     );
208     }
209     }
210    
211     @GatewayEventListener("interactionCreate")
212     async onInteractionCreate(interaction: Interaction) {
213     if (!interaction.isAutocomplete() || interaction.commandName !== this.name) {
214     return;
215     }
216    
217     const query = interaction.options.getFocused();
218     const configType = (interaction.options.getString("config_type") ?? "guild") as
219     | "guild"
220     | "system";
221    
222     if (configType === "system" && !isSystemAdmin(this.client, interaction.user.id)) {
223     await interaction.respond([]);
224     return;
225     }
226    
227     const config =
228     configType === "guild"
229     ? this.dottedConfig.guild[interaction.guildId!]
230     : this.dottedConfig.system;
231     const keys = [];
232    
233     for (const key of config) {
234     if (keys.length >= 25) {
235     break;
236     }
237    
238     if (key.includes(query)) {
239     keys.push({ name: key, value: key });
240     }
241     }
242    
243     await interaction.respond(keys);
244     }
245    
246     async execute(message: CommandMessage, context: BasicCommandContext): Promise<CommandReturn> {
247     await this.deferIfInteraction(message);
248    
249     const subcommand = context.isLegacy
250     ? context.parsedNamedArgs.subcommand
251     : context.options.getSubcommand(true);
252    
253     if (
254     !context.isLegacy &&
255     (subcommand === "get" || subcommand === "set") &&
256     context.options.getString("config_type") === "system" &&
257     !isSystemAdmin(this.client, message.member!.user!.id)
258     ) {
259     await this.error(
260     message,
261     "You do not have permission to view or change system configuration."
262     );
263    
264     return;
265     }
266    
267     switch (subcommand) {
268     case "get":
269     return this.get(message, context);
270     case "set":
271     return this.set(message, context);
272     case "save":
273     return this.save(message);
274     case "restore":
275     return this.restore(message);
276     default:
277     await this.error(
278     message,
279     `The subcommand \`${escapeInlineCode(
280     subcommand
281     )}\` does not exist. Please use one of the following subcommands: \`${this.subcommands.join(
282     "`, `"
283     )}\`.`
284     );
285     return;
286     }
287     }
288    
289     private async get(
290     message: CommandMessage,
291     context: BasicCommandContext
292     ): Promise<CommandReturn> {
293     const key = context.isLegacy ? context.args[1] : context.options.getString("key", true);
294    
295     if (!key) {
296     await this.error(message, "You must provide a configuration key to view.");
297     return;
298     }
299    
300     const configType = (
301     context.isLegacy ? "guild" : context.options.getString("config_type") ?? "guild"
302     ) as "guild" | "system";
303     const config =
304     configType === "guild" ? context.config : this.client.configManager.systemConfig;
305    
306     if (!has(config, key)) {
307     await this.error(
308     message,
309     `The configuration key \`${escapeInlineCode(key)}\` does not exist.`
310     );
311     return;
312     }
313    
314     const configValue = get(config, key);
315     const embed = new EmbedBuilder()
316     .setTitle("Configuration Value")
317     .setDescription(
318     `### ${inlineCode(key)}\n\n${codeBlock(
319     "json",
320     JSON5.stringify(configValue, {
321     space: 2,
322     replacer: null,
323     quote: '"'
324     })
325     )}`
326     )
327     .setColor(Colors.Green)
328     .setTimestamp();
329    
330     await this.deferredReply(message, { embeds: [embed] });
331     }
332    
333     private async set(
334     message: CommandMessage,
335     context: BasicCommandContext
336     ): Promise<CommandReturn> {
337     if (context.isLegacy) {
338     if (!context.args[1]) {
339     await this.error(message, "You must provide a configuration key to set.");
340     return;
341     }
342    
343     if (!context.args[2]) {
344     await this.error(
345     message,
346     "You must provide a value to set the configuration key to."
347     );
348     return;
349     }
350     }
351    
352     const key = context.isLegacy ? context.args[1] : context.options.getString("key", true);
353     const value =
354     message instanceof Message && context.isLegacy
355     ? message.content
356     .slice(context.prefix.length)
357     .trimStart()
358     .slice(context.argv[0].length)
359     .trimStart()
360     .slice(context.argv[1].length)
361     .trimStart()
362     .slice(context.argv[2].length)
363     .trim() // FIXME: Extract this into a method
364     : (message as ChatInputCommandInteraction).options.getString("value", true);
365     const cast = (
366     context.isLegacy ? "json" : context.options.getString("cast") ?? "string"
367     ) as CastType;
368     const save = context.isLegacy ? false : context.options.getBoolean("save");
369     const noCreate = context.isLegacy ? false : context.options.getBoolean("no_create");
370     const configType = (
371     context.isLegacy ? "guild" : context.options.getString("config_type") ?? "guild"
372     ) as "guild" | "system";
373     const config =
374     configType === "guild" ? context.config : this.client.configManager.systemConfig;
375    
376     if (!key) {
377     await this.error(message, "You must provide a configuration key to set.");
378     return;
379     }
380    
381     if (noCreate && !has(config, key)) {
382     await this.error(
383     message,
384     `The configuration key \`${escapeInlineCode(key)}\` does not exist.`
385     );
386     return;
387     }
388    
389     let finalValue;
390    
391     switch (cast) {
392     case "string":
393     finalValue = value;
394     break;
395     case "number":
396     finalValue = parseFloat(value);
397    
398     if (isNaN(finalValue)) {
399     await this.error(
400     message,
401     `The value \`${escapeInlineCode(value)}\` is not a valid number.`
402     );
403     return;
404     }
405    
406     break;
407     case "boolean":
408     {
409     const lowerCased = value.toLowerCase();
410    
411     if (lowerCased !== "true" && lowerCased !== "false") {
412     await this.error(
413     message,
414     `The value \`${escapeInlineCode(value)}\` is not a valid boolean.`
415     );
416     return;
417     }
418    
419     finalValue = lowerCased === "true";
420     }
421     break;
422     case "json":
423     try {
424     finalValue = JSON5.parse(value);
425     } catch (e) {
426     const error = codeBlock(
427     e instanceof Object && "message" in e ? `${e.message}` : `${e}`
428     );
429     await this.deferredReply(message, {
430     embeds: [
431     {
432     description: `### ${this.emoji(
433     "error"
434     )} Failed to parse the value as JSON\n\n${error.slice(0, 1800)}${
435     error.length > 1800
436     ? "\n... The error message is loo long."
437     : ""
438     }`,
439     color: Colors.Red,
440     footer: {
441     text: "No changes were made to the configuration"
442     },
443     timestamp: new Date().toISOString()
444     }
445     ]
446     });
447    
448     return;
449     }
450    
451     break;
452     }
453    
454     set(config, key, finalValue);
455    
456     const embed = new EmbedBuilder();
457     const error = this.client.configManager.testConfig();
458     const errorString = error
459     ? JSON5.stringify(error.error.format(), {
460     space: 2,
461     replacer: null,
462     quote: '"'
463     })
464     : null;
465    
466     if (errorString && error) {
467     await this.client.configManager.load();
468    
469     embed
470     .setDescription(
471     `### ${this.emoji("error")} The configuration is invalid (${inlineCode(
472     error.type
473     )})\n\nThe changes were not saved.\n\n${errorString.slice(0, 1800)}${
474     errorString.length > 1800 ? "\n... The error description is loo long." : ""
475     }`
476     )
477     .setColor(Colors.Red)
478     .setFooter({ text: "The configuration was not saved." });
479    
480     await this.deferredReply(message, { embeds: [embed] });
481     return;
482     }
483    
484     embed
485     .setTitle("Configuration Value Changed")
486     .setDescription(
487     `### ${inlineCode(key)}\n\n${codeBlock(
488     "json",
489     JSON5.stringify(finalValue, {
490     space: 2,
491     replacer: null,
492     quote: '"'
493     })
494     )}`
495     )
496     .setColor(Colors.Green)
497     .setTimestamp()
498     .setFooter({ text: `The configuration was ${save ? "saved" : "applied"}.` });
499    
500     if (save) {
501     await this.client.configManager.write({
502     guild: configType === "guild",
503     system: configType === "system"
504     });
505     }
506    
507     await this.deferredReply(message, { embeds: [embed] });
508     this.reloadDottedConfig(configType);
509     }
510    
511     private async save(message: CommandMessage): Promise<CommandReturn> {
512     await this.client.configManager.write();
513     await this.success(message, "The configuration was saved.");
514     }
515    
516     private async restore(message: CommandMessage): Promise<CommandReturn> {
517     await this.client.configManager.load();
518     await this.success(message, "The configuration was restored.");
519     }
520     }
521    
522     type CastType = "string" | "number" | "boolean" | "json";

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26