@open-discord-bots/framework 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/LICENSE.md +713 -0
  2. package/README.md +104 -0
  3. package/dist/api/api.d.ts +26 -0
  4. package/dist/api/api.js +44 -0
  5. package/dist/api/main.d.ts +133 -0
  6. package/dist/api/main.js +87 -0
  7. package/dist/api/modules/action.d.ts +34 -0
  8. package/dist/api/modules/action.js +58 -0
  9. package/dist/api/modules/base.d.ts +329 -0
  10. package/dist/api/modules/base.js +804 -0
  11. package/dist/api/modules/builder.d.ts +647 -0
  12. package/dist/api/modules/builder.js +1441 -0
  13. package/dist/api/modules/checker.d.ts +648 -0
  14. package/dist/api/modules/checker.js +1324 -0
  15. package/dist/api/modules/client.d.ts +768 -0
  16. package/dist/api/modules/client.js +1859 -0
  17. package/dist/api/modules/code.d.ts +33 -0
  18. package/dist/api/modules/code.js +57 -0
  19. package/dist/api/modules/config.d.ts +70 -0
  20. package/dist/api/modules/config.js +206 -0
  21. package/dist/api/modules/console.d.ts +305 -0
  22. package/dist/api/modules/console.js +598 -0
  23. package/dist/api/modules/cooldown.d.ts +138 -0
  24. package/dist/api/modules/cooldown.js +359 -0
  25. package/dist/api/modules/database.d.ts +135 -0
  26. package/dist/api/modules/database.js +271 -0
  27. package/dist/api/modules/event.d.ts +43 -0
  28. package/dist/api/modules/event.js +100 -0
  29. package/dist/api/modules/flag.d.ts +40 -0
  30. package/dist/api/modules/flag.js +72 -0
  31. package/dist/api/modules/fuse.d.ts +218 -0
  32. package/dist/api/modules/fuse.js +123 -0
  33. package/dist/api/modules/helpmenu.d.ts +106 -0
  34. package/dist/api/modules/helpmenu.js +167 -0
  35. package/dist/api/modules/language.d.ts +85 -0
  36. package/dist/api/modules/language.js +195 -0
  37. package/dist/api/modules/permission.d.ts +121 -0
  38. package/dist/api/modules/permission.js +314 -0
  39. package/dist/api/modules/plugin.d.ts +128 -0
  40. package/dist/api/modules/plugin.js +168 -0
  41. package/dist/api/modules/post.d.ts +44 -0
  42. package/dist/api/modules/post.js +92 -0
  43. package/dist/api/modules/progressbar.d.ts +108 -0
  44. package/dist/api/modules/progressbar.js +233 -0
  45. package/dist/api/modules/responder.d.ts +506 -0
  46. package/dist/api/modules/responder.js +1468 -0
  47. package/dist/api/modules/session.d.ts +58 -0
  48. package/dist/api/modules/session.js +171 -0
  49. package/dist/api/modules/startscreen.d.ts +165 -0
  50. package/dist/api/modules/startscreen.js +293 -0
  51. package/dist/api/modules/stat.d.ts +142 -0
  52. package/dist/api/modules/stat.js +293 -0
  53. package/dist/api/modules/verifybar.d.ts +54 -0
  54. package/dist/api/modules/verifybar.js +60 -0
  55. package/dist/api/modules/worker.d.ts +41 -0
  56. package/dist/api/modules/worker.js +93 -0
  57. package/dist/api/utils.d.ts +61 -0
  58. package/dist/api/utils.js +254 -0
  59. package/dist/index.d.ts +4 -1
  60. package/dist/index.js +40 -0
  61. package/dist/startup/dump.d.ts +14 -0
  62. package/dist/startup/dump.js +79 -0
  63. package/dist/startup/errorHandling.d.ts +2 -0
  64. package/dist/startup/errorHandling.js +43 -0
  65. package/dist/startup/pluginLauncher.d.ts +2 -0
  66. package/dist/startup/pluginLauncher.js +202 -0
  67. package/package.json +9 -3
  68. package/src/api/api.ts +29 -0
  69. package/src/api/main.ts +189 -0
  70. package/src/api/modules/action.ts +58 -0
  71. package/src/api/modules/base.ts +811 -0
  72. package/src/api/modules/builder.ts +1554 -0
  73. package/src/api/modules/checker.ts +1549 -0
  74. package/src/api/modules/client.ts +2247 -0
  75. package/src/api/modules/code.ts +58 -0
  76. package/src/api/modules/config.ts +159 -0
  77. package/src/api/modules/console.ts +665 -0
  78. package/src/api/modules/cooldown.ts +348 -0
  79. package/src/api/modules/database.ts +278 -0
  80. package/src/api/modules/event.ts +99 -0
  81. package/src/api/modules/flag.ts +73 -0
  82. package/src/api/modules/fuse.ts +348 -0
  83. package/src/api/modules/helpmenu.ts +216 -0
  84. package/src/api/modules/language.ts +201 -0
  85. package/src/api/modules/permission.ts +340 -0
  86. package/src/api/modules/plugin.ts +242 -0
  87. package/src/api/modules/post.ts +90 -0
  88. package/src/api/modules/progressbar.ts +232 -0
  89. package/src/api/modules/responder.ts +1420 -0
  90. package/src/api/modules/session.ts +155 -0
  91. package/src/api/modules/startscreen.ts +320 -0
  92. package/src/api/modules/stat.ts +313 -0
  93. package/src/api/modules/verifybar.ts +61 -0
  94. package/src/api/modules/worker.ts +93 -0
  95. package/src/api/utils.ts +206 -0
  96. package/src/cli/cli.ts +151 -0
  97. package/src/cli/editConfig.ts +943 -0
  98. package/src/index.ts +6 -1
  99. package/src/startup/compilation.ts +186 -0
  100. package/src/startup/dump.ts +45 -0
  101. package/src/startup/errorHandling.ts +38 -0
  102. package/src/startup/pluginLauncher.ts +261 -0
  103. package/LICENSE +0 -21
