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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 577 - (show 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 /**
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