@@ -0,0 +1,1859 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ODAutocompleteManager = exports.ODContextMenu = exports.ODContextMenuManager = exports.ODContextMenuComparator = exports.ODTextCommandManager = exports.ODTextCommand = exports.ODSlashCommand = exports.ODSlashCommandManager = exports.ODSlashCommandComparator = exports.ODClientActivityManager = exports.ODClientManager = void 0;
37
+ ///////////////////////////////////////
38
+ //DISCORD CLIENT MODULE
39
+ ///////////////////////////////////////
40
+ const base_1 = require("./base");
41
+ const discord = __importStar(require("discord.js"));
42
+ const rest_1 = require("@discordjs/rest");
43
+ const console_1 = require("./console");
44
+ /**## ODClientManager `class`
45
+ * This is an Open Discord client manager.
46
+ *
47
+ * It is responsible for managing the discord.js client. Here, you can set the status, register slash commands and much more!
48
+ *
49
+ * If you want, you can also listen for custom events on the `ODClientManager.client` variable (`discord.Client`)
50
+ */
51
+ class ODClientManager {
52
+ /**Alias to Open Discord debugger. */
53
+ #debug;
54
+ /**List of required bot intents. Add intents to this list using the `onClientLoad` event. */
55
+ intents = [];
56
+ /**List of required bot privileged intents. Add intents to this list using the `onClientLoad` event. */
57
+ privileges = [];
58
+ /**List of required bot partials. Add intents to this list using the `onClientLoad` event. **❌ Only use when neccessery!** */
59
+ partials = [];
60
+ /**List of required bot permissions. Add permissions to this list using the `onClientLoad` event. */
61
+ permissions = [];
62
+ /**The discord bot token, empty by default. */
63
+ set token(value) {
64
+ this.#token = value;
65
+ this.rest.setToken(value);
66
+ }
67
+ get token() {
68
+ return this.#token;
69
+ }
70
+ /**The discord bot token. **DON'T USE THIS!!!** (use `ODClientManager.token` instead) */
71
+ #token = "";
72
+ /**The discord.js `discord.Client`. Only use it when initiated! */
73
+ client = new discord.Client({ intents: [] }); //temporary client
74
+ /**The discord.js REST client. Used for stuff that discord.js can't handle :) */
75
+ rest = new rest_1.REST({ version: "10" });
76
+ /**Is the bot initiated? */
77
+ initiated = false;
78
+ /**Is the bot logged in? */
79
+ loggedIn = false;
80
+ /**Is the bot ready? */
81
+ ready = false;
82
+ /**The main server of the bot. Provided by serverId in the config */
83
+ mainServer = null;
84
+ /**(❌ DO NOT OVERWRITE ❌) Internal Open Discord function to continue the startup when the client is ready! */
85
+ readyListener = null;
86
+ /**The status manager is responsible for setting the bot status. */
87
+ activity;
88
+ /**The slash command manager is responsible for all slash commands & their events inside the bot. */
89
+ slashCommands;
90
+ /**The text command manager is responsible for all text commands & their events inside the bot. */
91
+ textCommands;
92
+ /**The context menu manager is responsible for all context menus & their events inside the bot. */
93
+ contextMenus;
94
+ /**The autocomplete manager is responsible for all autocomplete events inside the bot. */
95
+ autocompletes;
96
+ constructor(debug) {
97
+ this.#debug = debug;
98
+ this.activity = new ODClientActivityManager(this.#debug, this);
99
+ this.slashCommands = new ODSlashCommandManager(this.#debug, this);
100
+ this.textCommands = new ODTextCommandManager(this.#debug, this);
101
+ this.contextMenus = new ODContextMenuManager(this.#debug, this);
102
+ this.autocompletes = new ODAutocompleteManager(this.#debug, this);
103
+ }
104
+ /**Initiate the `client` variable & add the intents & partials to the bot. */
105
+ initClient() {
106
+ if (!this.intents.every((value) => typeof discord.GatewayIntentBits[value] != "undefined"))
107
+ throw new base_1.ODSystemError("Client has non-existing intents!");
108
+ if (!this.privileges.every((value) => typeof { GuildMembers: true, MessageContent: true, Presence: true }[value] != "undefined"))
109
+ throw new base_1.ODSystemError("Client has non-existing privileged intents!");
110
+ if (!this.partials.every((value) => typeof discord.Partials[value] != "undefined"))
111
+ throw new base_1.ODSystemError("Client has non-existing partials!");
112
+ if (!this.permissions.every((value) => typeof discord.PermissionFlagsBits[value] != "undefined"))
113
+ throw new base_1.ODSystemError("Client has non-existing partials!");
114
+ const intents = this.intents.map((value) => discord.GatewayIntentBits[value]);
115
+ const partials = this.partials.map((value) => discord.Partials[value]);
116
+ const oldClient = this.client;
117
+ this.client = new discord.Client({ intents, partials });
118
+ //@ts-ignore
119
+ oldClient.eventNames().forEach((event) => {
120
+ //@ts-ignore
121
+ const callbacks = oldClient.rawListeners(event);
122
+ callbacks.forEach((cb) => {
123
+ this.client.on(event, cb);
124
+ });
125
+ });
126
+ this.initiated = true;
127
+ this.#debug.debug("Created client with intents: " + this.intents.join(", "));
128
+ this.#debug.debug("Created client with privileged intents: " + this.privileges.join(", "));
129
+ this.#debug.debug("Created client with partials: " + this.partials.join(", "));
130
+ this.#debug.debug("Created client with permissions: " + this.permissions.join(", "));
131
+ }
132
+ /**Get all servers the bot is part of. */
133
+ async getGuilds() {
134
+ if (!this.initiated)
135
+ throw new base_1.ODSystemError("Client isn't initiated yet!");
136
+ if (!this.ready)
137
+ throw new base_1.ODSystemError("Client isn't ready yet!");
138
+ return this.client.guilds.cache.map((guild) => guild);
139
+ }
140
+ /**Check if the bot is in a specific guild */
141
+ checkBotInGuild(guild) {
142
+ return (guild.members.me) ? true : false;
143
+ }
144
+ /**Check if a specific guild has all required permissions (or `Administrator`) */
145
+ checkGuildPerms(guild) {
146
+ if (!guild.members.me)
147
+ throw new base_1.ODSystemError("Client isn't a member in this server!");
148
+ const perms = guild.members.me.permissions;
149
+ if (perms.has("Administrator"))
150
+ return true;
151
+ else {
152
+ return this.permissions.every((perm) => {
153
+ return perms.has(perm);
154
+ });
155
+ }
156
+ }
157
+ /**Log-in with a discord auth token. Rejects returns `false` using 'softErrors' on failure. */
158
+ login(softErrors) {
159
+ return new Promise(async (resolve, reject) => {
160
+ if (!this.initiated)
161
+ reject("Client isn't initiated yet!");
162
+ if (!this.token)
163
+ reject("Client doesn't have a token!");
164
+ try {
165
+ this.client.once("clientReady", async () => {
166
+ this.ready = true;
167
+ //set slashCommandManager & contextMenuManager to client applicationCommandManager
168
+ if (!this.client.application)
169
+ throw new base_1.ODSystemError("Couldn't get client application for slashCommand & contextMenu managers!");
170
+ this.slashCommands.commandManager = this.client.application.commands;
171
+ this.contextMenus.commandManager = this.client.application.commands;
172
+ this.autocompletes.commandManager = this.client.application.commands;
173
+ if (this.readyListener)
174
+ await this.readyListener();
175
+ resolve(true);
176
+ });
177
+ this.#debug.debug("Actual discord.js client.login()");
178
+ await this.client.login(this.token);
179
+ this.#debug.debug("Finished discord.js client.login()");
180
+ this.loggedIn = true;
181
+ }
182
+ catch (err) {
183
+ if (softErrors)
184
+ return resolve(false);
185
+ else if (err.message.toLowerCase().includes("used disallowed intents")) {
186
+ process.emit("uncaughtException", new base_1.ODSystemError("Used disallowed intents"));
187
+ }
188
+ else if (err.message.toLowerCase().includes("tokeninvalid") || err.message.toLowerCase().includes("an invalid token was provided")) {
189
+ process.emit("uncaughtException", new base_1.ODSystemError("Invalid discord bot token provided"));
190
+ }
191
+ else
192
+ reject("OD Login Error: " + err);
193
+ }
194
+ });
195
+ }
196
+ /**A simplified shortcut to get a `discord.User` :) */
197
+ async fetchUser(id) {
198
+ if (!this.initiated)
199
+ throw new base_1.ODSystemError("Client isn't initiated yet!");
200
+ if (!this.ready)
201
+ throw new base_1.ODSystemError("Client isn't ready yet!");
202
+ try {
203
+ return await this.client.users.fetch(id);
204
+ }
205
+ catch {
206
+ return null;
207
+ }
208
+ }
209
+ /**A simplified shortcut to get a `discord.Guild` :) */
210
+ async fetchGuild(id) {
211
+ if (!this.initiated)
212
+ throw new base_1.ODSystemError("Client isn't initiated yet!");
213
+ if (!this.ready)
214
+ throw new base_1.ODSystemError("Client isn't ready yet!");
215
+ try {
216
+ return await this.client.guilds.fetch(id);
217
+ }
218
+ catch {
219
+ return null;
220
+ }
221
+ }
222
+ /**A simplified shortcut to get a `discord.Channel` :) */
223
+ async fetchChannel(id) {
224
+ if (!this.initiated)
225
+ throw new base_1.ODSystemError("Client isn't initiated yet!");
226
+ if (!this.ready)
227
+ throw new base_1.ODSystemError("Client isn't ready yet!");
228
+ try {
229
+ return await this.client.channels.fetch(id);
230
+ }
231
+ catch {
232
+ return null;
233
+ }
234
+ }
235
+ /**A simplified shortcut to get a `discord.GuildBasedChannel` :) */
236
+ async fetchGuildChannel(guildId, id) {
237
+ if (!this.initiated)
238
+ throw new base_1.ODSystemError("Client isn't initiated yet!");
239
+ if (!this.ready)
240
+ throw new base_1.ODSystemError("Client isn't ready yet!");
241
+ try {
242
+ const guild = (guildId instanceof discord.Guild) ? guildId : await this.fetchGuild(guildId);
243
+ if (!guild)
244
+ return null;
245
+ const channel = await guild.channels.fetch(id);
246
+ return channel;
247
+ }
248
+ catch {
249
+ return null;
250
+ }
251
+ }
252
+ /**A simplified shortcut to get a `discord.TextChannel` :) */
253
+ async fetchGuildTextChannel(guildId, id) {
254
+ if (!this.initiated)
255
+ throw new base_1.ODSystemError("Client isn't initiated yet!");
256
+ if (!this.ready)
257
+ throw new base_1.ODSystemError("Client isn't ready yet!");
258
+ try {
259
+ const guild = (guildId instanceof discord.Guild) ? guildId : await this.fetchGuild(guildId);
260
+ if (!guild)
261
+ return null;
262
+ const channel = await guild.channels.fetch(id);
263
+ if (!channel || channel.type != discord.ChannelType.GuildText)
264
+ return null;
265
+ return channel;
266
+ }
267
+ catch {
268
+ return null;
269
+ }
270
+ }
271
+ /**A simplified shortcut to get a `discord.CategoryChannel` :) */
272
+ async fetchGuildCategoryChannel(guildId, id) {
273
+ if (!this.initiated)
274
+ throw new base_1.ODSystemError("Client isn't initiated yet!");
275
+ if (!this.ready)
276
+ throw new base_1.ODSystemError("Client isn't ready yet!");
277
+ try {
278
+ const guild = (guildId instanceof discord.Guild) ? guildId : await this.fetchGuild(guildId);
279
+ if (!guild)
280
+ return null;
281
+ const channel = await guild.channels.fetch(id);
282
+ if (!channel || channel.type != discord.ChannelType.GuildCategory)
283
+ return null;
284
+ return channel;
285
+ }
286
+ catch {
287
+ return null;
288
+ }
289
+ }
290
+ /**A simplified shortcut to get a `discord.GuildMember` :) */
291
+ async fetchGuildMember(guildId, id) {
292
+ if (!this.initiated)
293
+ throw new base_1.ODSystemError("Client isn't initiated yet!");
294
+ if (!this.ready)
295
+ throw new base_1.ODSystemError("Client isn't ready yet!");
296
+ if (typeof id != "string")
297
+ throw new base_1.ODSystemError("TEMP ERROR => ODClientManager.fetchGuildMember() => id param isn't string");
298
+ try {
299
+ const guild = (guildId instanceof discord.Guild) ? guildId : await this.fetchGuild(guildId);
300
+ if (!guild)
301
+ return null;
302
+ return await guild.members.fetch(id);
303
+ }
304
+ catch {
305
+ return null;
306
+ }
307
+ }
308
+ /**A simplified shortcut to get a `discord.Role` :) */
309
+ async fetchGuildRole(guildId, id) {
310
+ if (!this.initiated)
311
+ throw new base_1.ODSystemError("Client isn't initiated yet!");
312
+ if (!this.ready)
313
+ throw new base_1.ODSystemError("Client isn't ready yet!");
314
+ if (typeof id != "string")
315
+ throw new base_1.ODSystemError("TEMP ERROR => ODClientManager.fetchGuildRole() => id param isn't string");
316
+ try {
317
+ const guild = (guildId instanceof discord.Guild) ? guildId : await this.fetchGuild(guildId);
318
+ if (!guild)
319
+ return null;
320
+ return await guild.roles.fetch(id);
321
+ }
322
+ catch {
323
+ return null;
324
+ }
325
+ }
326
+ async fetchGuildChannelMessage(guildId, channelId, id) {
327
+ if (!this.initiated)
328
+ throw new base_1.ODSystemError("Client isn't initiated yet!");
329
+ if (!this.ready)
330
+ throw new base_1.ODSystemError("Client isn't ready yet!");
331
+ try {
332
+ if (guildId instanceof discord.TextChannel && typeof channelId == "string") {
333
+ const channel = guildId;
334
+ return await channel.messages.fetch(channelId);
335
+ }
336
+ else if (!(guildId instanceof discord.TextChannel) && id) {
337
+ const channel = (channelId instanceof discord.TextChannel) ? channelId : await this.fetchGuildTextChannel(guildId, channelId);
338
+ if (!channel)
339
+ return null;
340
+ return await channel.messages.fetch(id);
341
+ }
342
+ else
343
+ return null;
344
+ }
345
+ catch {
346
+ return null;
347
+ }
348
+ }
349
+ /**A simplified shortcut to send a DM to a user :) */
350
+ async sendUserDm(user, message) {
351
+ if (!this.initiated)
352
+ throw new base_1.ODSystemError("Client isn't initiated yet!");
353
+ if (!this.ready)
354
+ throw new base_1.ODSystemError("Client isn't ready yet!");
355
+ try {
356
+ if (user instanceof discord.User) {
357
+ if (user.bot)
358
+ return { success: false, message: null };
359
+ const channel = await user.createDM();
360
+ const msg = await channel.send(message.message);
361
+ return { success: true, message: msg };
362
+ }
363
+ else {
364
+ const newUser = await this.fetchUser(user);
365
+ if (!newUser)
366
+ throw new Error();
367
+ if (newUser.bot)
368
+ return { success: false, message: null };
369
+ const channel = await newUser.createDM();
370
+ const msg = await channel.send(message.message);
371
+ return { success: true, message: msg };
372
+ }
373
+ }
374
+ catch {
375
+ try {
376
+ this.#debug.console.log("Failed to send DM to user! ", "warning", [
377
+ { key: "id", value: (user instanceof discord.User ? user.id : user) },
378
+ { key: "message", value: message.id.value }
379
+ ]);
380
+ }
381
+ catch { }
382
+ return { success: false, message: null };
383
+ }
384
+ }
385
+ }
386
+ exports.ODClientManager = ODClientManager;
387
+ /**## ODClientActivityManager `class`
388
+ * This is an Open Discord client activity manager.
389
+ *
390
+ * It's responsible for managing the client status. Here, you can set the activity & status of the bot.
391
+ *
392
+ * It also has a built-in refresh function, so the status will refresh every 10 minutes to keep it visible.
393
+ */
394
+ class ODClientActivityManager {
395
+ /**Alias to Open Discord debugger. */
396
+ #debug;
397
+ /**Copy of discord.js client */
398
+ manager;
399
+ /**The current status type */
400
+ type = false;
401
+ /**The current status text */
402
+ text = "";
403
+ /**The current status mode */
404
+ mode = "online";
405
+ /**Additional state text */
406
+ state = "";
407
+ /**The timer responsible for refreshing the status. Stop it using `clearInterval(interval)` */
408
+ interval;
409
+ /**status refresh interval in seconds (5 minutes by default)*/
410
+ refreshInterval = 600;
411
+ /**Is the status already initiated? */
412
+ initiated = false;
413
+ constructor(debug, manager) {
414
+ this.#debug = debug;
415
+ this.manager = manager;
416
+ }
417
+ /**Update the status. When already initiated, it can take up to 10min to see the updated status in discord. */
418
+ setStatus(type, text, mode, state, forceUpdate) {
419
+ this.type = type;
420
+ this.text = text;
421
+ this.mode = mode;
422
+ this.state = state;
423
+ if (forceUpdate)
424
+ this.#updateClientActivity(this.type, this.text);
425
+ }
426
+ /**When initiating the status, the bot starts updating the status using `discord.js`. Returns `true` when successfull. */
427
+ initStatus() {
428
+ if (this.initiated || !this.manager.ready)
429
+ return false;
430
+ this.#updateClientActivity(this.type, this.text);
431
+ this.interval = setInterval(() => {
432
+ this.#updateClientActivity(this.type, this.text);
433
+ this.#debug.debug("Client status update cycle");
434
+ }, this.refreshInterval * 1000);
435
+ this.initiated = true;
436
+ this.#debug.debug("Client status initiated");
437
+ return true;
438
+ }
439
+ /**Update the client status */
440
+ #updateClientActivity(type, text) {
441
+ if (!this.manager.client.user)
442
+ throw new base_1.ODSystemError("Couldn't set client status: client.user == undefined");
443
+ if (type == false) {
444
+ this.manager.client.user.setActivity();
445
+ return;
446
+ }
447
+ this.manager.client.user.setPresence({
448
+ activities: [{
449
+ type: this.#getStatusTypeEnum(type),
450
+ state: this.state ? this.state : undefined,
451
+ name: text,
452
+ }],
453
+ status: this.mode
454
+ });
455
+ }
456
+ /**Get the enum that links to the correct type */
457
+ #getStatusTypeEnum(type) {
458
+ if (type == "playing")
459
+ return discord.ActivityType.Playing;
460
+ else if (type == "listening")
461
+ return discord.ActivityType.Listening;
462
+ else if (type == "watching")
463
+ return discord.ActivityType.Watching;
464
+ else if (type == "custom")
465
+ return discord.ActivityType.Custom;
466
+ else
467
+ return discord.ActivityType.Listening;
468
+ }
469
+ /**Get the status type (for displaying the status) */
470
+ getStatusType() {
471
+ if (this.type == "listening" || this.type == "playing" || this.type == "watching")
472
+ return this.type + " ";
473
+ else
474
+ return "";
475
+ }
476
+ }
477
+ exports.ODClientActivityManager = ODClientActivityManager;
478
+ /**## ODSlashCommandComparator `class`
479
+ * A utility class to compare existing slash commands with newly registered ones.
480
+ */
481
+ class ODSlashCommandComparator {
482
+ /**Convert a `discord.ApplicationCommandOptionChoiceData<string>` to a universal Open Discord slash command option choice object for comparison. */
483
+ #convertOptionChoice(choice) {
484
+ const nameLoc = choice.nameLocalizations ?? {};
485
+ return {
486
+ name: choice.name,
487
+ nameLocalizations: Object.keys(nameLoc).map((key) => { return { language: key, value: nameLoc[key] }; }),
488
+ value: choice.value
489
+ };
490
+ }
491
+ /**Convert a `discord.ApplicationCommandOptionData` to a universal Open Discord slash command option object for comparison. */
492
+ #convertBuilderOption(option) {
493
+ const nameLoc = option.nameLocalizations ?? {};
494
+ const descLoc = option.descriptionLocalizations ?? {};
495
+ return {
496
+ type: option.type,
497
+ name: option.name,
498
+ nameLocalizations: Object.keys(nameLoc).map((key) => { return { language: key, value: nameLoc[key] }; }),
499
+ description: option.description,
500
+ descriptionLocalizations: Object.keys(descLoc).map((key) => { return { language: key, value: descLoc[key] }; }),
501
+ required: (option.type != discord.ApplicationCommandOptionType.SubcommandGroup && option.type != discord.ApplicationCommandOptionType.Subcommand && option.required) ? true : false,
502
+ autocomplete: option.autocomplete ?? false,
503
+ choices: (option.type == discord.ApplicationCommandOptionType.String && !option.autocomplete && option.choices) ? option.choices.map((choice) => this.#convertOptionChoice(choice)) : [],
504
+ options: ((option.type == discord.ApplicationCommandOptionType.SubcommandGroup || option.type == discord.ApplicationCommandOptionType.Subcommand) && option.options) ? option.options.map((opt) => this.#convertBuilderOption(opt)) : [],
505
+ channelTypes: (option.type == discord.ApplicationCommandOptionType.Channel && option.channelTypes) ? option.channelTypes : [],
506
+ minValue: (option.type == discord.ApplicationCommandOptionType.Number && option.minValue) ? option.minValue : null,
507
+ maxValue: (option.type == discord.ApplicationCommandOptionType.Number && option.maxValue) ? option.maxValue : null,
508
+ minLength: (option.type == discord.ApplicationCommandOptionType.String && option.minLength) ? option.minLength : null,
509
+ maxLength: (option.type == discord.ApplicationCommandOptionType.String && option.maxLength) ? option.maxLength : null
510
+ };
511
+ }
512
+ /**Convert a `discord.ApplicationCommandOption` to a universal Open Discord slash command option object for comparison. */
513
+ #convertCommandOption(option) {
514
+ const nameLoc = option.nameLocalizations ?? {};
515
+ const descLoc = option.descriptionLocalizations ?? {};
516
+ return {
517
+ type: option.type,
518
+ name: option.name,
519
+ nameLocalizations: Object.keys(nameLoc).map((key) => { return { language: key, value: nameLoc[key] }; }),
520
+ description: option.description,
521
+ descriptionLocalizations: Object.keys(descLoc).map((key) => { return { language: key, value: descLoc[key] }; }),
522
+ required: (option.type != discord.ApplicationCommandOptionType.SubcommandGroup && option.type != discord.ApplicationCommandOptionType.Subcommand && option.required) ? true : false,
523
+ autocomplete: option.autocomplete ?? false,
524
+ choices: (option.type == discord.ApplicationCommandOptionType.String && !option.autocomplete && option.choices) ? option.choices.map((choice) => this.#convertOptionChoice(choice)) : [],
525
+ options: ((option.type == discord.ApplicationCommandOptionType.SubcommandGroup || option.type == discord.ApplicationCommandOptionType.Subcommand) && option.options) ? option.options.map((opt) => this.#convertBuilderOption(opt)) : [],
526
+ channelTypes: (option.type == discord.ApplicationCommandOptionType.Channel && option.channelTypes) ? option.channelTypes : [],
527
+ minValue: (option.type == discord.ApplicationCommandOptionType.Number && option.minValue) ? option.minValue : null,
528
+ maxValue: (option.type == discord.ApplicationCommandOptionType.Number && option.maxValue) ? option.maxValue : null,
529
+ minLength: (option.type == discord.ApplicationCommandOptionType.String && option.minLength) ? option.minLength : null,
530
+ maxLength: (option.type == discord.ApplicationCommandOptionType.String && option.maxLength) ? option.maxLength : null
531
+ };
532
+ }
533
+ /**Convert a `ODSlashCommandBuilder` to a universal Open Discord slash command object for comparison. */
534
+ convertBuilder(builder, guildId) {
535
+ if (builder.type != discord.ApplicationCommandType.ChatInput)
536
+ return null; //throw new ODSystemError("ODSlashCommandComparator:convertBuilder() is not supported for other types than 'ChatInput'!")
537
+ const nameLoc = builder.nameLocalizations ?? {};
538
+ const descLoc = builder.descriptionLocalizations ?? {};
539
+ return {
540
+ type: 1,
541
+ name: builder.name,
542
+ nameLocalizations: Object.keys(nameLoc).map((key) => { return { language: key, value: nameLoc[key] }; }),
543
+ description: builder.description,
544
+ descriptionLocalizations: Object.keys(descLoc).map((key) => { return { language: key, value: descLoc[key] }; }),
545
+ guildId: guildId,
546
+ nsfw: builder.nsfw ?? false,
547
+ options: builder.options ? builder.options.map((opt) => this.#convertBuilderOption(opt)) : [],
548
+ defaultMemberPermissions: discord.PermissionsBitField.resolve(builder.defaultMemberPermissions ?? ["ViewChannel"]),
549
+ dmPermission: (builder.contexts && builder.contexts.includes(discord.InteractionContextType.BotDM)) ?? false,
550
+ integrationTypes: builder.integrationTypes ?? [discord.ApplicationIntegrationType.GuildInstall],
551
+ contexts: builder.contexts ?? []
552
+ };
553
+ }
554
+ /**Convert a `discord.ApplicationCommand` to a universal Open Discord slash command object for comparison. */
555
+ convertCommand(cmd) {
556
+ if (cmd.type != discord.ApplicationCommandType.ChatInput)
557
+ return null; //throw new ODSystemError("ODSlashCommandComparator:convertCommand() is not supported for other types than 'ChatInput'!")
558
+ const nameLoc = cmd.nameLocalizations ?? {};
559
+ const descLoc = cmd.descriptionLocalizations ?? {};
560
+ return {
561
+ type: 1,
562
+ name: cmd.name,
563
+ nameLocalizations: Object.keys(nameLoc).map((key) => { return { language: key, value: nameLoc[key] }; }),
564
+ description: cmd.description,
565
+ descriptionLocalizations: Object.keys(descLoc).map((key) => { return { language: key, value: descLoc[key] }; }),
566
+ guildId: cmd.guildId,
567
+ nsfw: cmd.nsfw,
568
+ options: cmd.options ? cmd.options.map((opt) => this.#convertCommandOption(opt)) : [],
569
+ defaultMemberPermissions: discord.PermissionsBitField.resolve(cmd.defaultMemberPermissions ?? ["ViewChannel"]),
570
+ dmPermission: (cmd.contexts && cmd.contexts.includes(discord.InteractionContextType.BotDM)) ? true : false,
571
+ integrationTypes: cmd.integrationTypes ?? [discord.ApplicationIntegrationType.GuildInstall],
572
+ contexts: cmd.contexts ?? []
573
+ };
574
+ }
575
+ /**Returns `true` when the 2 slash command options are the same. */
576
+ compareOption(optA, optB) {
577
+ if (optA.name != optB.name)
578
+ return false;
579
+ if (optA.description != optB.description)
580
+ return false;
581
+ if (optA.type != optB.type)
582
+ return false;
583
+ if (optA.required != optB.required)
584
+ return false;
585
+ if (optA.autocomplete != optB.autocomplete)
586
+ return false;
587
+ if (optA.minValue != optB.minValue)
588
+ return false;
589
+ if (optA.maxValue != optB.maxValue)
590
+ return false;
591
+ if (optA.minLength != optB.minLength)
592
+ return false;
593
+ if (optA.maxLength != optB.maxLength)
594
+ return false;
595
+ //nameLocalizations
596
+ if (optA.nameLocalizations.length != optB.nameLocalizations.length)
597
+ return false;
598
+ if (!optA.nameLocalizations.every((nameA) => {
599
+ const nameB = optB.nameLocalizations.find((nameB) => nameB.language == nameA.language);
600
+ if (!nameB || nameA.value != nameB.value)
601
+ return false;
602
+ else
603
+ return true;
604
+ }))
605
+ return false;
606
+ //descriptionLocalizations
607
+ if (optA.descriptionLocalizations.length != optB.descriptionLocalizations.length)
608
+ return false;
609
+ if (!optA.descriptionLocalizations.every((descA) => {
610
+ const descB = optB.descriptionLocalizations.find((descB) => descB.language == descA.language);
611
+ if (!descB || descA.value != descB.value)
612
+ return false;
613
+ else
614
+ return true;
615
+ }))
616
+ return false;
617
+ //choices
618
+ if (optA.choices.length != optB.choices.length)
619
+ return false;
620
+ if (!optA.choices.every((choiceA, index) => {
621
+ const choiceB = optB.choices[index];
622
+ if (choiceA.name != choiceB.name)
623
+ return false;
624
+ if (choiceA.value != choiceB.value)
625
+ return false;
626
+ //nameLocalizations
627
+ if (choiceA.nameLocalizations.length != choiceB.nameLocalizations.length)
628
+ return false;
629
+ if (!choiceA.nameLocalizations.every((nameA) => {
630
+ const nameB = choiceB.nameLocalizations.find((nameB) => nameB.language == nameA.language);
631
+ if (!nameB || nameA.value != nameB.value)
632
+ return false;
633
+ else
634
+ return true;
635
+ }))
636
+ return false;
637
+ return true;
638
+ }))
639
+ return false;
640
+ //channelTypes
641
+ if (optA.channelTypes.length != optB.channelTypes.length)
642
+ return false;
643
+ if (!optA.channelTypes.every((typeA) => {
644
+ return optB.channelTypes.includes(typeA);
645
+ }))
646
+ return false;
647
+ //options
648
+ if (optA.options.length != optB.options.length)
649
+ return false;
650
+ if (!optA.options.every((subOptA, index) => {
651
+ return this.compareOption(subOptA, optB.options[index]);
652
+ }))
653
+ return false;
654
+ return true;
655
+ }
656
+ /**Returns `true` when the 2 slash commands are the same. */
657
+ compare(cmdA, cmdB) {
658
+ if (cmdA.name != cmdB.name)
659
+ return false;
660
+ if (cmdA.description != cmdB.description)
661
+ return false;
662
+ if (cmdA.type != cmdB.type)
663
+ return false;
664
+ if (cmdA.nsfw != cmdB.nsfw)
665
+ return false;
666
+ if (cmdA.guildId != cmdB.guildId)
667
+ return false;
668
+ if (cmdA.dmPermission != cmdB.dmPermission)
669
+ return false;
670
+ if (cmdA.defaultMemberPermissions != cmdB.defaultMemberPermissions)
671
+ return false;
672
+ //nameLocalizations
673
+ if (cmdA.nameLocalizations.length != cmdB.nameLocalizations.length)
674
+ return false;
675
+ if (!cmdA.nameLocalizations.every((nameA) => {
676
+ const nameB = cmdB.nameLocalizations.find((nameB) => nameB.language == nameA.language);
677
+ if (!nameB || nameA.value != nameB.value)
678
+ return false;
679
+ else
680
+ return true;
681
+ }))
682
+ return false;
683
+ //descriptionLocalizations
684
+ if (cmdA.descriptionLocalizations.length != cmdB.descriptionLocalizations.length)
685
+ return false;
686
+ if (!cmdA.descriptionLocalizations.every((descA) => {
687
+ const descB = cmdB.descriptionLocalizations.find((descB) => descB.language == descA.language);
688
+ if (!descB || descA.value != descB.value)
689
+ return false;
690
+ else
691
+ return true;
692
+ }))
693
+ return false;
694
+ //contexts
695
+ if (cmdA.contexts.length != cmdB.contexts.length)
696
+ return false;
697
+ if (!cmdA.contexts.every((contextA) => {
698
+ return cmdB.contexts.includes(contextA);
699
+ }))
700
+ return false;
701
+ //integrationTypes
702
+ if (cmdA.integrationTypes.length != cmdB.integrationTypes.length)
703
+ return false;
704
+ if (!cmdA.integrationTypes.every((integrationA) => {
705
+ return cmdB.integrationTypes.includes(integrationA);
706
+ }))
707
+ return false;
708
+ //options
709
+ if (cmdA.options.length != cmdB.options.length)
710
+ return false;
711
+ if (!cmdA.options.every((optA, index) => {
712
+ return this.compareOption(optA, cmdB.options[index]);
713
+ }))
714
+ return false;
715
+ return true;
716
+ }
717
+ }
718
+ exports.ODSlashCommandComparator = ODSlashCommandComparator;
719
+ /**## ODSlashCommandManager `class`
720
+ * This is an Open Discord client slash manager.
721
+ *
722
+ * It's responsible for managing all the slash commands from the client.
723
+ *
724
+ * Here, you can add & remove slash commands & the bot will do the (de)registering.
725
+ */
726
+ class ODSlashCommandManager extends base_1.ODManager {
727
+ /**Alias to Open Discord debugger. */
728
+ #debug;
729
+ /**Refrerence to discord.js client. */
730
+ manager;
731
+ /**Discord.js application commands manager. */
732
+ commandManager;
733
+ /**Collection of all interaction listeners. */
734
+ #interactionListeners = [];
735
+ /**Set the soft limit for maximum amount of listeners. A warning will be shown when there are more listeners than this limit. */
736
+ listenerLimit = 100;
737
+ /**A utility class used to compare 2 slash commands with each other. */
738
+ comparator = new ODSlashCommandComparator();
739
+ constructor(debug, manager) {
740
+ super(debug, "slash command");
741
+ this.#debug = debug;
742
+ this.manager = manager;
743
+ this.commandManager = (manager.client.application) ? manager.client.application.commands : null;
744
+ }
745
+ /**Get all registered & unregistered slash commands. */
746
+ async getAllRegisteredCommands(guildId) {
747
+ if (!this.commandManager)
748
+ throw new base_1.ODSystemError("Couldn't get client application to register slash commands!");
749
+ const cmds = (await this.commandManager.fetch({ guildId })).toJSON();
750
+ const registered = [];
751
+ const unregistered = [];
752
+ const unused = [];
753
+ await this.loopAll((instance) => {
754
+ if (guildId && instance.guildId != guildId)
755
+ return;
756
+ const index = cmds.findIndex((cmd) => cmd.name == instance.name);
757
+ const cmd = cmds[index];
758
+ cmds.splice(index, 1);
759
+ if (cmd) {
760
+ //command is registered (and may need to be updated)
761
+ const universalBuilder = this.comparator.convertBuilder(instance.builder, instance.guildId);
762
+ const universalCmd = this.comparator.convertCommand(cmd);
763
+ //command is not of the type 'chatinput'
764
+ if (!universalBuilder || !universalCmd)
765
+ return;
766
+ const didChange = !this.comparator.compare(universalBuilder, universalCmd);
767
+ const requiresUpdate = didChange || (instance.requiresUpdate ? instance.requiresUpdate(universalCmd) : false);
768
+ registered.push({ instance, cmd: universalCmd, requiresUpdate });
769
+ //command is not registered
770
+ }
771
+ else
772
+ unregistered.push({ instance, cmd: null, requiresUpdate: true });
773
+ });
774
+ cmds.forEach((cmd) => {
775
+ //command does not exist in the manager (only append to unused when type == 'chatinput')
776
+ const universalCmd = this.comparator.convertCommand(cmd);
777
+ if (!universalCmd)
778
+ return;
779
+ unused.push({ instance: null, cmd: universalCmd, requiresUpdate: false });
780
+ });
781
+ return { registered, unregistered, unused };
782
+ }
783
+ /**Create all commands that are not registered yet.*/
784
+ async createNewCommands(instances, progress) {
785
+ if (!this.manager.ready)
786
+ throw new base_1.ODSystemError("Client isn't ready yet! Unable to register slash commands!");
787
+ if (instances.length > 0 && progress) {
788
+ progress.max = instances.length;
789
+ progress.start();
790
+ }
791
+ for (const instance of instances) {
792
+ await this.createCmd(instance);
793
+ this.#debug.debug("Created new slash command", [
794
+ { key: "id", value: instance.id.value },
795
+ { key: "name", value: instance.name }
796
+ ]);
797
+ if (progress)
798
+ progress.increase(1);
799
+ }
800
+ }
801
+ /**Update all commands that are already registered. */
802
+ async updateExistingCommands(instances, progress) {
803
+ if (!this.manager.ready)
804
+ throw new base_1.ODSystemError("Client isn't ready yet! Unable to register slash commands!");
805
+ if (instances.length > 0 && progress) {
806
+ progress.max = instances.length;
807
+ progress.start();
808
+ }
809
+ for (const instance of instances) {
810
+ await this.createCmd(instance);
811
+ this.#debug.debug("Updated existing slash command", [{ key: "id", value: instance.id.value }, { key: "name", value: instance.name }]);
812
+ if (progress)
813
+ progress.increase(1);
814
+ }
815
+ }
816
+ /**Remove all commands that are registered but unused by Open Discord. */
817
+ async removeUnusedCommands(instances, guildId, progress) {
818
+ if (!this.manager.ready)
819
+ throw new base_1.ODSystemError("Client isn't ready yet! Unable to register slash commands!");
820
+ if (!this.commandManager)
821
+ throw new base_1.ODSystemError("Couldn't get client application to register slash commands!");
822
+ if (instances.length > 0 && progress) {
823
+ progress.max = instances.length;
824
+ progress.start();
825
+ }
826
+ const cmds = await this.commandManager.fetch({ guildId });
827
+ for (const instance of instances) {
828
+ const cmd = cmds.find((cmd) => cmd.name == instance.name);
829
+ if (cmd) {
830
+ try {
831
+ await cmd.delete();
832
+ this.#debug.debug("Removed existing slash command", [{ key: "name", value: cmd.name }, { key: "guildId", value: guildId ?? "/" }]);
833
+ }
834
+ catch (err) {
835
+ process.emit("uncaughtException", err);
836
+ throw new base_1.ODSystemError("Failed to delete slash command '/" + cmd.name + "'!");
837
+ }
838
+ }
839
+ if (progress)
840
+ progress.increase(1);
841
+ }
842
+ }
843
+ /**Create a slash command. **(SYSTEM ONLY)** => Use `ODSlashCommandManager` for registering commands the default way! */
844
+ async createCmd(cmd) {
845
+ if (!this.commandManager)
846
+ throw new base_1.ODSystemError("Couldn't get client application to register slash commands!");
847
+ try {
848
+ await this.commandManager.create(cmd.builder, (cmd.guildId ?? undefined));
849
+ }
850
+ catch (err) {
851
+ process.emit("uncaughtException", err);
852
+ throw new base_1.ODSystemError("Failed to register slash command '/" + cmd.name + "'!");
853
+ }
854
+ }
855
+ /**Start listening to the discord.js client `interactionCreate` event. */
856
+ startListeningToInteractions() {
857
+ this.manager.client.on("interactionCreate", (interaction) => {
858
+ //return when not in main server or DM
859
+ if (!this.manager.mainServer || (interaction.guild && interaction.guild.id != this.manager.mainServer.id))
860
+ return;
861
+ if (!interaction.isChatInputCommand())
862
+ return;
863
+ const cmd = this.getFiltered((cmd) => cmd.name == interaction.commandName)[0];
864
+ if (!cmd)
865
+ return;
866
+ this.#interactionListeners.forEach((listener) => {
867
+ if (typeof listener.name == "string" && (interaction.commandName != listener.name))
868
+ return;
869
+ else if (listener.name instanceof RegExp && !listener.name.test(interaction.commandName))
870
+ return;
871
+ //this is a valid listener
872
+ listener.callback(interaction, cmd);
873
+ });
874
+ });
875
+ }
876
+ /**Callback on interaction from one or multiple slash commands. */
877
+ onInteraction(commandName, callback) {
878
+ this.#interactionListeners.push({
879
+ name: commandName,
880
+ callback
881
+ });
882
+ if (this.#interactionListeners.length > this.listenerLimit) {
883
+ this.#debug.console.log(new console_1.ODConsoleWarningMessage("Possible slash command interaction memory leak detected!", [
884
+ { key: "listeners", value: this.#interactionListeners.length.toString() }
885
+ ]));
886
+ }
887
+ }
888
+ }
889
+ exports.ODSlashCommandManager = ODSlashCommandManager;
890
+ /**## ODSlashCommand `class`
891
+ * This is an Open Discord slash command.
892
+ *
893
+ * When registered, you can listen for this command using the `ODCommandResponder`. The advantages of using this class for creating a slash command are:
894
+ * - automatic option parsing (even for channels, users, roles & mentions)!
895
+ * - automatic registration in discord.js
896
+ * - error reporting to the user when the bot fails to respond
897
+ * - plugins can extend this command
898
+ * - the bot won't re-register the command when it already exists (except when requested)!
899
+ *
900
+ * And more!
901
+ */
902
+ class ODSlashCommand extends base_1.ODManagerData {
903
+ /**The discord.js builder for this slash command. */
904
+ builder;
905
+ /**The id of the guild this command is for. Null when not set. */
906
+ guildId;
907
+ /**Function to check if the slash command requires to be updated (when it already exists). */
908
+ requiresUpdate = null;
909
+ constructor(id, builder, requiresUpdate, guildId) {
910
+ super(id);
911
+ if (builder.type != discord.ApplicationCommandType.ChatInput)
912
+ throw new base_1.ODSystemError("ApplicationCommandData is required to be the 'ChatInput' type!");
913
+ this.builder = builder;
914
+ this.guildId = guildId ?? null;
915
+ this.requiresUpdate = requiresUpdate ?? null;
916
+ }
917
+ /**The name of this slash command. */
918
+ get name() {
919
+ return this.builder.name;
920
+ }
921
+ set name(name) {
922
+ this.builder.name = name;
923
+ }
924
+ }
925
+ exports.ODSlashCommand = ODSlashCommand;
926
+ /**## ODTextCommand `class`
927
+ * This is an Open Discord text command.
928
+ *
929
+ * When registered, you can listen for this command using the `ODCommandResponder`. The advantages of using this class for creating a text command are:
930
+ * - automatic option parsing (even for channels, users, roles & mentions)!
931
+ * - automatic errors on invalid parameters
932
+ * - error reporting to the user when the bot fails to respond
933
+ * - plugins can extend this command
934
+ *
935
+ * And more!
936
+ */
937
+ class ODTextCommand extends base_1.ODManagerData {
938
+ /**The builder for this slash command. */
939
+ builder;
940
+ /**The name of this slash command. */
941
+ name;
942
+ constructor(id, builder) {
943
+ super(id);
944
+ this.builder = builder;
945
+ this.name = builder.name;
946
+ }
947
+ }
948
+ exports.ODTextCommand = ODTextCommand;
949
+ /**## ODTextCommandManager `class`
950
+ * This is an Open Discord client text manager.
951
+ *
952
+ * It's responsible for managing all the text commands from the client.
953
+ *
954
+ * Here, you can add & remove text commands & the bot will do the (de)registering.
955
+ */
956
+ class ODTextCommandManager extends base_1.ODManager {
957
+ /**Alias to Open Discord debugger. */
958
+ #debug;
959
+ /**Copy of discord.js client. */
960
+ manager;
961
+ /**Collection of all interaction listeners. */
962
+ #interactionListeners = [];
963
+ /**Collection of all error listeners. */
964
+ #errorListeners = [];
965
+ /**Set the soft limit for maximum amount of listeners. A warning will be shown when there are more listeners than this limit. */
966
+ listenerLimit = 100;
967
+ constructor(debug, manager) {
968
+ super(debug, "text command");
969
+ this.#debug = debug;
970
+ this.manager = manager;
971
+ }
972
+ /*Check if a message is a registered command. */
973
+ async #checkMessage(msg) {
974
+ if (this.manager.client.user && msg.author.id == this.manager.client.user.id)
975
+ return false;
976
+ //filter commands for correct prefix
977
+ const validPrefixCommands = [];
978
+ await this.loopAll((cmd) => {
979
+ if (msg.content.startsWith(cmd.builder.prefix))
980
+ validPrefixCommands.push({
981
+ cmd: cmd,
982
+ newContent: msg.content.substring(cmd.builder.prefix.length)
983
+ });
984
+ });
985
+ //return when no command with prefix
986
+ if (validPrefixCommands.length == 0) {
987
+ this.#errorListeners.forEach((cb) => cb({
988
+ type: "unknown_prefix",
989
+ msg: msg
990
+ }));
991
+ return false;
992
+ }
993
+ //filter commands for correct name
994
+ const validNameCommands = [];
995
+ validPrefixCommands.forEach((cmd) => {
996
+ if (cmd.newContent.startsWith(cmd.cmd.builder.name + " ") || cmd.newContent == cmd.cmd.builder.name)
997
+ validNameCommands.push({
998
+ cmd: cmd.cmd,
999
+ newContent: cmd.newContent.substring(cmd.cmd.builder.name.length + 1) //+1 because of space after command name
1000
+ });
1001
+ });
1002
+ //return when no command with name
1003
+ if (validNameCommands.length == 0) {
1004
+ this.#errorListeners.forEach((cb) => cb({
1005
+ type: "unknown_command",
1006
+ msg: msg
1007
+ }));
1008
+ return false;
1009
+ }
1010
+ //the final command
1011
+ const command = validNameCommands[0];
1012
+ const builder = command.cmd.builder;
1013
+ //check additional options
1014
+ if (typeof builder.allowBots != "undefined" && !builder.allowBots && msg.author.bot)
1015
+ return false;
1016
+ else if (typeof builder.dmPermission != "undefined" && !builder.dmPermission && msg.channel.type == discord.ChannelType.DM)
1017
+ return false;
1018
+ else if (typeof builder.guildPermission != "undefined" && !builder.guildPermission && msg.guild)
1019
+ return false;
1020
+ else if (typeof builder.allowedGuildIds != "undefined" && msg.guild && !builder.allowedGuildIds.includes(msg.guild.id))
1021
+ return false;
1022
+ //check all command options & return when incorrect
1023
+ const options = await this.#checkOptions(command.cmd, command.newContent, msg);
1024
+ if (!options.valid)
1025
+ return false;
1026
+ //a command matched this message => emit event
1027
+ this.#interactionListeners.forEach((listener) => {
1028
+ if (typeof listener.prefix == "string" && (command.cmd.builder.prefix != listener.prefix))
1029
+ return;
1030
+ if (typeof listener.name == "string" && (command.cmd.name.split(" ")[0] != listener.name))
1031
+ return;
1032
+ else if (listener.name instanceof RegExp && !listener.name.test(command.cmd.name.split(" ")[0]))
1033
+ return;
1034
+ //this is a valid listener
1035
+ listener.callback(msg, command.cmd, options.data);
1036
+ });
1037
+ return true;
1038
+ }
1039
+ /**Check if all options of a command are correct. */
1040
+ async #checkOptions(cmd, newContent, msg) {
1041
+ const options = cmd.builder.options;
1042
+ if (!options)
1043
+ return { valid: true, data: [] };
1044
+ let tempContent = newContent;
1045
+ let optionInvalid = false;
1046
+ const optionData = [];
1047
+ const optionError = (type, option, location, value, reason) => {
1048
+ //ERROR INVALID
1049
+ if (type == "invalid_option" && value && reason) {
1050
+ this.#errorListeners.forEach((cb) => cb({
1051
+ type: "invalid_option",
1052
+ msg: msg,
1053
+ prefix: cmd.builder.prefix,
1054
+ command: cmd,
1055
+ name: cmd.builder.name,
1056
+ option,
1057
+ location,
1058
+ value,
1059
+ reason
1060
+ }));
1061
+ }
1062
+ else if (type == "missing_option") {
1063
+ this.#errorListeners.forEach((cb) => cb({
1064
+ type: "missing_option",
1065
+ msg: msg,
1066
+ prefix: cmd.builder.prefix,
1067
+ command: cmd,
1068
+ name: cmd.builder.name,
1069
+ option,
1070
+ location
1071
+ }));
1072
+ }
1073
+ optionInvalid = true;
1074
+ };
1075
+ for (let location = 0; location < options.length; location++) {
1076
+ const option = options[location];
1077
+ if (optionInvalid)
1078
+ break;
1079
+ //CHECK BOOLEAN
1080
+ if (option.type == "boolean") {
1081
+ const falseValue = option.falseValue ?? "false";
1082
+ const trueValue = option.trueValue ?? "true";
1083
+ if (tempContent.startsWith(falseValue + " ")) {
1084
+ //FALSE VALUE
1085
+ optionData.push({
1086
+ name: option.name,
1087
+ type: "boolean",
1088
+ value: false
1089
+ });
1090
+ tempContent = tempContent.substring(falseValue.length + 1);
1091
+ }
1092
+ else if (tempContent.startsWith(trueValue + " ")) {
1093
+ //TRUE VALUE
1094
+ optionData.push({
1095
+ name: option.name,
1096
+ type: "boolean",
1097
+ value: true
1098
+ });
1099
+ tempContent = tempContent.substring(trueValue.length + 1);
1100
+ }
1101
+ else if (option.required) {
1102
+ //REQUIRED => ERROR IF NOT EXISTING
1103
+ const invalidregex = /^[^ ]+/;
1104
+ const invalidRes = invalidregex.exec(tempContent);
1105
+ if (invalidRes)
1106
+ optionError("invalid_option", option, location, invalidRes[0], "boolean");
1107
+ else
1108
+ optionError("missing_option", option, location);
1109
+ }
1110
+ //CHECK NUMBER
1111
+ }
1112
+ else if (option.type == "number") {
1113
+ const numRegex = /^[0-9\.\,]+/;
1114
+ const res = numRegex.exec(tempContent);
1115
+ if (res) {
1116
+ const value = res[0].replace(/\,/g, ".");
1117
+ tempContent = tempContent.substring(value.length + 1);
1118
+ const numValue = Number(value);
1119
+ if (isNaN(numValue)) {
1120
+ optionError("invalid_option", option, location, value, "number_invalid");
1121
+ }
1122
+ else if (typeof option.allowDecimal == "boolean" && !option.allowDecimal && (numValue % 1) !== 0) {
1123
+ optionError("invalid_option", option, location, value, "number_decimal");
1124
+ }
1125
+ else if (typeof option.allowNegative == "boolean" && !option.allowNegative && numValue < 0) {
1126
+ optionError("invalid_option", option, location, value, "number_negative");
1127
+ }
1128
+ else if (typeof option.allowPositive == "boolean" && !option.allowPositive && numValue > 0) {
1129
+ optionError("invalid_option", option, location, value, "number_positive");
1130
+ }
1131
+ else if (typeof option.allowZero == "boolean" && !option.allowZero && numValue == 0) {
1132
+ optionError("invalid_option", option, location, value, "number_zero");
1133
+ }
1134
+ else if (typeof option.max == "number" && numValue > option.max) {
1135
+ optionError("invalid_option", option, location, value, "number_max");
1136
+ }
1137
+ else if (typeof option.min == "number" && numValue < option.min) {
1138
+ optionError("invalid_option", option, location, value, "number_min");
1139
+ }
1140
+ else {
1141
+ //VALID NUMBER
1142
+ optionData.push({
1143
+ name: option.name,
1144
+ type: "number",
1145
+ value: numValue
1146
+ });
1147
+ }
1148
+ }
1149
+ else if (option.required) {
1150
+ //REQUIRED => ERROR IF NOT EXISTING
1151
+ const invalidRegex = /^[^ ]+/;
1152
+ const invalidRes = invalidRegex.exec(tempContent);
1153
+ if (invalidRes)
1154
+ optionError("invalid_option", option, location, invalidRes[0], "number_invalid");
1155
+ else
1156
+ optionError("missing_option", option, location);
1157
+ }
1158
+ //CHECK STRING
1159
+ }
1160
+ else if (option.type == "string") {
1161
+ if (option.allowSpaces) {
1162
+ //STRING WITH SPACES
1163
+ const value = tempContent;
1164
+ tempContent = "";
1165
+ if (typeof option.minLength == "number" && value.length < option.minLength) {
1166
+ optionError("invalid_option", option, location, value, "string_min_length");
1167
+ }
1168
+ else if (typeof option.maxLength == "number" && value.length > option.maxLength) {
1169
+ optionError("invalid_option", option, location, value, "string_max_length");
1170
+ }
1171
+ else if (option.regex && !option.regex.test(value)) {
1172
+ optionError("invalid_option", option, location, value, "string_regex");
1173
+ }
1174
+ else if (option.choices && !option.choices.includes(value)) {
1175
+ optionError("invalid_option", option, location, value, "string_choice");
1176
+ }
1177
+ else if (option.required && value === "") {
1178
+ //REQUIRED => ERROR IF NOT EXISTING
1179
+ optionError("missing_option", option, location);
1180
+ }
1181
+ else {
1182
+ //VALID STRING
1183
+ optionData.push({
1184
+ name: option.name,
1185
+ type: "string",
1186
+ value
1187
+ });
1188
+ }
1189
+ }
1190
+ else {
1191
+ //STRING WITHOUT SPACES
1192
+ const stringRegex = /^[^ ]+/;
1193
+ const res = stringRegex.exec(tempContent);
1194
+ if (res) {
1195
+ const value = res[0];
1196
+ tempContent = tempContent.substring(value.length + 1);
1197
+ if (typeof option.minLength == "number" && value.length < option.minLength) {
1198
+ optionError("invalid_option", option, location, value, "string_min_length");
1199
+ }
1200
+ else if (typeof option.maxLength == "number" && value.length > option.maxLength) {
1201
+ optionError("invalid_option", option, location, value, "string_max_length");
1202
+ }
1203
+ else if (option.regex && !option.regex.test(value)) {
1204
+ optionError("invalid_option", option, location, value, "string_regex");
1205
+ }
1206
+ else if (option.choices && !option.choices.includes(value)) {
1207
+ optionError("invalid_option", option, location, value, "string_choice");
1208
+ }
1209
+ else {
1210
+ //VALID STRING
1211
+ optionData.push({
1212
+ name: option.name,
1213
+ type: "string",
1214
+ value
1215
+ });
1216
+ }
1217
+ }
1218
+ else if (option.required) {
1219
+ //REQUIRED => ERROR IF NOT EXISTING
1220
+ optionError("missing_option", option, location);
1221
+ }
1222
+ }
1223
+ //CHECK CHANNEL
1224
+ }
1225
+ else if (option.type == "channel") {
1226
+ const channelRegex = /^(?:<#)?([0-9]+)>?/;
1227
+ const res = channelRegex.exec(tempContent);
1228
+ if (res) {
1229
+ const value = res[0];
1230
+ tempContent = tempContent.substring(value.length + 1);
1231
+ const channelId = res[1];
1232
+ if (!msg.guild) {
1233
+ optionError("invalid_option", option, location, value, "not_in_guild");
1234
+ }
1235
+ else {
1236
+ try {
1237
+ const channel = await msg.guild.channels.fetch(channelId);
1238
+ if (!channel) {
1239
+ optionError("invalid_option", option, location, value, "channel_not_found");
1240
+ }
1241
+ else if (option.channelTypes && !option.channelTypes.includes(channel.type)) {
1242
+ optionError("invalid_option", option, location, value, "channel_type");
1243
+ }
1244
+ else {
1245
+ //VALID CHANNEL
1246
+ optionData.push({
1247
+ name: option.name,
1248
+ type: "channel",
1249
+ value: channel
1250
+ });
1251
+ }
1252
+ }
1253
+ catch {
1254
+ optionError("invalid_option", option, location, value, "channel_not_found");
1255
+ }
1256
+ }
1257
+ }
1258
+ else if (option.required) {
1259
+ //REQUIRED => ERROR IF NOT EXISTING
1260
+ const invalidRegex = /^[^ ]+/;
1261
+ const invalidRes = invalidRegex.exec(tempContent);
1262
+ if (invalidRes)
1263
+ optionError("invalid_option", option, location, invalidRes[0], "channel_not_found");
1264
+ else
1265
+ optionError("missing_option", option, location);
1266
+ }
1267
+ //CHECK ROLE
1268
+ }
1269
+ else if (option.type == "role") {
1270
+ const roleRegex = /^(?:<@&)?([0-9]+)>?/;
1271
+ const res = roleRegex.exec(tempContent);
1272
+ if (res) {
1273
+ const value = res[0];
1274
+ tempContent = tempContent.substring(value.length + 1);
1275
+ const roleId = res[1];
1276
+ if (!msg.guild) {
1277
+ optionError("invalid_option", option, location, value, "not_in_guild");
1278
+ }
1279
+ else {
1280
+ try {
1281
+ const role = await msg.guild.roles.fetch(roleId);
1282
+ if (!role) {
1283
+ optionError("invalid_option", option, location, value, "role_not_found");
1284
+ }
1285
+ else {
1286
+ //VALID ROLE
1287
+ optionData.push({
1288
+ name: option.name,
1289
+ type: "role",
1290
+ value: role
1291
+ });
1292
+ }
1293
+ }
1294
+ catch {
1295
+ optionError("invalid_option", option, location, value, "role_not_found");
1296
+ }
1297
+ }
1298
+ }
1299
+ else if (option.required) {
1300
+ //REQUIRED => ERROR IF NOT EXISTING
1301
+ const invalidRegex = /^[^ ]+/;
1302
+ const invalidRes = invalidRegex.exec(tempContent);
1303
+ if (invalidRes)
1304
+ optionError("invalid_option", option, location, invalidRes[0], "role_not_found");
1305
+ else
1306
+ optionError("missing_option", option, location);
1307
+ }
1308
+ //CHECK GUILD MEMBER
1309
+ }
1310
+ else if (option.type == "guildmember") {
1311
+ const memberRegex = /^(?:<@)?([0-9]+)>?/;
1312
+ const res = memberRegex.exec(tempContent);
1313
+ if (res) {
1314
+ const value = res[0];
1315
+ tempContent = tempContent.substring(value.length + 1);
1316
+ const memberId = res[1];
1317
+ if (!msg.guild) {
1318
+ optionError("invalid_option", option, location, value, "not_in_guild");
1319
+ }
1320
+ else {
1321
+ try {
1322
+ const member = await msg.guild.members.fetch(memberId);
1323
+ if (!member) {
1324
+ optionError("invalid_option", option, location, value, "member_not_found");
1325
+ }
1326
+ else {
1327
+ //VALID GUILD MEMBER
1328
+ optionData.push({
1329
+ name: option.name,
1330
+ type: "guildmember",
1331
+ value: member
1332
+ });
1333
+ }
1334
+ }
1335
+ catch {
1336
+ optionError("invalid_option", option, location, value, "member_not_found");
1337
+ }
1338
+ }
1339
+ }
1340
+ else if (option.required) {
1341
+ //REQUIRED => ERROR IF NOT EXISTING
1342
+ const invalidRegex = /^[^ ]+/;
1343
+ const invalidRes = invalidRegex.exec(tempContent);
1344
+ if (invalidRes)
1345
+ optionError("invalid_option", option, location, invalidRes[0], "member_not_found");
1346
+ else
1347
+ optionError("missing_option", option, location);
1348
+ }
1349
+ //CHECK USER
1350
+ }
1351
+ else if (option.type == "user") {
1352
+ const userRegex = /^(?:<@)?([0-9]+)>?/;
1353
+ const res = userRegex.exec(tempContent);
1354
+ if (res) {
1355
+ const value = res[0];
1356
+ tempContent = tempContent.substring(value.length + 1);
1357
+ const userId = res[1];
1358
+ try {
1359
+ const user = await this.manager.client.users.fetch(userId);
1360
+ if (!user) {
1361
+ optionError("invalid_option", option, location, value, "user_not_found");
1362
+ }
1363
+ else {
1364
+ //VALID USER
1365
+ optionData.push({
1366
+ name: option.name,
1367
+ type: "user",
1368
+ value: user
1369
+ });
1370
+ }
1371
+ }
1372
+ catch {
1373
+ optionError("invalid_option", option, location, value, "user_not_found");
1374
+ }
1375
+ }
1376
+ else if (option.required) {
1377
+ //REQUIRED => ERROR IF NOT EXISTING
1378
+ const invalidRegex = /^[^ ]+/;
1379
+ const invalidRes = invalidRegex.exec(tempContent);
1380
+ if (invalidRes)
1381
+ optionError("invalid_option", option, location, invalidRes[0], "user_not_found");
1382
+ else
1383
+ optionError("missing_option", option, location);
1384
+ }
1385
+ //CHECK MENTIONABLE
1386
+ }
1387
+ else if (option.type == "mentionable") {
1388
+ const mentionableRegex = /^<(@&?)([0-9]+)>/;
1389
+ const res = mentionableRegex.exec(tempContent);
1390
+ if (res) {
1391
+ const value = res[0];
1392
+ const type = (res[1] == "@&") ? "role" : "user";
1393
+ tempContent = tempContent.substring(value.length + 1);
1394
+ const mentionableId = res[2];
1395
+ if (!msg.guild) {
1396
+ optionError("invalid_option", option, location, value, "not_in_guild");
1397
+ }
1398
+ else if (type == "role") {
1399
+ try {
1400
+ const role = await msg.guild.roles.fetch(mentionableId);
1401
+ if (!role) {
1402
+ optionError("invalid_option", option, location, value, "mentionable_not_found");
1403
+ }
1404
+ else {
1405
+ //VALID ROLE
1406
+ optionData.push({
1407
+ name: option.name,
1408
+ type: "mentionable",
1409
+ value: role
1410
+ });
1411
+ }
1412
+ }
1413
+ catch {
1414
+ optionError("invalid_option", option, location, value, "mentionable_not_found");
1415
+ }
1416
+ }
1417
+ else if (type == "user") {
1418
+ try {
1419
+ const user = await this.manager.client.users.fetch(mentionableId);
1420
+ if (!user) {
1421
+ optionError("invalid_option", option, location, value, "mentionable_not_found");
1422
+ }
1423
+ else {
1424
+ //VALID USER
1425
+ optionData.push({
1426
+ name: option.name,
1427
+ type: "mentionable",
1428
+ value: user
1429
+ });
1430
+ }
1431
+ }
1432
+ catch {
1433
+ optionError("invalid_option", option, location, value, "mentionable_not_found");
1434
+ }
1435
+ }
1436
+ }
1437
+ else if (option.required) {
1438
+ //REQUIRED => ERROR IF NOT EXISTING
1439
+ const invalidRegex = /^[^ ]+/;
1440
+ const invalidRes = invalidRegex.exec(tempContent);
1441
+ if (invalidRes)
1442
+ optionError("invalid_option", option, location, invalidRes[0], "mentionable_not_found");
1443
+ else
1444
+ optionError("missing_option", option, location);
1445
+ }
1446
+ }
1447
+ }
1448
+ return { valid: !optionInvalid, data: optionData };
1449
+ }
1450
+ /**Start listening to the discord.js client `messageCreate` event. */
1451
+ startListeningToInteractions() {
1452
+ this.manager.client.on("messageCreate", (msg) => {
1453
+ //return when not in main server or DM
1454
+ if (!this.manager.mainServer || (msg.guild && msg.guild.id != this.manager.mainServer.id))
1455
+ return;
1456
+ this.#checkMessage(msg);
1457
+ });
1458
+ }
1459
+ /**Check if optional values are only present at the end of the command. */
1460
+ #checkBuilderOptions(builder) {
1461
+ let optionalVisited = false;
1462
+ let valid = true;
1463
+ let reason = null;
1464
+ if (!builder.options)
1465
+ return { valid: true, reason: null };
1466
+ builder.options.forEach((opt, index, list) => {
1467
+ if (!opt.required)
1468
+ optionalVisited = true;
1469
+ if (optionalVisited && opt.required) {
1470
+ valid = false;
1471
+ reason = "required_after_optional";
1472
+ }
1473
+ if (opt.type == "string" && opt.allowSpaces && ((index + 1) != list.length)) {
1474
+ valid = false;
1475
+ reason = "allowspaces_not_last";
1476
+ }
1477
+ });
1478
+ return { valid, reason };
1479
+ }
1480
+ /**Callback on interaction from one of the registered text commands */
1481
+ onInteraction(commandPrefix, commandName, callback) {
1482
+ this.#interactionListeners.push({
1483
+ prefix: commandPrefix,
1484
+ name: commandName,
1485
+ callback
1486
+ });
1487
+ if (this.#interactionListeners.length > this.listenerLimit) {
1488
+ this.#debug.console.log(new console_1.ODConsoleWarningMessage("Possible text command interaction memory leak detected!", [
1489
+ { key: "listeners", value: this.#interactionListeners.length.toString() }
1490
+ ]));
1491
+ }
1492
+ }
1493
+ /**Callback on error from all the registered text commands */
1494
+ onError(callback) {
1495
+ this.#errorListeners.push(callback);
1496
+ }
1497
+ add(data, overwrite) {
1498
+ const checkResult = this.#checkBuilderOptions(data.builder);
1499
+ if (!checkResult.valid && checkResult.reason == "required_after_optional")
1500
+ throw new base_1.ODSystemError("Invalid text command '" + data.id.value + "' => optional options are only allowed at the end of a command!");
1501
+ else if (!checkResult.valid && checkResult.reason == "allowspaces_not_last")
1502
+ throw new base_1.ODSystemError("Invalid text command '" + data.id.value + "' => string option with 'allowSpaces' is only allowed at the end of a command!");
1503
+ else
1504
+ return super.add(data, overwrite);
1505
+ }
1506
+ }
1507
+ exports.ODTextCommandManager = ODTextCommandManager;
1508
+ /**## ODContextMenuComparator `class`
1509
+ * A utility class to compare existing context menu's with newly registered ones.
1510
+ */
1511
+ class ODContextMenuComparator {
1512
+ /**Convert a `ODContextMenuBuilder` to a universal Open Discord context menu object for comparison. */
1513
+ convertBuilder(builder, guildId) {
1514
+ if (builder.type != discord.ApplicationCommandType.Message && builder.type != discord.ApplicationCommandType.User)
1515
+ return null;
1516
+ const nameLoc = builder.nameLocalizations ?? {};
1517
+ return {
1518
+ type: builder.type,
1519
+ name: builder.name,
1520
+ nameLocalizations: Object.keys(nameLoc).map((key) => { return { language: key, value: nameLoc[key] }; }),
1521
+ guildId: guildId,
1522
+ nsfw: builder.nsfw ?? false,
1523
+ defaultMemberPermissions: discord.PermissionsBitField.resolve(builder.defaultMemberPermissions ?? ["ViewChannel"]),
1524
+ dmPermission: (builder.contexts && builder.contexts.includes(discord.InteractionContextType.BotDM)) ?? false,
1525
+ integrationTypes: builder.integrationTypes ?? [discord.ApplicationIntegrationType.GuildInstall],
1526
+ contexts: builder.contexts ?? []
1527
+ };
1528
+ }
1529
+ /**Convert a `discord.ApplicationCommand` to a universal Open Discord context menu object for comparison. */
1530
+ convertMenu(cmd) {
1531
+ if (cmd.type != discord.ApplicationCommandType.Message && cmd.type != discord.ApplicationCommandType.User)
1532
+ return null;
1533
+ const nameLoc = cmd.nameLocalizations ?? {};
1534
+ return {
1535
+ type: cmd.type,
1536
+ name: cmd.name,
1537
+ nameLocalizations: Object.keys(nameLoc).map((key) => { return { language: key, value: nameLoc[key] }; }),
1538
+ guildId: cmd.guildId,
1539
+ nsfw: cmd.nsfw,
1540
+ defaultMemberPermissions: discord.PermissionsBitField.resolve(cmd.defaultMemberPermissions ?? ["ViewChannel"]),
1541
+ dmPermission: (cmd.contexts && cmd.contexts.includes(discord.InteractionContextType.BotDM)) ? true : false,
1542
+ integrationTypes: cmd.integrationTypes ?? [discord.ApplicationIntegrationType.GuildInstall],
1543
+ contexts: cmd.contexts ?? []
1544
+ };
1545
+ }
1546
+ /**Returns `true` when the 2 context menus are the same. */
1547
+ compare(ctxA, ctxB) {
1548
+ if (ctxA.name != ctxB.name)
1549
+ return false;
1550
+ if (ctxA.type != ctxB.type)
1551
+ return false;
1552
+ if (ctxA.nsfw != ctxB.nsfw)
1553
+ return false;
1554
+ if (ctxA.guildId != ctxB.guildId)
1555
+ return false;
1556
+ if (ctxA.dmPermission != ctxB.dmPermission)
1557
+ return false;
1558
+ if (ctxA.defaultMemberPermissions != ctxB.defaultMemberPermissions)
1559
+ return false;
1560
+ //nameLocalizations
1561
+ if (ctxA.nameLocalizations.length != ctxB.nameLocalizations.length)
1562
+ return false;
1563
+ if (!ctxA.nameLocalizations.every((nameA) => {
1564
+ const nameB = ctxB.nameLocalizations.find((nameB) => nameB.language == nameA.language);
1565
+ if (!nameB || nameA.value != nameB.value)
1566
+ return false;
1567
+ else
1568
+ return true;
1569
+ }))
1570
+ return false;
1571
+ //contexts
1572
+ if (ctxA.contexts.length != ctxB.contexts.length)
1573
+ return false;
1574
+ if (!ctxA.contexts.every((contextA) => {
1575
+ return ctxB.contexts.includes(contextA);
1576
+ }))
1577
+ return false;
1578
+ //integrationTypes
1579
+ if (ctxA.integrationTypes.length != ctxB.integrationTypes.length)
1580
+ return false;
1581
+ if (!ctxA.integrationTypes.every((integrationA) => {
1582
+ return ctxB.integrationTypes.includes(integrationA);
1583
+ }))
1584
+ return false;
1585
+ return true;
1586
+ }
1587
+ }
1588
+ exports.ODContextMenuComparator = ODContextMenuComparator;
1589
+ /**## ODContextMenuManager `class`
1590
+ * This is an Open Discord client context menu manager.
1591
+ *
1592
+ * It's responsible for managing all the context interactions from the client.
1593
+ *
1594
+ * Here, you can add & remove context interactions & the bot will do the (de)registering.
1595
+ */
1596
+ class ODContextMenuManager extends base_1.ODManager {
1597
+ /**Alias to Open Discord debugger. */
1598
+ #debug;
1599
+ /**Refrerence to discord.js client. */
1600
+ manager;
1601
+ /**Discord.js application commands manager. */
1602
+ commandManager;
1603
+ /**Collection of all interaction listeners. */
1604
+ #interactionListeners = [];
1605
+ /**Set the soft limit for maximum amount of listeners. A warning will be shown when there are more listeners than this limit. */
1606
+ listenerLimit = 100;
1607
+ /**A utility class used to compare 2 context menus with each other. */
1608
+ comparator = new ODContextMenuComparator();
1609
+ constructor(debug, manager) {
1610
+ super(debug, "context menu");
1611
+ this.#debug = debug;
1612
+ this.manager = manager;
1613
+ this.commandManager = (manager.client.application) ? manager.client.application.commands : null;
1614
+ }
1615
+ /**Get all registered & unregistered message context menu commands. */
1616
+ async getAllRegisteredMenus(guildId) {
1617
+ if (!this.commandManager)
1618
+ throw new base_1.ODSystemError("Couldn't get client application to register message context menus!");
1619
+ const menus = (await this.commandManager.fetch({ guildId })).toJSON();
1620
+ const registered = [];
1621
+ const unregistered = [];
1622
+ const unused = [];
1623
+ await this.loopAll((instance) => {
1624
+ if (guildId && instance.guildId != guildId)
1625
+ return;
1626
+ const index = menus.findIndex((menu) => menu.name == instance.name);
1627
+ const menu = menus[index];
1628
+ menus.splice(index, 1);
1629
+ if (menu) {
1630
+ //menu is registered (and may need to be updated)
1631
+ const universalBuilder = this.comparator.convertBuilder(instance.builder, instance.guildId);
1632
+ const universalMenu = this.comparator.convertMenu(menu);
1633
+ //menu is not of the type 'message'|'user'
1634
+ if (!universalBuilder || !universalMenu)
1635
+ return;
1636
+ const didChange = !this.comparator.compare(universalBuilder, universalMenu);
1637
+ const requiresUpdate = didChange || (instance.requiresUpdate ? instance.requiresUpdate(universalMenu) : false);
1638
+ registered.push({ instance, menu: universalMenu, requiresUpdate });
1639
+ //menu is not registered
1640
+ }
1641
+ else
1642
+ unregistered.push({ instance, menu: null, requiresUpdate: true });
1643
+ });
1644
+ menus.forEach((menu) => {
1645
+ //menu does not exist in the manager (only append to unused when type == 'message'|'user')
1646
+ const universalCmd = this.comparator.convertMenu(menu);
1647
+ if (!universalCmd)
1648
+ return;
1649
+ unused.push({ instance: null, menu: universalCmd, requiresUpdate: false });
1650
+ });
1651
+ return { registered, unregistered, unused };
1652
+ }
1653
+ /**Create all context menus that are not registered yet.*/
1654
+ async createNewMenus(instances, progress) {
1655
+ if (!this.manager.ready)
1656
+ throw new base_1.ODSystemError("Client isn't ready yet! Unable to register context menus!");
1657
+ if (instances.length > 0 && progress) {
1658
+ progress.max = instances.length;
1659
+ progress.start();
1660
+ }
1661
+ for (const instance of instances) {
1662
+ await this.createMenu(instance);
1663
+ this.#debug.debug("Created new context menu", [
1664
+ { key: "id", value: instance.id.value },
1665
+ { key: "name", value: instance.name },
1666
+ { key: "type", value: (instance.builder.type == discord.ApplicationCommandType.Message) ? "message-context" : "user-context" }
1667
+ ]);
1668
+ if (progress)
1669
+ progress.increase(1);
1670
+ }
1671
+ }
1672
+ /**Update all context menus that are already registered. */
1673
+ async updateExistingMenus(instances, progress) {
1674
+ if (!this.manager.ready)
1675
+ throw new base_1.ODSystemError("Client isn't ready yet! Unable to register context menus!");
1676
+ if (instances.length > 0 && progress) {
1677
+ progress.max = instances.length;
1678
+ progress.start();
1679
+ }
1680
+ for (const instance of instances) {
1681
+ await this.createMenu(instance);
1682
+ this.#debug.debug("Updated existing context menu", [
1683
+ { key: "id", value: instance.id.value },
1684
+ { key: "name", value: instance.name },
1685
+ { key: "type", value: (instance.builder.type == discord.ApplicationCommandType.Message) ? "message-context" : "user-context" }
1686
+ ]);
1687
+ if (progress)
1688
+ progress.increase(1);
1689
+ }
1690
+ }
1691
+ /**Remove all context menus that are registered but unused by Open Discord. */
1692
+ async removeUnusedMenus(instances, guildId, progress) {
1693
+ if (!this.manager.ready)
1694
+ throw new base_1.ODSystemError("Client isn't ready yet! Unable to register context menus!");
1695
+ if (!this.commandManager)
1696
+ throw new base_1.ODSystemError("Couldn't get client application to register context menus!");
1697
+ if (instances.length > 0 && progress) {
1698
+ progress.max = instances.length;
1699
+ progress.start();
1700
+ }
1701
+ const menus = await this.commandManager.fetch({ guildId });
1702
+ for (const instance of instances) {
1703
+ const menu = menus.find((menu) => menu.name == instance.name);
1704
+ if (menu) {
1705
+ try {
1706
+ await menu.delete();
1707
+ this.#debug.debug("Removed existing context menu", [
1708
+ { key: "name", value: menu.name },
1709
+ { key: "guildId", value: guildId ?? "/" },
1710
+ { key: "type", value: (instance.type == discord.ApplicationCommandType.Message) ? "message-context" : "user-context" }
1711
+ ]);
1712
+ }
1713
+ catch (err) {
1714
+ process.emit("uncaughtException", err);
1715
+ throw new base_1.ODSystemError("Failed to delete context menu '" + menu.name + "'!");
1716
+ }
1717
+ }
1718
+ if (progress)
1719
+ progress.increase(1);
1720
+ }
1721
+ }
1722
+ /**Create a context menu. **(SYSTEM ONLY)** => Use `ODContextMenuManager` for registering context menu's the default way! */
1723
+ async createMenu(menu) {
1724
+ if (!this.commandManager)
1725
+ throw new base_1.ODSystemError("Couldn't get client application to register context menu's!");
1726
+ try {
1727
+ await this.commandManager.create(menu.builder, (menu.guildId ?? undefined));
1728
+ }
1729
+ catch (err) {
1730
+ process.emit("uncaughtException", err);
1731
+ throw new base_1.ODSystemError("Failed to register context menu '" + menu.name + "'!");
1732
+ }
1733
+ }
1734
+ /**Start listening to the discord.js client `interactionCreate` event. */
1735
+ startListeningToInteractions() {
1736
+ this.manager.client.on("interactionCreate", (interaction) => {
1737
+ //return when not in main server or DM
1738
+ if (!this.manager.mainServer || (interaction.guild && interaction.guild.id != this.manager.mainServer.id))
1739
+ return;
1740
+ if (!interaction.isContextMenuCommand())
1741
+ return;
1742
+ const menu = this.getFiltered((menu) => menu.name == interaction.commandName)[0];
1743
+ if (!menu)
1744
+ return;
1745
+ this.#interactionListeners.forEach((listener) => {
1746
+ if (typeof listener.name == "string" && (interaction.commandName != listener.name))
1747
+ return;
1748
+ else if (listener.name instanceof RegExp && !listener.name.test(interaction.commandName))
1749
+ return;
1750
+ //this is a valid listener
1751
+ listener.callback(interaction, menu);
1752
+ });
1753
+ });
1754
+ }
1755
+ /**Callback on interaction from one or multiple context menu's. */
1756
+ onInteraction(menuName, callback) {
1757
+ this.#interactionListeners.push({
1758
+ name: menuName,
1759
+ callback
1760
+ });
1761
+ if (this.#interactionListeners.length > this.listenerLimit) {
1762
+ this.#debug.console.log("Possible context menu interaction memory leak detected!", "warning", [
1763
+ { key: "listeners", value: this.#interactionListeners.length.toString() }
1764
+ ]);
1765
+ }
1766
+ }
1767
+ }
1768
+ exports.ODContextMenuManager = ODContextMenuManager;
1769
+ /**## ODContextMenu `class`
1770
+ * This is an Open Discord context menu.
1771
+ *
1772
+ * When registered, you can listen for this context menu using the `ODContextResponder`. The advantages of using this class for creating a context menu are:
1773
+ * - automatic registration in discord.js
1774
+ * - error reporting to the user when the bot fails to respond
1775
+ * - plugins can extend this context menu
1776
+ * - the bot won't re-register the context menu when it already exists (except when requested)!
1777
+ *
1778
+ * And more!
1779
+ */
1780
+ class ODContextMenu extends base_1.ODManagerData {
1781
+ /**The discord.js builder for this context menu. */
1782
+ builder;
1783
+ /**The id of the guild this context menu is for. `null` when not set. */
1784
+ guildId;
1785
+ /**Function to check if the context menu requires to be updated (when it already exists). */
1786
+ requiresUpdate = null;
1787
+ constructor(id, builder, requiresUpdate, guildId) {
1788
+ super(id);
1789
+ if (builder.type != discord.ApplicationCommandType.Message && builder.type != discord.ApplicationCommandType.User)
1790
+ throw new base_1.ODSystemError("ApplicationCommandData is required to be the 'Message'|'User' type!");
1791
+ this.builder = builder;
1792
+ this.guildId = guildId ?? null;
1793
+ this.requiresUpdate = requiresUpdate ?? null;
1794
+ }
1795
+ /**The name of this context menu. */
1796
+ get name() {
1797
+ return this.builder.name;
1798
+ }
1799
+ set name(name) {
1800
+ this.builder.name = name;
1801
+ }
1802
+ }
1803
+ exports.ODContextMenu = ODContextMenu;
1804
+ /**## ODAutocompleteManager `class`
1805
+ * This is an Open Discord client autocomplete interaction manager.
1806
+ *
1807
+ * It's responsible for managing all the autocomplete interactions from the client.
1808
+ */
1809
+ class ODAutocompleteManager {
1810
+ /**Alias to Open Discord debugger. */
1811
+ #debug;
1812
+ /**Refrerence to discord.js client. */
1813
+ manager;
1814
+ /**Discord.js application commands manager. */
1815
+ commandManager;
1816
+ /**Collection of all interaction listeners. */
1817
+ #interactionListeners = [];
1818
+ /**Set the soft limit for maximum amount of listeners. A warning will be shown when there are more listeners than this limit. */
1819
+ listenerLimit = 100;
1820
+ constructor(debug, manager) {
1821
+ this.#debug = debug;
1822
+ this.manager = manager;
1823
+ this.commandManager = (manager.client.application) ? manager.client.application.commands : null;
1824
+ }
1825
+ /**Start listening to the discord.js client `interactionCreate` event. */
1826
+ startListeningToInteractions() {
1827
+ this.manager.client.on("interactionCreate", (interaction) => {
1828
+ //return when not in main server or DM
1829
+ if (!this.manager.mainServer || (interaction.guild && interaction.guild.id != this.manager.mainServer.id))
1830
+ return;
1831
+ if (!interaction.isAutocomplete())
1832
+ return;
1833
+ this.#interactionListeners.forEach((listener) => {
1834
+ if (typeof listener.cmdName == "string" && (interaction.commandName != listener.cmdName))
1835
+ return;
1836
+ else if (listener.cmdName instanceof RegExp && !listener.cmdName.test(interaction.commandName))
1837
+ return;
1838
+ if (typeof listener.optName == "string" && (interaction.options.getFocused(true).name != listener.optName))
1839
+ return;
1840
+ else if (listener.optName instanceof RegExp && !listener.optName.test(interaction.options.getFocused(true).name))
1841
+ return;
1842
+ //this is a valid listener
1843
+ listener.callback(interaction);
1844
+ });
1845
+ });
1846
+ }
1847
+ /**Callback on interaction from one or multiple autocompletes. */
1848
+ onInteraction(cmdName, optName, callback) {
1849
+ this.#interactionListeners.push({
1850
+ cmdName, optName, callback
1851
+ });
1852
+ if (this.#interactionListeners.length > this.listenerLimit) {
1853
+ this.#debug.console.log("Possible autocomplete interaction memory leak detected!", "warning", [
1854
+ { key: "listeners", value: this.#interactionListeners.length.toString() }
1855
+ ]);
1856
+ }
1857
+ }
1858
+ }
1859
+ exports.ODAutocompleteManager = ODAutocompleteManager;