@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,2247 @@
1
+ ///////////////////////////////////////
2
+ //DISCORD CLIENT MODULE
3
+ ///////////////////////////////////////
4
+ import { ODId, ODManager, ODManagerData, ODSystemError, ODValidId } from "./base"
5
+ import * as discord from "discord.js"
6
+ import {REST} from "@discordjs/rest"
7
+ import { ODConsoleWarningMessage, ODDebugger } from "./console"
8
+ import { ODMessageBuildResult, ODMessageBuildSentResult } from "./builder"
9
+ import { ODManualProgressBar } from "./progressbar"
10
+
11
+ /**## ODClientIntents `type`
12
+ * A list of intents required when inviting the bot.
13
+ */
14
+ export type ODClientIntents = ("Guilds"|"GuildMembers"|"GuildModeration"|"GuildEmojisAndStickers"|"GuildIntegrations"|"GuildWebhooks"|"GuildInvites"|"GuildVoiceStates"|"GuildPresences"|"GuildMessages"|"GuildMessageReactions"|"GuildMessageTyping"|"DirectMessages"|"DirectMessageReactions"|"DirectMessageTyping"|"MessageContent"|"GuildScheduledEvents"|"AutoModerationConfiguration"|"AutoModerationExecution")
15
+ /**## ODClientPriviligedIntents `type`
16
+ * A list of priviliged intents required to be enabled in the developer portal.
17
+ */
18
+ export type ODClientPriviligedIntents = ("GuildMembers"|"MessageContent"|"Presence")
19
+ /**## ODClientPartials `type`
20
+ * A list of partials required for the bot to work. (`Message` & `Channel` are for receiving DM messages from uncached channels)
21
+ */
22
+ export type ODClientPartials = ("User"|"Channel"|"GuildMember"|"Message"|"Reaction"|"GuildScheduledEvent"|"ThreadMember")
23
+ /**## ODClientPermissions `type`
24
+ * A list of permissions required in the server that the bot is active in.
25
+ */
26
+ export type ODClientPermissions = ("CreateInstantInvite"|"KickMembers"|"BanMembers"|"Administrator"|"ManageChannels"|"ManageGuild"|"AddReactions"|"ViewAuditLog"|"PrioritySpeaker"|"Stream"|"ViewChannel"|"SendMessages"|"SendTTSMessages"|"ManageMessages"|"EmbedLinks"|"AttachFiles"|"ReadMessageHistory"|"MentionEveryone"|"UseExternalEmojis"|"ViewGuildInsights"|"Connect"|"Speak"|"MuteMembers"|"DeafenMembers"|"MoveMembers"|"UseVAD"|"ChangeNickname"|"ManageNicknames"|"ManageRoles"|"ManageWebhooks"|"ManageGuildExpressions"|"UseApplicationCommands"|"RequestToSpeak"|"ManageEvents"|"ManageThreads"|"CreatePublicThreads"|"CreatePrivateThreads"|"UseExternalStickers"|"SendMessagesInThreads"|"UseEmbeddedActivities"|"ModerateMembers"|"ViewCreatorMonetizationAnalytics"|"UseSoundboard"|"UseExternalSounds"|"SendVoiceMessages")
27
+
28
+ /**## ODClientManager `class`
29
+ * This is an Open Discord client manager.
30
+ *
31
+ * It is responsible for managing the discord.js client. Here, you can set the status, register slash commands and much more!
32
+ *
33
+ * If you want, you can also listen for custom events on the `ODClientManager.client` variable (`discord.Client`)
34
+ */
35
+ export class ODClientManager {
36
+ /**Alias to Open Discord debugger. */
37
+ #debug: ODDebugger
38
+
39
+ /**List of required bot intents. Add intents to this list using the `onClientLoad` event. */
40
+ intents: ODClientIntents[] = []
41
+ /**List of required bot privileged intents. Add intents to this list using the `onClientLoad` event. */
42
+ privileges: ODClientPriviligedIntents[] = []
43
+ /**List of required bot partials. Add intents to this list using the `onClientLoad` event. **❌ Only use when neccessery!** */
44
+ partials: ODClientPartials[] = []
45
+ /**List of required bot permissions. Add permissions to this list using the `onClientLoad` event. */
46
+ permissions: ODClientPermissions[] = []
47
+ /**The discord bot token, empty by default. */
48
+ set token(value:string){
49
+ this.#token = value
50
+ this.rest.setToken(value)
51
+ }
52
+ get token(){
53
+ return this.#token
54
+ }
55
+ /**The discord bot token. **DON'T USE THIS!!!** (use `ODClientManager.token` instead) */
56
+ #token: string = ""
57
+
58
+ /**The discord.js `discord.Client`. Only use it when initiated! */
59
+ client: discord.Client<true> = new discord.Client({intents:[]}) //temporary client
60
+ /**The discord.js REST client. Used for stuff that discord.js can't handle :) */
61
+ rest: discord.REST = new REST({version:"10"})
62
+ /**Is the bot initiated? */
63
+ initiated: boolean = false
64
+ /**Is the bot logged in? */
65
+ loggedIn: boolean = false
66
+ /**Is the bot ready? */
67
+ ready: boolean = false
68
+
69
+ /**The main server of the bot. Provided by serverId in the config */
70
+ mainServer: discord.Guild|null = null
71
+ /**(❌ DO NOT OVERWRITE ❌) Internal Open Discord function to continue the startup when the client is ready! */
72
+ readyListener: (() => Promise<void>)|null = null
73
+ /**The status manager is responsible for setting the bot status. */
74
+ activity: ODClientActivityManager
75
+ /**The slash command manager is responsible for all slash commands & their events inside the bot. */
76
+ slashCommands: ODSlashCommandManager
77
+ /**The text command manager is responsible for all text commands & their events inside the bot. */
78
+ textCommands: ODTextCommandManager
79
+ /**The context menu manager is responsible for all context menus & their events inside the bot. */
80
+ contextMenus: ODContextMenuManager
81
+ /**The autocomplete manager is responsible for all autocomplete events inside the bot. */
82
+ autocompletes: ODAutocompleteManager
83
+
84
+ constructor(debug:ODDebugger){
85
+ this.#debug = debug
86
+ this.activity = new ODClientActivityManager(this.#debug,this)
87
+ this.slashCommands = new ODSlashCommandManager(this.#debug,this)
88
+ this.textCommands = new ODTextCommandManager(this.#debug,this)
89
+ this.contextMenus = new ODContextMenuManager(this.#debug,this)
90
+ this.autocompletes = new ODAutocompleteManager(this.#debug,this)
91
+ }
92
+
93
+ /**Initiate the `client` variable & add the intents & partials to the bot. */
94
+ initClient(){
95
+ if (!this.intents.every((value) => typeof discord.GatewayIntentBits[value] != "undefined")) throw new ODSystemError("Client has non-existing intents!")
96
+ if (!this.privileges.every((value) => typeof {GuildMembers:true,MessageContent:true,Presence:true}[value] != "undefined")) throw new ODSystemError("Client has non-existing privileged intents!")
97
+ if (!this.partials.every((value) => typeof discord.Partials[value] != "undefined")) throw new ODSystemError("Client has non-existing partials!")
98
+ if (!this.permissions.every((value) => typeof discord.PermissionFlagsBits[value] != "undefined")) throw new ODSystemError("Client has non-existing partials!")
99
+
100
+ const intents = this.intents.map((value) => discord.GatewayIntentBits[value])
101
+ const partials = this.partials.map((value) => discord.Partials[value])
102
+
103
+ const oldClient = this.client
104
+ this.client = new discord.Client({intents,partials})
105
+
106
+ //@ts-ignore
107
+ oldClient.eventNames().forEach((event:keyof discord.ClientEvents) => {
108
+ //@ts-ignore
109
+ const callbacks = oldClient.rawListeners(event)
110
+ callbacks.forEach((cb:() => void) => {
111
+ this.client.on(event,cb)
112
+ })
113
+ })
114
+
115
+ this.initiated = true
116
+
117
+ this.#debug.debug("Created client with intents: "+this.intents.join(", "))
118
+ this.#debug.debug("Created client with privileged intents: "+this.privileges.join(", "))
119
+ this.#debug.debug("Created client with partials: "+this.partials.join(", "))
120
+ this.#debug.debug("Created client with permissions: "+this.permissions.join(", "))
121
+ }
122
+ /**Get all servers the bot is part of. */
123
+ async getGuilds(): Promise<discord.Guild[]> {
124
+ if (!this.initiated) throw new ODSystemError("Client isn't initiated yet!")
125
+ if (!this.ready) throw new ODSystemError("Client isn't ready yet!")
126
+
127
+ return this.client.guilds.cache.map((guild) => guild)
128
+ }
129
+ /**Check if the bot is in a specific guild */
130
+ checkBotInGuild(guild:discord.Guild){
131
+ return (guild.members.me) ? true : false
132
+ }
133
+ /**Check if a specific guild has all required permissions (or `Administrator`) */
134
+ checkGuildPerms(guild:discord.Guild){
135
+ if (!guild.members.me) throw new ODSystemError("Client isn't a member in this server!")
136
+ const perms = guild.members.me.permissions
137
+ if (perms.has("Administrator")) return true
138
+ else{
139
+ return this.permissions.every((perm) => {
140
+ return perms.has(perm)
141
+ })
142
+ }
143
+ }
144
+ /**Log-in with a discord auth token. Rejects returns `false` using 'softErrors' on failure. */
145
+ login(softErrors?:boolean): Promise<boolean> {
146
+ return new Promise(async (resolve,reject) => {
147
+ if (!this.initiated) reject("Client isn't initiated yet!")
148
+ if (!this.token) reject("Client doesn't have a token!")
149
+
150
+ try {
151
+ this.client.once("clientReady",async () => {
152
+ this.ready = true
153
+
154
+ //set slashCommandManager & contextMenuManager to client applicationCommandManager
155
+ if (!this.client.application) throw new ODSystemError("Couldn't get client application for slashCommand & contextMenu managers!")
156
+ this.slashCommands.commandManager = this.client.application.commands
157
+ this.contextMenus.commandManager = this.client.application.commands
158
+ this.autocompletes.commandManager = this.client.application.commands
159
+
160
+ if (this.readyListener) await this.readyListener()
161
+ resolve(true)
162
+ })
163
+
164
+ this.#debug.debug("Actual discord.js client.login()")
165
+ await this.client.login(this.token)
166
+ this.#debug.debug("Finished discord.js client.login()")
167
+ this.loggedIn = true
168
+ }catch(err){
169
+ if (softErrors) return resolve(false)
170
+ else if (err.message.toLowerCase().includes("used disallowed intents")){
171
+ process.emit("uncaughtException",new ODSystemError("Used disallowed intents"))
172
+ }else if (err.message.toLowerCase().includes("tokeninvalid") || err.message.toLowerCase().includes("an invalid token was provided")){
173
+ process.emit("uncaughtException",new ODSystemError("Invalid discord bot token provided"))
174
+ }else reject("OD Login Error: "+err)
175
+ }
176
+ })
177
+ }
178
+ /**A simplified shortcut to get a `discord.User` :) */
179
+ async fetchUser(id:string): Promise<discord.User|null> {
180
+ if (!this.initiated) throw new ODSystemError("Client isn't initiated yet!")
181
+ if (!this.ready) throw new ODSystemError("Client isn't ready yet!")
182
+
183
+ try{
184
+ return await this.client.users.fetch(id)
185
+ }catch{
186
+ return null
187
+ }
188
+ }
189
+ /**A simplified shortcut to get a `discord.Guild` :) */
190
+ async fetchGuild(id:string): Promise<discord.Guild|null> {
191
+ if (!this.initiated) throw new ODSystemError("Client isn't initiated yet!")
192
+ if (!this.ready) throw new ODSystemError("Client isn't ready yet!")
193
+
194
+ try{
195
+ return await this.client.guilds.fetch(id)
196
+ }catch{
197
+ return null
198
+ }
199
+ }
200
+ /**A simplified shortcut to get a `discord.Channel` :) */
201
+ async fetchChannel(id:string): Promise<discord.Channel|null> {
202
+ if (!this.initiated) throw new ODSystemError("Client isn't initiated yet!")
203
+ if (!this.ready) throw new ODSystemError("Client isn't ready yet!")
204
+
205
+ try{
206
+ return await this.client.channels.fetch(id)
207
+ }catch{
208
+ return null
209
+ }
210
+ }
211
+ /**A simplified shortcut to get a `discord.GuildBasedChannel` :) */
212
+ async fetchGuildChannel(guildId:string|discord.Guild, id:string): Promise<discord.GuildBasedChannel|null> {
213
+ if (!this.initiated) throw new ODSystemError("Client isn't initiated yet!")
214
+ if (!this.ready) throw new ODSystemError("Client isn't ready yet!")
215
+
216
+ try{
217
+ const guild = (guildId instanceof discord.Guild) ? guildId : await this.fetchGuild(guildId)
218
+ if (!guild) return null
219
+ const channel = await guild.channels.fetch(id)
220
+ return channel
221
+ }catch{
222
+ return null
223
+ }
224
+ }
225
+ /**A simplified shortcut to get a `discord.TextChannel` :) */
226
+ async fetchGuildTextChannel(guildId:string|discord.Guild, id:string): Promise<discord.TextChannel|null> {
227
+ if (!this.initiated) throw new ODSystemError("Client isn't initiated yet!")
228
+ if (!this.ready) throw new ODSystemError("Client isn't ready yet!")
229
+
230
+ try{
231
+ const guild = (guildId instanceof discord.Guild) ? guildId : await this.fetchGuild(guildId)
232
+ if (!guild) return null
233
+ const channel = await guild.channels.fetch(id)
234
+ if (!channel || channel.type != discord.ChannelType.GuildText) return null
235
+ return channel
236
+ }catch{
237
+ return null
238
+ }
239
+ }
240
+ /**A simplified shortcut to get a `discord.CategoryChannel` :) */
241
+ async fetchGuildCategoryChannel(guildId:string|discord.Guild, id:string): Promise<discord.CategoryChannel|null> {
242
+ if (!this.initiated) throw new ODSystemError("Client isn't initiated yet!")
243
+ if (!this.ready) throw new ODSystemError("Client isn't ready yet!")
244
+
245
+ try{
246
+ const guild = (guildId instanceof discord.Guild) ? guildId : await this.fetchGuild(guildId)
247
+ if (!guild) return null
248
+ const channel = await guild.channels.fetch(id)
249
+ if (!channel || channel.type != discord.ChannelType.GuildCategory) return null
250
+ return channel
251
+ }catch{
252
+ return null
253
+ }
254
+ }
255
+ /**A simplified shortcut to get a `discord.GuildMember` :) */
256
+ async fetchGuildMember(guildId:string|discord.Guild, id:string): Promise<discord.GuildMember|null> {
257
+ if (!this.initiated) throw new ODSystemError("Client isn't initiated yet!")
258
+ if (!this.ready) throw new ODSystemError("Client isn't ready yet!")
259
+ if (typeof id != "string") throw new ODSystemError("TEMP ERROR => ODClientManager.fetchGuildMember() => id param isn't string")
260
+
261
+ try{
262
+ const guild = (guildId instanceof discord.Guild) ? guildId : await this.fetchGuild(guildId)
263
+ if (!guild) return null
264
+ return await guild.members.fetch(id)
265
+ }catch{
266
+ return null
267
+ }
268
+ }
269
+ /**A simplified shortcut to get a `discord.Role` :) */
270
+ async fetchGuildRole(guildId:string|discord.Guild, id:string): Promise<discord.Role|null> {
271
+ if (!this.initiated) throw new ODSystemError("Client isn't initiated yet!")
272
+ if (!this.ready) throw new ODSystemError("Client isn't ready yet!")
273
+ if (typeof id != "string") throw new ODSystemError("TEMP ERROR => ODClientManager.fetchGuildRole() => id param isn't string")
274
+
275
+ try{
276
+ const guild = (guildId instanceof discord.Guild) ? guildId : await this.fetchGuild(guildId)
277
+ if (!guild) return null
278
+ return await guild.roles.fetch(id)
279
+ }catch{
280
+ return null
281
+ }
282
+ }
283
+ /**A simplified shortcut to get a `discord.Message` :) */
284
+ async fetchGuildChannelMessage(guildId:string|discord.Guild, channelId:string|discord.TextChannel, id:string): Promise<discord.Message<true>|null>
285
+ async fetchGuildChannelMessage(channelId:discord.TextChannel, id:string): Promise<discord.Message<true>|null>
286
+ async fetchGuildChannelMessage(guildId:string|discord.Guild|discord.TextChannel, channelId:string|discord.TextChannel|string, id?:string): Promise<discord.Message<true>|null> {
287
+ if (!this.initiated) throw new ODSystemError("Client isn't initiated yet!")
288
+ if (!this.ready) throw new ODSystemError("Client isn't ready yet!")
289
+
290
+ try{
291
+ if (guildId instanceof discord.TextChannel && typeof channelId == "string"){
292
+ const channel = guildId
293
+ return await channel.messages.fetch(channelId)
294
+ }else if (!(guildId instanceof discord.TextChannel) && id){
295
+ const channel = (channelId instanceof discord.TextChannel) ? channelId : await this.fetchGuildTextChannel(guildId,channelId)
296
+ if (!channel) return null
297
+ return await channel.messages.fetch(id)
298
+ }else return null
299
+ }catch{
300
+ return null
301
+ }
302
+ }
303
+ /**A simplified shortcut to send a DM to a user :) */
304
+ async sendUserDm(user:string|discord.User, message:ODMessageBuildResult): Promise<ODMessageBuildSentResult<false>> {
305
+ if (!this.initiated) throw new ODSystemError("Client isn't initiated yet!")
306
+ if (!this.ready) throw new ODSystemError("Client isn't ready yet!")
307
+
308
+ try{
309
+ if (user instanceof discord.User){
310
+ if (user.bot) return {success:false,message:null}
311
+ const channel = await user.createDM()
312
+ const msg = await channel.send(message.message)
313
+ return {success:true,message:msg}
314
+ }else{
315
+ const newUser = await this.fetchUser(user)
316
+ if (!newUser) throw new Error()
317
+ if (newUser.bot) return {success:false,message:null}
318
+ const channel = await newUser.createDM()
319
+ const msg = await channel.send(message.message)
320
+ return {success:true,message:msg}
321
+ }
322
+ }catch{
323
+ try{
324
+ this.#debug.console.log("Failed to send DM to user! ","warning",[
325
+ {key:"id",value:(user instanceof discord.User ? user.id : user)},
326
+ {key:"message",value:message.id.value}
327
+ ])
328
+ }catch{}
329
+ return {success:false,message:null}
330
+ }
331
+ }
332
+ }
333
+
334
+ /**## ODClientActivityType `type`
335
+ * Possible activity types for the bot.
336
+ */
337
+ export type ODClientActivityType = ("playing"|"listening"|"watching"|"custom"|false)
338
+ /**## ODClientActivityMode `type`
339
+ * Possible activity statuses for the bot.
340
+ */
341
+ export type ODClientActivityMode = ("online"|"invisible"|"idle"|"dnd")
342
+
343
+
344
+ /**## ODClientActivityManager `class`
345
+ * This is an Open Discord client activity manager.
346
+ *
347
+ * It's responsible for managing the client status. Here, you can set the activity & status of the bot.
348
+ *
349
+ * It also has a built-in refresh function, so the status will refresh every 10 minutes to keep it visible.
350
+ */
351
+ export class ODClientActivityManager {
352
+ /**Alias to Open Discord debugger. */
353
+ #debug: ODDebugger
354
+
355
+ /**Copy of discord.js client */
356
+ manager: ODClientManager
357
+ /**The current status type */
358
+ type: ODClientActivityType = false
359
+ /**The current status text */
360
+ text: string = ""
361
+ /**The current status mode */
362
+ mode: ODClientActivityMode = "online"
363
+ /**Additional state text */
364
+ state: string = ""
365
+
366
+ /**The timer responsible for refreshing the status. Stop it using `clearInterval(interval)` */
367
+ interval?: NodeJS.Timeout
368
+ /**status refresh interval in seconds (5 minutes by default)*/
369
+ refreshInterval: number = 600
370
+ /**Is the status already initiated? */
371
+ initiated: boolean = false
372
+
373
+ constructor(debug:ODDebugger, manager:ODClientManager){
374
+ this.#debug = debug
375
+ this.manager = manager
376
+ }
377
+
378
+ /**Update the status. When already initiated, it can take up to 10min to see the updated status in discord. */
379
+ setStatus(type:ODClientActivityType, text:string, mode:ODClientActivityMode, state:string, forceUpdate?:boolean){
380
+ this.type = type
381
+ this.text = text
382
+ this.mode = mode
383
+ this.state = state
384
+ if (forceUpdate) this.#updateClientActivity(this.type,this.text)
385
+ }
386
+
387
+ /**When initiating the status, the bot starts updating the status using `discord.js`. Returns `true` when successfull. */
388
+ initStatus(): boolean {
389
+ if (this.initiated || !this.manager.ready) return false
390
+ this.#updateClientActivity(this.type,this.text)
391
+ this.interval = setInterval(() => {
392
+ this.#updateClientActivity(this.type,this.text)
393
+ this.#debug.debug("Client status update cycle")
394
+ },this.refreshInterval*1000)
395
+ this.initiated = true
396
+ this.#debug.debug("Client status initiated")
397
+ return true
398
+ }
399
+
400
+ /**Update the client status */
401
+ #updateClientActivity(type:ODClientActivityType,text:string){
402
+ if (!this.manager.client.user) throw new ODSystemError("Couldn't set client status: client.user == undefined")
403
+ if (type == false){
404
+ this.manager.client.user.setActivity()
405
+ return
406
+ }
407
+ this.manager.client.user.setPresence({
408
+ activities:[{
409
+ type:this.#getStatusTypeEnum(type),
410
+ state:this.state ? this.state : undefined,
411
+ name:text,
412
+ }],
413
+ status:this.mode
414
+ })
415
+ }
416
+ /**Get the enum that links to the correct type */
417
+ #getStatusTypeEnum(type:Exclude<ODClientActivityType,false>){
418
+ if (type == "playing") return discord.ActivityType.Playing
419
+ else if (type == "listening") return discord.ActivityType.Listening
420
+ else if (type == "watching") return discord.ActivityType.Watching
421
+ else if (type == "custom") return discord.ActivityType.Custom
422
+ else return discord.ActivityType.Listening
423
+ }
424
+ /**Get the status type (for displaying the status) */
425
+ getStatusType(): "listening "|"playing "|"watching "|"" {
426
+ if (this.type == "listening" || this.type == "playing" || this.type == "watching") return this.type+" " as "listening "|"playing "|"watching "|""
427
+ else return ""
428
+ }
429
+ }
430
+
431
+ /**## ODSlashCommandUniversalTranslation `interface`
432
+ * A universal template for a slash command translation. (used in names & descriptions)
433
+ *
434
+ * Why universal? Both **existing slash commands** & **unregistered templates** can be converted to this type.
435
+ */
436
+ export interface ODSlashCommandUniversalTranslation {
437
+ /**The language code or locale of this language. */
438
+ language:`${discord.Locale}`,
439
+ /**The translation of the name in this language. */
440
+ value:string
441
+ }
442
+
443
+ /**## ODSlashCommandUniversalOptionChoice `interface`
444
+ * A universal template for a slash command option choice. (used in `string` options)
445
+ *
446
+ * Why universal? Both **existing slash commands** & **unregistered templates** can be converted to this type.
447
+ */
448
+ export interface ODSlashCommandUniversalOptionChoice {
449
+ /**The name of this choice. */
450
+ name:string,
451
+ /**All localized names of this choice. */
452
+ nameLocalizations:readonly ODSlashCommandUniversalTranslation[],
453
+ /**The value of this choice. */
454
+ value:string
455
+ }
456
+
457
+ /**## ODSlashCommandUniversalOption `interface`
458
+ * A universal template for a slash command option.
459
+ *
460
+ * Why universal? Both **existing slash commands** & **unregistered templates** can be converted to this type.
461
+ */
462
+ export interface ODSlashCommandUniversalOption {
463
+ /**The type of this option. */
464
+ type:discord.ApplicationCommandOptionType,
465
+ /**The name of this option. */
466
+ name:string,
467
+ /**All localized names of this option. */
468
+ nameLocalizations:readonly ODSlashCommandUniversalTranslation[],
469
+ /**The description of this option. */
470
+ description:string,
471
+ /**All localized descriptions of this option. */
472
+ descriptionLocalizations:readonly ODSlashCommandUniversalTranslation[],
473
+ /**Is this option required? */
474
+ required:boolean,
475
+
476
+ /**Is autocomplete enabled in this option? */
477
+ autocomplete:boolean|null,
478
+ /**Choices for this option (only when type is `string`) */
479
+ choices:ODSlashCommandUniversalOptionChoice[],
480
+ /**A list of sub-options for this option (only when type is `subCommand` or `subCommandGroup`) */
481
+ options:readonly ODSlashCommandUniversalOption[],
482
+ /**A list of allowed channel types for this option (only when type is `channel`) */
483
+ channelTypes:readonly discord.ChannelType[],
484
+ /**The minimum amount required for this option (only when type is `number` or `integer`) */
485
+ minValue:number|null,
486
+ /**The maximum amount required for this option (only when type is `number` or `integer`) */
487
+ maxValue:number|null,
488
+ /**The minimum length required for this option (only when type is `string`) */
489
+ minLength:number|null,
490
+ /**The maximum length required for this option (only when type is `string`) */
491
+ maxLength:number|null
492
+ }
493
+
494
+ /**## ODSlashCommandUniversalCommand `interface`
495
+ * A universal template for a slash command.
496
+ *
497
+ * Why universal? Both **existing slash commands** & **unregistered templates** can be converted to this type.
498
+ */
499
+ export interface ODSlashCommandUniversalCommand {
500
+ /**The type of this command. (required => `ChatInput`) */
501
+ type:discord.ApplicationCommandType.ChatInput,
502
+ /**The name of this command. */
503
+ name:string,
504
+ /**All localized names of this command. */
505
+ nameLocalizations:readonly ODSlashCommandUniversalTranslation[],
506
+ /**The description of this command. */
507
+ description:string,
508
+ /**All localized descriptions of this command. */
509
+ descriptionLocalizations:readonly ODSlashCommandUniversalTranslation[],
510
+ /**The id of the guild this command is registered in. */
511
+ guildId:string|null,
512
+ /**Is this command for 18+ users only? */
513
+ nsfw:boolean,
514
+ /**A list of options for this command. */
515
+ options:readonly ODSlashCommandUniversalOption[],
516
+ /**A bitfield of the user permissions required to use this command. */
517
+ defaultMemberPermissions:bigint,
518
+ /**Is this command available in DM? */
519
+ dmPermission:boolean,
520
+ /**A list of contexts where you can install this command. */
521
+ integrationTypes:readonly discord.ApplicationIntegrationType[],
522
+ /**A list of contexts where you can use this command. */
523
+ contexts:readonly discord.InteractionContextType[]
524
+ }
525
+
526
+ /**## ODSlashCommandBuilder `interface`
527
+ * The builder for slash commands. Here you can add options to the command.
528
+ */
529
+ export interface ODSlashCommandBuilder extends discord.ChatInputApplicationCommandData {
530
+ /**This field is required in Open Discord for future compatibility. */
531
+ integrationTypes:discord.ApplicationIntegrationType[],
532
+ /**This field is required in Open Discord for future compatibility. */
533
+ contexts:discord.InteractionContextType[]
534
+ }
535
+
536
+ /**## ODSlashCommandComparator `class`
537
+ * A utility class to compare existing slash commands with newly registered ones.
538
+ */
539
+ export class ODSlashCommandComparator {
540
+ /**Convert a `discord.ApplicationCommandOptionChoiceData<string>` to a universal Open Discord slash command option choice object for comparison. */
541
+ #convertOptionChoice(choice:discord.ApplicationCommandOptionChoiceData<string>): ODSlashCommandUniversalOptionChoice {
542
+ const nameLoc = choice.nameLocalizations ?? {}
543
+ return {
544
+ name:choice.name,
545
+ nameLocalizations:Object.keys(nameLoc).map((key) => {return {language:key as `${discord.Locale}`,value:nameLoc[key]}}),
546
+ value:choice.value
547
+ }
548
+ }
549
+ /**Convert a `discord.ApplicationCommandOptionData` to a universal Open Discord slash command option object for comparison. */
550
+ #convertBuilderOption(option:discord.ApplicationCommandOptionData): ODSlashCommandUniversalOption {
551
+ const nameLoc = option.nameLocalizations ?? {}
552
+ const descLoc = option.descriptionLocalizations ?? {}
553
+ return {
554
+ type:option.type,
555
+ name:option.name,
556
+ nameLocalizations:Object.keys(nameLoc).map((key) => {return {language:key as `${discord.Locale}`,value:nameLoc[key]}}),
557
+ description:option.description,
558
+ descriptionLocalizations:Object.keys(descLoc).map((key) => {return {language:key as `${discord.Locale}`,value:descLoc[key]}}),
559
+ required:(option.type != discord.ApplicationCommandOptionType.SubcommandGroup && option.type != discord.ApplicationCommandOptionType.Subcommand && option.required) ? true : false,
560
+
561
+ autocomplete:option.autocomplete ?? false,
562
+ choices:(option.type == discord.ApplicationCommandOptionType.String && !option.autocomplete && option.choices) ? option.choices.map((choice) => this.#convertOptionChoice(choice)) : [],
563
+ options:((option.type == discord.ApplicationCommandOptionType.SubcommandGroup || option.type == discord.ApplicationCommandOptionType.Subcommand) && option.options) ? option.options.map((opt) => this.#convertBuilderOption(opt)) : [],
564
+ channelTypes:(option.type == discord.ApplicationCommandOptionType.Channel && option.channelTypes) ? option.channelTypes : [],
565
+ minValue:(option.type == discord.ApplicationCommandOptionType.Number && option.minValue) ? option.minValue : null,
566
+ maxValue:(option.type == discord.ApplicationCommandOptionType.Number && option.maxValue) ? option.maxValue : null,
567
+ minLength:(option.type == discord.ApplicationCommandOptionType.String && option.minLength) ? option.minLength : null,
568
+ maxLength:(option.type == discord.ApplicationCommandOptionType.String && option.maxLength) ? option.maxLength : null
569
+ }
570
+ }
571
+ /**Convert a `discord.ApplicationCommandOption` to a universal Open Discord slash command option object for comparison. */
572
+ #convertCommandOption(option:discord.ApplicationCommandOption): ODSlashCommandUniversalOption {
573
+ const nameLoc = option.nameLocalizations ?? {}
574
+ const descLoc = option.descriptionLocalizations ?? {}
575
+
576
+ return {
577
+ type:option.type,
578
+ name:option.name,
579
+ nameLocalizations:Object.keys(nameLoc).map((key) => {return {language:key as `${discord.Locale}`,value:nameLoc[key]}}),
580
+ description:option.description,
581
+ descriptionLocalizations:Object.keys(descLoc).map((key) => {return {language:key as `${discord.Locale}`,value:descLoc[key]}}),
582
+ required:(option.type != discord.ApplicationCommandOptionType.SubcommandGroup && option.type != discord.ApplicationCommandOptionType.Subcommand && option.required) ? true : false,
583
+
584
+ autocomplete:option.autocomplete ?? false,
585
+ choices:(option.type == discord.ApplicationCommandOptionType.String && !option.autocomplete && option.choices) ? option.choices.map((choice) => this.#convertOptionChoice(choice)) : [],
586
+ options:((option.type == discord.ApplicationCommandOptionType.SubcommandGroup || option.type == discord.ApplicationCommandOptionType.Subcommand) && option.options) ? option.options.map((opt) => this.#convertBuilderOption(opt)) : [],
587
+ channelTypes:(option.type == discord.ApplicationCommandOptionType.Channel && option.channelTypes) ? option.channelTypes : [],
588
+ minValue:(option.type == discord.ApplicationCommandOptionType.Number && option.minValue) ? option.minValue : null,
589
+ maxValue:(option.type == discord.ApplicationCommandOptionType.Number && option.maxValue) ? option.maxValue : null,
590
+ minLength:(option.type == discord.ApplicationCommandOptionType.String && option.minLength) ? option.minLength : null,
591
+ maxLength:(option.type == discord.ApplicationCommandOptionType.String && option.maxLength) ? option.maxLength : null
592
+ }
593
+ }
594
+ /**Convert a `ODSlashCommandBuilder` to a universal Open Discord slash command object for comparison. */
595
+ convertBuilder(builder:ODSlashCommandBuilder,guildId:string|null): ODSlashCommandUniversalCommand|null {
596
+ if (builder.type != discord.ApplicationCommandType.ChatInput) return null //throw new ODSystemError("ODSlashCommandComparator:convertBuilder() is not supported for other types than 'ChatInput'!")
597
+ const nameLoc = builder.nameLocalizations ?? {}
598
+ const descLoc = builder.descriptionLocalizations ?? {}
599
+ return {
600
+ type:1,
601
+ name:builder.name,
602
+ nameLocalizations:Object.keys(nameLoc).map((key) => {return {language:key as `${discord.Locale}`,value:nameLoc[key]}}),
603
+ description:builder.description,
604
+ descriptionLocalizations:Object.keys(descLoc).map((key) => {return {language:key as `${discord.Locale}`,value:descLoc[key]}}),
605
+ guildId:guildId,
606
+ nsfw:builder.nsfw ?? false,
607
+ options:builder.options ? builder.options.map((opt) => this.#convertBuilderOption(opt)) : [],
608
+ defaultMemberPermissions:discord.PermissionsBitField.resolve(builder.defaultMemberPermissions ?? ["ViewChannel"]),
609
+ dmPermission:(builder.contexts && builder.contexts.includes(discord.InteractionContextType.BotDM)) ?? false,
610
+ integrationTypes:builder.integrationTypes ?? [discord.ApplicationIntegrationType.GuildInstall],
611
+ contexts:builder.contexts ?? []
612
+ }
613
+ }
614
+ /**Convert a `discord.ApplicationCommand` to a universal Open Discord slash command object for comparison. */
615
+ convertCommand(cmd:discord.ApplicationCommand): ODSlashCommandUniversalCommand|null {
616
+ if (cmd.type != discord.ApplicationCommandType.ChatInput) return null //throw new ODSystemError("ODSlashCommandComparator:convertCommand() is not supported for other types than 'ChatInput'!")
617
+ const nameLoc = cmd.nameLocalizations ?? {}
618
+ const descLoc = cmd.descriptionLocalizations ?? {}
619
+ return {
620
+ type:1,
621
+ name:cmd.name,
622
+ nameLocalizations:Object.keys(nameLoc).map((key) => {return {language:key as `${discord.Locale}`,value:nameLoc[key]}}),
623
+ description:cmd.description,
624
+ descriptionLocalizations:Object.keys(descLoc).map((key) => {return {language:key as `${discord.Locale}`,value:descLoc[key]}}),
625
+ guildId:cmd.guildId,
626
+ nsfw:cmd.nsfw,
627
+ options:cmd.options ? cmd.options.map((opt) => this.#convertCommandOption(opt)) : [],
628
+ defaultMemberPermissions:discord.PermissionsBitField.resolve(cmd.defaultMemberPermissions ?? ["ViewChannel"]),
629
+ dmPermission:(cmd.contexts && cmd.contexts.includes(discord.InteractionContextType.BotDM)) ? true : false,
630
+ integrationTypes:cmd.integrationTypes ?? [discord.ApplicationIntegrationType.GuildInstall],
631
+ contexts:cmd.contexts ?? []
632
+ }
633
+ }
634
+ /**Returns `true` when the 2 slash command options are the same. */
635
+ compareOption(optA:ODSlashCommandUniversalOption,optB:ODSlashCommandUniversalOption): boolean {
636
+ if (optA.name != optB.name) return false
637
+ if (optA.description != optB.description) return false
638
+ if (optA.type != optB.type) return false
639
+ if (optA.required != optB.required) return false
640
+ if (optA.autocomplete != optB.autocomplete) return false
641
+ if (optA.minValue != optB.minValue) return false
642
+ if (optA.maxValue != optB.maxValue) return false
643
+ if (optA.minLength != optB.minLength) return false
644
+ if (optA.maxLength != optB.maxLength) return false
645
+
646
+ //nameLocalizations
647
+ if (optA.nameLocalizations.length != optB.nameLocalizations.length) return false
648
+ if (!optA.nameLocalizations.every((nameA) => {
649
+ const nameB = optB.nameLocalizations.find((nameB) => nameB.language == nameA.language)
650
+ if (!nameB || nameA.value != nameB.value) return false
651
+ else return true
652
+ })) return false
653
+
654
+ //descriptionLocalizations
655
+ if (optA.descriptionLocalizations.length != optB.descriptionLocalizations.length) return false
656
+ if (!optA.descriptionLocalizations.every((descA) => {
657
+ const descB = optB.descriptionLocalizations.find((descB) => descB.language == descA.language)
658
+ if (!descB || descA.value != descB.value) return false
659
+ else return true
660
+ })) return false
661
+
662
+ //choices
663
+ if (optA.choices.length != optB.choices.length) return false
664
+ if (!optA.choices.every((choiceA,index) => {
665
+ const choiceB = optB.choices[index]
666
+ if (choiceA.name != choiceB.name) return false
667
+ if (choiceA.value != choiceB.value) return false
668
+
669
+ //nameLocalizations
670
+ if (choiceA.nameLocalizations.length != choiceB.nameLocalizations.length) return false
671
+ if (!choiceA.nameLocalizations.every((nameA) => {
672
+ const nameB = choiceB.nameLocalizations.find((nameB) => nameB.language == nameA.language)
673
+ if (!nameB || nameA.value != nameB.value) return false
674
+ else return true
675
+ })) return false
676
+
677
+ return true
678
+ })) return false
679
+
680
+ //channelTypes
681
+ if (optA.channelTypes.length != optB.channelTypes.length) return false
682
+ if (!optA.channelTypes.every((typeA) => {
683
+ return optB.channelTypes.includes(typeA)
684
+ })) return false
685
+
686
+ //options
687
+ if (optA.options.length != optB.options.length) return false
688
+ if (!optA.options.every((subOptA,index) => {
689
+ return this.compareOption(subOptA,optB.options[index])
690
+ })) return false
691
+
692
+ return true
693
+ }
694
+ /**Returns `true` when the 2 slash commands are the same. */
695
+ compare(cmdA:ODSlashCommandUniversalCommand,cmdB:ODSlashCommandUniversalCommand): boolean {
696
+ if (cmdA.name != cmdB.name) return false
697
+ if (cmdA.description != cmdB.description) return false
698
+ if (cmdA.type != cmdB.type) return false
699
+ if (cmdA.nsfw != cmdB.nsfw) return false
700
+ if (cmdA.guildId != cmdB.guildId) return false
701
+ if (cmdA.dmPermission != cmdB.dmPermission) return false
702
+ if (cmdA.defaultMemberPermissions != cmdB.defaultMemberPermissions) return false
703
+
704
+ //nameLocalizations
705
+ if (cmdA.nameLocalizations.length != cmdB.nameLocalizations.length) return false
706
+ if (!cmdA.nameLocalizations.every((nameA) => {
707
+ const nameB = cmdB.nameLocalizations.find((nameB) => nameB.language == nameA.language)
708
+ if (!nameB || nameA.value != nameB.value) return false
709
+ else return true
710
+ })) return false
711
+
712
+ //descriptionLocalizations
713
+ if (cmdA.descriptionLocalizations.length != cmdB.descriptionLocalizations.length) return false
714
+ if (!cmdA.descriptionLocalizations.every((descA) => {
715
+ const descB = cmdB.descriptionLocalizations.find((descB) => descB.language == descA.language)
716
+ if (!descB || descA.value != descB.value) return false
717
+ else return true
718
+ })) return false
719
+
720
+ //contexts
721
+ if (cmdA.contexts.length != cmdB.contexts.length) return false
722
+ if (!cmdA.contexts.every((contextA) => {
723
+ return cmdB.contexts.includes(contextA)
724
+ })) return false
725
+
726
+ //integrationTypes
727
+ if (cmdA.integrationTypes.length != cmdB.integrationTypes.length) return false
728
+ if (!cmdA.integrationTypes.every((integrationA) => {
729
+ return cmdB.integrationTypes.includes(integrationA)
730
+ })) return false
731
+
732
+ //options
733
+ if (cmdA.options.length != cmdB.options.length) return false
734
+ if (!cmdA.options.every((optA,index) => {
735
+ return this.compareOption(optA,cmdB.options[index])
736
+ })) return false
737
+
738
+ return true
739
+ }
740
+ }
741
+
742
+ /**## ODSlashCommandInteractionCallback `type`
743
+ * Callback for the slash command interaction listener.
744
+ */
745
+ export type ODSlashCommandInteractionCallback = (interaction:discord.ChatInputCommandInteraction,cmd:ODSlashCommand) => void
746
+
747
+ /**## ODSlashCommandRegisteredResult `type`
748
+ * The result which will be returned when getting all (un)registered slash commands from the manager.
749
+ */
750
+ export type ODSlashCommandRegisteredResult = {
751
+ /**A list of all registered commands. */
752
+ registered:{
753
+ /**The instance (`ODSlashCommand`) from this command. */
754
+ instance:ODSlashCommand,
755
+ /**The (universal) slash command object/template of this command. */
756
+ cmd:ODSlashCommandUniversalCommand,
757
+ /**Does this command require an update? */
758
+ requiresUpdate:boolean
759
+ }[],
760
+ /**A list of all unregistered commands. */
761
+ unregistered:{
762
+ /**The instance (`ODSlashCommand`) from this command. */
763
+ instance:ODSlashCommand,
764
+ /**The (universal) slash command object/template of this command. */
765
+ cmd:null,
766
+ /**Does this command require an update? */
767
+ requiresUpdate:true
768
+ }[],
769
+ /**A list of all unused commands (not found in `ODSlashCommandManager`). */
770
+ unused:{
771
+ /**The instance (`ODSlashCommand`) from this command. */
772
+ instance:null,
773
+ /**The (universal) slash command object/template of this command. */
774
+ cmd:ODSlashCommandUniversalCommand,
775
+ /**Does this command require an update? */
776
+ requiresUpdate:false
777
+ }[]
778
+ }
779
+
780
+ /**## ODSlashCommandManager `class`
781
+ * This is an Open Discord client slash manager.
782
+ *
783
+ * It's responsible for managing all the slash commands from the client.
784
+ *
785
+ * Here, you can add & remove slash commands & the bot will do the (de)registering.
786
+ */
787
+ export class ODSlashCommandManager extends ODManager<ODSlashCommand> {
788
+ /**Alias to Open Discord debugger. */
789
+ #debug: ODDebugger
790
+
791
+ /**Refrerence to discord.js client. */
792
+ manager: ODClientManager
793
+ /**Discord.js application commands manager. */
794
+ commandManager: discord.ApplicationCommandManager|null
795
+ /**Collection of all interaction listeners. */
796
+ #interactionListeners: {name:string|RegExp, callback:ODSlashCommandInteractionCallback}[] = []
797
+ /**Set the soft limit for maximum amount of listeners. A warning will be shown when there are more listeners than this limit. */
798
+ listenerLimit: number = 100
799
+ /**A utility class used to compare 2 slash commands with each other. */
800
+ comparator: ODSlashCommandComparator = new ODSlashCommandComparator()
801
+
802
+ constructor(debug:ODDebugger, manager:ODClientManager){
803
+ super(debug,"slash command")
804
+ this.#debug = debug
805
+ this.manager = manager
806
+ this.commandManager = (manager.client.application) ? manager.client.application.commands : null
807
+ }
808
+
809
+ /**Get all registered & unregistered slash commands. */
810
+ async getAllRegisteredCommands(guildId?:string): Promise<ODSlashCommandRegisteredResult> {
811
+ if (!this.commandManager) throw new ODSystemError("Couldn't get client application to register slash commands!")
812
+
813
+ const cmds = (await this.commandManager.fetch({guildId})).toJSON()
814
+ const registered: {instance:ODSlashCommand, cmd:ODSlashCommandUniversalCommand, requiresUpdate:boolean}[] = []
815
+ const unregistered: {instance:ODSlashCommand, cmd:null, requiresUpdate:true}[] = []
816
+ const unused: {instance:null, cmd:ODSlashCommandUniversalCommand, requiresUpdate:false}[] = []
817
+
818
+ await this.loopAll((instance) => {
819
+ if (guildId && instance.guildId != guildId) return
820
+
821
+ const index = cmds.findIndex((cmd) => cmd.name == instance.name)
822
+ const cmd = cmds[index]
823
+ cmds.splice(index,1)
824
+ if (cmd){
825
+ //command is registered (and may need to be updated)
826
+ const universalBuilder = this.comparator.convertBuilder(instance.builder,instance.guildId)
827
+ const universalCmd = this.comparator.convertCommand(cmd)
828
+
829
+ //command is not of the type 'chatinput'
830
+ if (!universalBuilder || !universalCmd) return
831
+
832
+ const didChange = !this.comparator.compare(universalBuilder,universalCmd)
833
+ const requiresUpdate = didChange || (instance.requiresUpdate ? instance.requiresUpdate(universalCmd) : false)
834
+ registered.push({instance,cmd:universalCmd,requiresUpdate})
835
+
836
+ //command is not registered
837
+ }else unregistered.push({instance,cmd:null,requiresUpdate:true})
838
+ })
839
+
840
+ cmds.forEach((cmd) => {
841
+ //command does not exist in the manager (only append to unused when type == 'chatinput')
842
+ const universalCmd = this.comparator.convertCommand(cmd)
843
+ if (!universalCmd) return
844
+ unused.push({instance:null,cmd:universalCmd,requiresUpdate:false})
845
+ })
846
+
847
+ return {registered,unregistered,unused}
848
+ }
849
+ /**Create all commands that are not registered yet.*/
850
+ async createNewCommands(instances:ODSlashCommand[],progress?:ODManualProgressBar){
851
+ if (!this.manager.ready) throw new ODSystemError("Client isn't ready yet! Unable to register slash commands!")
852
+ if (instances.length > 0 && progress){
853
+ progress.max = instances.length
854
+ progress.start()
855
+ }
856
+
857
+ for (const instance of instances){
858
+ await this.createCmd(instance)
859
+ this.#debug.debug("Created new slash command",[
860
+ {key:"id",value:instance.id.value},
861
+ {key:"name",value:instance.name}
862
+ ])
863
+ if (progress) progress.increase(1)
864
+ }
865
+ }
866
+ /**Update all commands that are already registered. */
867
+ async updateExistingCommands(instances:ODSlashCommand[],progress?:ODManualProgressBar){
868
+ if (!this.manager.ready) throw new ODSystemError("Client isn't ready yet! Unable to register slash commands!")
869
+ if (instances.length > 0 && progress){
870
+ progress.max = instances.length
871
+ progress.start()
872
+ }
873
+
874
+ for (const instance of instances){
875
+ await this.createCmd(instance)
876
+ this.#debug.debug("Updated existing slash command",[{key:"id",value:instance.id.value},{key:"name",value:instance.name}])
877
+ if (progress) progress.increase(1)
878
+ }
879
+ }
880
+ /**Remove all commands that are registered but unused by Open Discord. */
881
+ async removeUnusedCommands(instances:ODSlashCommandUniversalCommand[],guildId?:string,progress?:ODManualProgressBar){
882
+ if (!this.manager.ready) throw new ODSystemError("Client isn't ready yet! Unable to register slash commands!")
883
+ if (!this.commandManager) throw new ODSystemError("Couldn't get client application to register slash commands!")
884
+ if (instances.length > 0 && progress){
885
+ progress.max = instances.length
886
+ progress.start()
887
+ }
888
+
889
+ const cmds = await this.commandManager.fetch({guildId})
890
+
891
+ for (const instance of instances){
892
+ const cmd = cmds.find((cmd) => cmd.name == instance.name)
893
+ if (cmd){
894
+ try {
895
+ await cmd.delete()
896
+ this.#debug.debug("Removed existing slash command",[{key:"name",value:cmd.name},{key:"guildId",value:guildId ?? "/"}])
897
+ }catch(err){
898
+ process.emit("uncaughtException",err)
899
+ throw new ODSystemError("Failed to delete slash command '/"+cmd.name+"'!")
900
+ }
901
+ }
902
+ if (progress) progress.increase(1)
903
+ }
904
+ }
905
+ /**Create a slash command. **(SYSTEM ONLY)** => Use `ODSlashCommandManager` for registering commands the default way! */
906
+ async createCmd(cmd:ODSlashCommand){
907
+ if (!this.commandManager) throw new ODSystemError("Couldn't get client application to register slash commands!")
908
+ try {
909
+ await this.commandManager.create(cmd.builder,(cmd.guildId ?? undefined))
910
+ }catch(err){
911
+ process.emit("uncaughtException",err)
912
+ throw new ODSystemError("Failed to register slash command '/"+cmd.name+"'!")
913
+ }
914
+ }
915
+ /**Start listening to the discord.js client `interactionCreate` event. */
916
+ startListeningToInteractions(){
917
+ this.manager.client.on("interactionCreate",(interaction) => {
918
+ //return when not in main server or DM
919
+ if (!this.manager.mainServer || (interaction.guild && interaction.guild.id != this.manager.mainServer.id)) return
920
+
921
+ if (!interaction.isChatInputCommand()) return
922
+ const cmd = this.getFiltered((cmd) => cmd.name == interaction.commandName)[0]
923
+ if (!cmd) return
924
+
925
+ this.#interactionListeners.forEach((listener) => {
926
+ if (typeof listener.name == "string" && (interaction.commandName != listener.name)) return
927
+ else if (listener.name instanceof RegExp && !listener.name.test(interaction.commandName)) return
928
+
929
+ //this is a valid listener
930
+ listener.callback(interaction,cmd)
931
+ })
932
+ })
933
+ }
934
+ /**Callback on interaction from one or multiple slash commands. */
935
+ onInteraction(commandName:string|RegExp, callback:ODSlashCommandInteractionCallback){
936
+ this.#interactionListeners.push({
937
+ name:commandName,
938
+ callback
939
+ })
940
+
941
+ if (this.#interactionListeners.length > this.listenerLimit){
942
+ this.#debug.console.log(new ODConsoleWarningMessage("Possible slash command interaction memory leak detected!",[
943
+ {key:"listeners",value:this.#interactionListeners.length.toString()}
944
+ ]))
945
+ }
946
+ }
947
+ }
948
+
949
+ /**## ODSlashCommandUpdateFunction `type`
950
+ * The function responsible for updating slash commands when they already exist.
951
+ */
952
+ export type ODSlashCommandUpdateFunction = (command:ODSlashCommandUniversalCommand) => boolean
953
+
954
+ /**## ODSlashCommand `class`
955
+ * This is an Open Discord slash command.
956
+ *
957
+ * When registered, you can listen for this command using the `ODCommandResponder`. The advantages of using this class for creating a slash command are:
958
+ * - automatic option parsing (even for channels, users, roles & mentions)!
959
+ * - automatic registration in discord.js
960
+ * - error reporting to the user when the bot fails to respond
961
+ * - plugins can extend this command
962
+ * - the bot won't re-register the command when it already exists (except when requested)!
963
+ *
964
+ * And more!
965
+ */
966
+ export class ODSlashCommand extends ODManagerData {
967
+ /**The discord.js builder for this slash command. */
968
+ builder: ODSlashCommandBuilder
969
+ /**The id of the guild this command is for. Null when not set. */
970
+ guildId: string|null
971
+ /**Function to check if the slash command requires to be updated (when it already exists). */
972
+ requiresUpdate: ODSlashCommandUpdateFunction|null = null
973
+
974
+ constructor(id:ODValidId, builder:ODSlashCommandBuilder, requiresUpdate?:ODSlashCommandUpdateFunction, guildId?:string){
975
+ super(id)
976
+ if (builder.type != discord.ApplicationCommandType.ChatInput) throw new ODSystemError("ApplicationCommandData is required to be the 'ChatInput' type!")
977
+
978
+ this.builder = builder
979
+ this.guildId = guildId ?? null
980
+ this.requiresUpdate = requiresUpdate ?? null
981
+ }
982
+
983
+ /**The name of this slash command. */
984
+ get name(): string {
985
+ return this.builder.name
986
+ }
987
+ set name(name:string){
988
+ this.builder.name = name
989
+ }
990
+ }
991
+
992
+ /**## ODTextCommandBuilderBaseOptionType `type`
993
+ * The types available in the text command option builder.
994
+ */
995
+ export type ODTextCommandBuilderBaseOptionType = "string"|"number"|"boolean"|"user"|"guildmember"|"role"|"mentionable"|"channel"
996
+
997
+ /**## ODTextCommandBuilderBaseOption `interface`
998
+ * The default option builder for text commands.
999
+ */
1000
+ export interface ODTextCommandBuilderBaseOption {
1001
+ /**The name of this option */
1002
+ name:string,
1003
+ /**The type of this option */
1004
+ type:ODTextCommandBuilderBaseOptionType,
1005
+ /**Is this option required? (optional options can only exist at the end of the command!) */
1006
+ required?:boolean
1007
+ }
1008
+
1009
+ /**## ODTextCommandBuilderStringOption `interface`
1010
+ * The string option builder for text commands.
1011
+ */
1012
+ export interface ODTextCommandBuilderStringOption extends ODTextCommandBuilderBaseOption {
1013
+ type:"string",
1014
+ /**Set the maximum length of this string */
1015
+ maxLength?:number,
1016
+ /**Set the minimum length of this string */
1017
+ minLength?:number,
1018
+ /**The string needs to match this regex or it will be invalid */
1019
+ regex?:RegExp,
1020
+ /**The string needs to match one of these choices or it will be invalid */
1021
+ choices?:string[],
1022
+ /**When this is the last option, allow this string to contain spaces */
1023
+ allowSpaces?:boolean
1024
+ }
1025
+
1026
+ /**## ODTextCommandBuilderNumberOption `interface`
1027
+ * The number option builder for text commands.
1028
+ */
1029
+ export interface ODTextCommandBuilderNumberOption extends ODTextCommandBuilderBaseOption {
1030
+ type:"number",
1031
+ /**The number can't be higher than this value */
1032
+ max?:number,
1033
+ /**The number can't be lower than this value */
1034
+ min?:number,
1035
+ /**Allow the number to be negative */
1036
+ allowNegative?:boolean,
1037
+ /**Allow the number to be positive */
1038
+ allowPositive?:boolean,
1039
+ /**Allow the number to be zero */
1040
+ allowZero?:boolean,
1041
+ /**Allow a number with decimal */
1042
+ allowDecimal?:boolean
1043
+ }
1044
+
1045
+ /**## ODTextCommandBuilderBooleanOption `interface`
1046
+ * The boolean option builder for text commands.
1047
+ */
1048
+ export interface ODTextCommandBuilderBooleanOption extends ODTextCommandBuilderBaseOption {
1049
+ type:"boolean",
1050
+ /**The value when `true` */
1051
+ trueValue?:string,
1052
+ /**The value when `false` */
1053
+ falseValue?:string
1054
+ }
1055
+
1056
+ /**## ODTextCommandBuilderChannelOption `interface`
1057
+ * The channel option builder for text commands.
1058
+ */
1059
+ export interface ODTextCommandBuilderChannelOption extends ODTextCommandBuilderBaseOption {
1060
+ type:"channel",
1061
+ /**When specified, only allow the following channel types */
1062
+ channelTypes?:discord.GuildChannelType[]
1063
+ }
1064
+
1065
+ /**## ODTextCommandBuilderRoleOption `interface`
1066
+ * The role option builder for text commands.
1067
+ */
1068
+ export interface ODTextCommandBuilderRoleOption extends ODTextCommandBuilderBaseOption {
1069
+ type:"role"
1070
+ }
1071
+
1072
+ /**## ODTextCommandBuilderUserOption `interface`
1073
+ * The user option builder for text commands.
1074
+ */
1075
+ export interface ODTextCommandBuilderUserOption extends ODTextCommandBuilderBaseOption {
1076
+ type:"user"
1077
+ }
1078
+
1079
+ /**## ODTextCommandBuilderGuildMemberOption `interface`
1080
+ * The guild member option builder for text commands.
1081
+ */
1082
+ export interface ODTextCommandBuilderGuildMemberOption extends ODTextCommandBuilderBaseOption {
1083
+ type:"guildmember"
1084
+ }
1085
+
1086
+ /**## ODTextCommandBuilderMentionableOption `interface`
1087
+ * The mentionable option builder for text commands.
1088
+ */
1089
+ export interface ODTextCommandBuilderMentionableOption extends ODTextCommandBuilderBaseOption {
1090
+ type:"mentionable"
1091
+ }
1092
+
1093
+ /**## ODTextCommandBuilderOption `type`
1094
+ * The option builder for text commands.
1095
+ */
1096
+ export type ODTextCommandBuilderOption = (
1097
+ ODTextCommandBuilderStringOption|
1098
+ ODTextCommandBuilderBooleanOption|
1099
+ ODTextCommandBuilderNumberOption|
1100
+ ODTextCommandBuilderChannelOption|
1101
+ ODTextCommandBuilderRoleOption|
1102
+ ODTextCommandBuilderUserOption|
1103
+ ODTextCommandBuilderGuildMemberOption|
1104
+ ODTextCommandBuilderMentionableOption
1105
+ )
1106
+
1107
+ /**## ODTextCommandBuilder `interface`
1108
+ * The builder for text commands. Here you can add options to the command.
1109
+ */
1110
+ export interface ODTextCommandBuilder {
1111
+ /**The prefix of this command */
1112
+ prefix:string,
1113
+ /**The name of this command (can include spaces for subcommands) */
1114
+ name:string,
1115
+ /**Is this command allowed in dm? */
1116
+ dmPermission?:boolean,
1117
+ /**Is this command allowed in guilds? */
1118
+ guildPermission?:boolean,
1119
+ /**When specified, only allow this command to be executed in the following guilds */
1120
+ allowedGuildIds?:string[],
1121
+ /**Are bots allowed to execute this command? */
1122
+ allowBots?:boolean
1123
+ /**The options for this text command (like slash commands) */
1124
+ options?:ODTextCommandBuilderOption[]
1125
+ }
1126
+
1127
+ /**## ODTextCommand `class`
1128
+ * This is an Open Discord text command.
1129
+ *
1130
+ * When registered, you can listen for this command using the `ODCommandResponder`. The advantages of using this class for creating a text command are:
1131
+ * - automatic option parsing (even for channels, users, roles & mentions)!
1132
+ * - automatic errors on invalid parameters
1133
+ * - error reporting to the user when the bot fails to respond
1134
+ * - plugins can extend this command
1135
+ *
1136
+ * And more!
1137
+ */
1138
+ export class ODTextCommand extends ODManagerData {
1139
+ /**The builder for this slash command. */
1140
+ builder: ODTextCommandBuilder
1141
+ /**The name of this slash command. */
1142
+ name: string
1143
+
1144
+ constructor(id:ODValidId, builder:ODTextCommandBuilder){
1145
+ super(id)
1146
+ this.builder = builder
1147
+ this.name = builder.name
1148
+ }
1149
+ }
1150
+
1151
+ /**## ODTextCommandInteractionOptionBase `interface`
1152
+ * The object returned for options from a text command interaction.
1153
+ */
1154
+ export interface ODTextCommandInteractionOptionBase<Name,Type> {
1155
+ /**The name of this option */
1156
+ name:string,
1157
+ /**The type of this option */
1158
+ type:Name,
1159
+ /**The value of this option */
1160
+ value:Type
1161
+ }
1162
+
1163
+ /**## ODTextCommandInteractionOption `type`
1164
+ * A list of types returned for options from a text command interaction.
1165
+ */
1166
+ export type ODTextCommandInteractionOption = (
1167
+ ODTextCommandInteractionOptionBase<"string",string>|
1168
+ ODTextCommandInteractionOptionBase<"number",number>|
1169
+ ODTextCommandInteractionOptionBase<"boolean",boolean>|
1170
+ ODTextCommandInteractionOptionBase<"channel",discord.GuildBasedChannel>|
1171
+ ODTextCommandInteractionOptionBase<"role",discord.Role>|
1172
+ ODTextCommandInteractionOptionBase<"user",discord.User>|
1173
+ ODTextCommandInteractionOptionBase<"guildmember",discord.GuildMember>|
1174
+ ODTextCommandInteractionOptionBase<"mentionable",discord.Role|discord.User>
1175
+ )
1176
+
1177
+ /**## ODTextCommandInteractionCallback `type`
1178
+ * Callback for the text command interaction listener.
1179
+ */
1180
+ export type ODTextCommandInteractionCallback = (msg:discord.Message, cmd:ODTextCommand, options:ODTextCommandInteractionOption[]) => void
1181
+
1182
+ /**## ODTextCommandErrorBase `interface`
1183
+ * The object returned from a text command error callback.
1184
+ */
1185
+ export interface ODTextCommandErrorBase {
1186
+ /**The type of text command error */
1187
+ type:"unknown_prefix"|"unknown_command"|"invalid_option"|"missing_option",
1188
+ /**The message this error originates from */
1189
+ msg:discord.Message
1190
+ }
1191
+
1192
+ /**## ODTextCommandErrorUnknownPrefix `interface`
1193
+ * The object returned from a text command unknown prefix error callback.
1194
+ */
1195
+ export interface ODTextCommandErrorUnknownPrefix extends ODTextCommandErrorBase {
1196
+ type:"unknown_prefix"
1197
+ }
1198
+
1199
+ /**## ODTextCommandErrorUnknownCommand `interface`
1200
+ * The object returned from a text command unknown command error callback.
1201
+ */
1202
+ export interface ODTextCommandErrorUnknownCommand extends ODTextCommandErrorBase {
1203
+ type:"unknown_command"
1204
+ }
1205
+
1206
+ /**## ODTextCommandErrorInvalidOptionReason `type`
1207
+ * A list of reasons for the invalid_option error to be thrown.
1208
+ */
1209
+ export type ODTextCommandErrorInvalidOptionReason = (
1210
+ "boolean"|
1211
+ "number_max"|
1212
+ "number_min"|
1213
+ "number_decimal"|
1214
+ "number_negative"|
1215
+ "number_positive"|
1216
+ "number_zero"|
1217
+ "number_invalid"|
1218
+ "string_max_length"|
1219
+ "string_min_length"|
1220
+ "string_regex"|
1221
+ "string_choice"|
1222
+ "not_in_guild"|
1223
+ "channel_not_found"|
1224
+ "channel_type"|
1225
+ "user_not_found"|
1226
+ "member_not_found"|
1227
+ "role_not_found"|
1228
+ "mentionable_not_found"
1229
+ )
1230
+
1231
+ /**## ODTextCommandErrorInvalidOption `interface`
1232
+ * The object returned from a text command invalid option error callback.
1233
+ */
1234
+ export interface ODTextCommandErrorInvalidOption extends ODTextCommandErrorBase {
1235
+ type:"invalid_option",
1236
+ /**The command this error originates from */
1237
+ command:ODTextCommand,
1238
+ /**The command prefix this error originates from */
1239
+ prefix:string,
1240
+ /**The command name this error originates from (can include spaces for subcommands) */
1241
+ name:string,
1242
+ /**The option that this error originates from */
1243
+ option:ODTextCommandBuilderOption
1244
+ /**The location that this option was found */
1245
+ location:number,
1246
+ /**The current value of this invalid option */
1247
+ value:string,
1248
+ /**The reason for this invalid option */
1249
+ reason:ODTextCommandErrorInvalidOptionReason
1250
+ }
1251
+
1252
+ /**## ODTextCommandErrorMissingOption `interface`
1253
+ * The object returned from a text command missing option error callback.
1254
+ */
1255
+ export interface ODTextCommandErrorMissingOption extends ODTextCommandErrorBase {
1256
+ type:"missing_option",
1257
+ /**The command this error originates from */
1258
+ command:ODTextCommand,
1259
+ /**The command prefix this error originates from */
1260
+ prefix:string,
1261
+ /**The command name this error originates from (can include spaces for subcommands) */
1262
+ name:string,
1263
+ /**The option that this error originates from */
1264
+ option:ODTextCommandBuilderOption
1265
+ /**The location that this option was found */
1266
+ location:number
1267
+ }
1268
+
1269
+ /**## ODTextCommandError `type`
1270
+ * A list of types returned for errors from a text command interaction.
1271
+ */
1272
+ export type ODTextCommandError = (
1273
+ ODTextCommandErrorUnknownPrefix|
1274
+ ODTextCommandErrorUnknownCommand|
1275
+ ODTextCommandErrorInvalidOption|
1276
+ ODTextCommandErrorMissingOption
1277
+ )
1278
+
1279
+ /**## ODTextCommandErrorCallback `type`
1280
+ * Callback for the text command error listener.
1281
+ */
1282
+ export type ODTextCommandErrorCallback = (error:ODTextCommandError) => void
1283
+
1284
+ /**## ODTextCommandManager `class`
1285
+ * This is an Open Discord client text manager.
1286
+ *
1287
+ * It's responsible for managing all the text commands from the client.
1288
+ *
1289
+ * Here, you can add & remove text commands & the bot will do the (de)registering.
1290
+ */
1291
+ export class ODTextCommandManager extends ODManager<ODTextCommand> {
1292
+ /**Alias to Open Discord debugger. */
1293
+ #debug: ODDebugger
1294
+ /**Copy of discord.js client. */
1295
+ manager: ODClientManager
1296
+ /**Collection of all interaction listeners. */
1297
+ #interactionListeners: {prefix:string, name:string|RegExp, callback:ODTextCommandInteractionCallback}[] = []
1298
+ /**Collection of all error listeners. */
1299
+ #errorListeners: ODTextCommandErrorCallback[] = []
1300
+ /**Set the soft limit for maximum amount of listeners. A warning will be shown when there are more listeners than this limit. */
1301
+ listenerLimit: number = 100
1302
+
1303
+ constructor(debug:ODDebugger, manager:ODClientManager){
1304
+ super(debug,"text command")
1305
+ this.#debug = debug
1306
+ this.manager = manager
1307
+ }
1308
+
1309
+ /*Check if a message is a registered command. */
1310
+ async #checkMessage(msg:discord.Message){
1311
+ if (this.manager.client.user && msg.author.id == this.manager.client.user.id) return false
1312
+
1313
+ //filter commands for correct prefix
1314
+ const validPrefixCommands: {cmd:ODTextCommand,newContent:string}[] = []
1315
+ await this.loopAll((cmd) => {
1316
+ if (msg.content.startsWith(cmd.builder.prefix)) validPrefixCommands.push({
1317
+ cmd:cmd,
1318
+ newContent:msg.content.substring(cmd.builder.prefix.length)
1319
+ })
1320
+ })
1321
+
1322
+ //return when no command with prefix
1323
+ if (validPrefixCommands.length == 0){
1324
+ this.#errorListeners.forEach((cb) => cb({
1325
+ type:"unknown_prefix",
1326
+ msg:msg
1327
+ }))
1328
+ return false
1329
+ }
1330
+
1331
+ //filter commands for correct name
1332
+ const validNameCommands: {cmd:ODTextCommand,newContent:string}[] = []
1333
+ validPrefixCommands.forEach((cmd) => {
1334
+ if (cmd.newContent.startsWith(cmd.cmd.builder.name+" ") || cmd.newContent == cmd.cmd.builder.name) validNameCommands.push({
1335
+ cmd:cmd.cmd,
1336
+ newContent:cmd.newContent.substring(cmd.cmd.builder.name.length+1) //+1 because of space after command name
1337
+ })
1338
+ })
1339
+
1340
+ //return when no command with name
1341
+ if (validNameCommands.length == 0){
1342
+ this.#errorListeners.forEach((cb) => cb({
1343
+ type:"unknown_command",
1344
+ msg:msg
1345
+ }))
1346
+ return false
1347
+ }
1348
+
1349
+ //the final command
1350
+ const command = validNameCommands[0]
1351
+ const builder = command.cmd.builder
1352
+
1353
+ //check additional options
1354
+ if (typeof builder.allowBots != "undefined" && !builder.allowBots && msg.author.bot) return false
1355
+ else if (typeof builder.dmPermission != "undefined" && !builder.dmPermission && msg.channel.type == discord.ChannelType.DM) return false
1356
+ else if (typeof builder.guildPermission != "undefined" && !builder.guildPermission && msg.guild) return false
1357
+ else if (typeof builder.allowedGuildIds != "undefined" && msg.guild && !builder.allowedGuildIds.includes(msg.guild.id)) return false
1358
+
1359
+ //check all command options & return when incorrect
1360
+ const options = await this.#checkOptions(command.cmd,command.newContent,msg)
1361
+ if (!options.valid) return false
1362
+
1363
+ //a command matched this message => emit event
1364
+ this.#interactionListeners.forEach((listener) => {
1365
+ if (typeof listener.prefix == "string" && (command.cmd.builder.prefix != listener.prefix)) return
1366
+ if (typeof listener.name == "string" && (command.cmd.name.split(" ")[0] != listener.name)) return
1367
+ else if (listener.name instanceof RegExp && !listener.name.test(command.cmd.name.split(" ")[0])) return
1368
+
1369
+ //this is a valid listener
1370
+ listener.callback(msg,command.cmd,options.data)
1371
+ })
1372
+ return true
1373
+ }
1374
+ /**Check if all options of a command are correct. */
1375
+ async #checkOptions(cmd:ODTextCommand, newContent:string, msg:discord.Message){
1376
+ const options = cmd.builder.options
1377
+ if (!options) return {valid:true,data:[]}
1378
+
1379
+ let tempContent = newContent
1380
+ let optionInvalid = false
1381
+ const optionData: ODTextCommandInteractionOption[] = []
1382
+
1383
+ const optionError = (type:"invalid_option"|"missing_option", option:ODTextCommandBuilderOption, location:number, value?:string, reason?:ODTextCommandErrorInvalidOptionReason) => {
1384
+ //ERROR INVALID
1385
+ if (type == "invalid_option" && value && reason){
1386
+ this.#errorListeners.forEach((cb) => cb({
1387
+ type:"invalid_option",
1388
+ msg:msg,
1389
+ prefix:cmd.builder.prefix,
1390
+ command:cmd,
1391
+ name:cmd.builder.name,
1392
+ option,
1393
+ location,
1394
+ value,
1395
+ reason
1396
+ }))
1397
+ }else if (type == "missing_option"){
1398
+ this.#errorListeners.forEach((cb) => cb({
1399
+ type:"missing_option",
1400
+ msg:msg,
1401
+ prefix:cmd.builder.prefix,
1402
+ command:cmd,
1403
+ name:cmd.builder.name,
1404
+ option,
1405
+ location
1406
+ }))
1407
+ }
1408
+ optionInvalid = true
1409
+ }
1410
+
1411
+ for (let location = 0;location < options.length;location++){
1412
+ const option = options[location]
1413
+ if (optionInvalid) break
1414
+
1415
+ //CHECK BOOLEAN
1416
+ if (option.type == "boolean"){
1417
+ const falseValue = option.falseValue ?? "false"
1418
+ const trueValue = option.trueValue ?? "true"
1419
+
1420
+ if (tempContent.startsWith(falseValue+" ")){
1421
+ //FALSE VALUE
1422
+ optionData.push({
1423
+ name:option.name,
1424
+ type:"boolean",
1425
+ value:false
1426
+ })
1427
+ tempContent = tempContent.substring(falseValue.length+1)
1428
+
1429
+ }else if (tempContent.startsWith(trueValue+" ")){
1430
+ //TRUE VALUE
1431
+ optionData.push({
1432
+ name:option.name,
1433
+ type:"boolean",
1434
+ value:true
1435
+ })
1436
+ tempContent = tempContent.substring(trueValue.length+1)
1437
+
1438
+ }else if (option.required){
1439
+ //REQUIRED => ERROR IF NOT EXISTING
1440
+ const invalidregex = /^[^ ]+/
1441
+ const invalidRes = invalidregex.exec(tempContent)
1442
+ if (invalidRes) optionError("invalid_option",option,location,invalidRes[0],"boolean")
1443
+ else optionError("missing_option",option,location)
1444
+ }
1445
+
1446
+ //CHECK NUMBER
1447
+ }else if (option.type == "number"){
1448
+ const numRegex = /^[0-9\.\,]+/
1449
+ const res = numRegex.exec(tempContent)
1450
+ if (res){
1451
+ const value = res[0].replace(/\,/g,".")
1452
+ tempContent = tempContent.substring(value.length+1)
1453
+ const numValue = Number(value)
1454
+
1455
+ if (isNaN(numValue)){
1456
+ optionError("invalid_option",option,location,value,"number_invalid")
1457
+
1458
+ }else if (typeof option.allowDecimal == "boolean" && !option.allowDecimal && (numValue % 1) !== 0){
1459
+ optionError("invalid_option",option,location,value,"number_decimal")
1460
+
1461
+ }else if (typeof option.allowNegative == "boolean" && !option.allowNegative && numValue < 0){
1462
+ optionError("invalid_option",option,location,value,"number_negative")
1463
+
1464
+ }else if (typeof option.allowPositive == "boolean" && !option.allowPositive && numValue > 0){
1465
+ optionError("invalid_option",option,location,value,"number_positive")
1466
+
1467
+ }else if (typeof option.allowZero == "boolean" && !option.allowZero && numValue == 0){
1468
+ optionError("invalid_option",option,location,value,"number_zero")
1469
+
1470
+ }else if (typeof option.max == "number" && numValue > option.max){
1471
+ optionError("invalid_option",option,location,value,"number_max")
1472
+
1473
+ }else if (typeof option.min == "number" && numValue < option.min){
1474
+ optionError("invalid_option",option,location,value,"number_min")
1475
+
1476
+ }else{
1477
+ //VALID NUMBER
1478
+ optionData.push({
1479
+ name:option.name,
1480
+ type:"number",
1481
+ value:numValue
1482
+ })
1483
+ }
1484
+ }else if (option.required){
1485
+ //REQUIRED => ERROR IF NOT EXISTING
1486
+ const invalidRegex = /^[^ ]+/
1487
+ const invalidRes = invalidRegex.exec(tempContent)
1488
+ if (invalidRes) optionError("invalid_option",option,location,invalidRes[0],"number_invalid")
1489
+ else optionError("missing_option",option,location)
1490
+ }
1491
+ //CHECK STRING
1492
+ }else if (option.type == "string"){
1493
+ if (option.allowSpaces){
1494
+ //STRING WITH SPACES
1495
+ const value = tempContent
1496
+ tempContent = ""
1497
+
1498
+ if (typeof option.minLength == "number" && value.length < option.minLength){
1499
+ optionError("invalid_option",option,location,value,"string_min_length")
1500
+
1501
+ }else if (typeof option.maxLength == "number" && value.length > option.maxLength){
1502
+ optionError("invalid_option",option,location,value,"string_max_length")
1503
+
1504
+ }else if (option.regex && !option.regex.test(value)){
1505
+ optionError("invalid_option",option,location,value,"string_regex")
1506
+
1507
+ }else if (option.choices && !option.choices.includes(value)){
1508
+ optionError("invalid_option",option,location,value,"string_choice")
1509
+
1510
+ }else if (option.required && value === ""){
1511
+ //REQUIRED => ERROR IF NOT EXISTING
1512
+ optionError("missing_option",option,location)
1513
+
1514
+ }else{
1515
+ //VALID STRING
1516
+ optionData.push({
1517
+ name:option.name,
1518
+ type:"string",
1519
+ value
1520
+ })
1521
+ }
1522
+ }else{
1523
+ //STRING WITHOUT SPACES
1524
+ const stringRegex = /^[^ ]+/
1525
+ const res = stringRegex.exec(tempContent)
1526
+ if (res){
1527
+ const value = res[0]
1528
+ tempContent = tempContent.substring(value.length+1)
1529
+
1530
+ if (typeof option.minLength == "number" && value.length < option.minLength){
1531
+ optionError("invalid_option",option,location,value,"string_min_length")
1532
+
1533
+ }else if (typeof option.maxLength == "number" && value.length > option.maxLength){
1534
+ optionError("invalid_option",option,location,value,"string_max_length")
1535
+
1536
+ }else if (option.regex && !option.regex.test(value)){
1537
+ optionError("invalid_option",option,location,value,"string_regex")
1538
+
1539
+ }else if (option.choices && !option.choices.includes(value)){
1540
+ optionError("invalid_option",option,location,value,"string_choice")
1541
+
1542
+ }else{
1543
+ //VALID STRING
1544
+ optionData.push({
1545
+ name:option.name,
1546
+ type:"string",
1547
+ value
1548
+ })
1549
+ }
1550
+ }else if (option.required){
1551
+ //REQUIRED => ERROR IF NOT EXISTING
1552
+ optionError("missing_option",option,location)
1553
+ }
1554
+ }
1555
+ //CHECK CHANNEL
1556
+ }else if (option.type == "channel"){
1557
+ const channelRegex = /^(?:<#)?([0-9]+)>?/
1558
+ const res = channelRegex.exec(tempContent)
1559
+ if (res){
1560
+ const value = res[0]
1561
+ tempContent = tempContent.substring(value.length+1)
1562
+ const channelId = res[1]
1563
+
1564
+ if (!msg.guild){
1565
+ optionError("invalid_option",option,location,value,"not_in_guild")
1566
+ }else{
1567
+ try{
1568
+ const channel = await msg.guild.channels.fetch(channelId)
1569
+ if (!channel){
1570
+ optionError("invalid_option",option,location,value,"channel_not_found")
1571
+
1572
+ }else if (option.channelTypes && !option.channelTypes.includes(channel.type)){
1573
+ optionError("invalid_option",option,location,value,"channel_type")
1574
+
1575
+ }else{
1576
+ //VALID CHANNEL
1577
+ optionData.push({
1578
+ name:option.name,
1579
+ type:"channel",
1580
+ value:channel
1581
+ })
1582
+ }
1583
+ }catch{
1584
+ optionError("invalid_option",option,location,value,"channel_not_found")
1585
+ }
1586
+ }
1587
+ }else if (option.required){
1588
+ //REQUIRED => ERROR IF NOT EXISTING
1589
+ const invalidRegex = /^[^ ]+/
1590
+ const invalidRes = invalidRegex.exec(tempContent)
1591
+ if (invalidRes) optionError("invalid_option",option,location,invalidRes[0],"channel_not_found")
1592
+ else optionError("missing_option",option,location)
1593
+ }
1594
+ //CHECK ROLE
1595
+ }else if (option.type == "role"){
1596
+ const roleRegex = /^(?:<@&)?([0-9]+)>?/
1597
+ const res = roleRegex.exec(tempContent)
1598
+ if (res){
1599
+ const value = res[0]
1600
+ tempContent = tempContent.substring(value.length+1)
1601
+ const roleId = res[1]
1602
+
1603
+ if (!msg.guild){
1604
+ optionError("invalid_option",option,location,value,"not_in_guild")
1605
+ }else{
1606
+ try{
1607
+ const role = await msg.guild.roles.fetch(roleId)
1608
+ if (!role){
1609
+ optionError("invalid_option",option,location,value,"role_not_found")
1610
+ }else{
1611
+ //VALID ROLE
1612
+ optionData.push({
1613
+ name:option.name,
1614
+ type:"role",
1615
+ value:role
1616
+ })
1617
+ }
1618
+ }catch{
1619
+ optionError("invalid_option",option,location,value,"role_not_found")
1620
+ }
1621
+ }
1622
+ }else if (option.required){
1623
+ //REQUIRED => ERROR IF NOT EXISTING
1624
+ const invalidRegex = /^[^ ]+/
1625
+ const invalidRes = invalidRegex.exec(tempContent)
1626
+ if (invalidRes) optionError("invalid_option",option,location,invalidRes[0],"role_not_found")
1627
+ else optionError("missing_option",option,location)
1628
+ }
1629
+ //CHECK GUILD MEMBER
1630
+ }else if (option.type == "guildmember"){
1631
+ const memberRegex = /^(?:<@)?([0-9]+)>?/
1632
+ const res = memberRegex.exec(tempContent)
1633
+ if (res){
1634
+ const value = res[0]
1635
+ tempContent = tempContent.substring(value.length+1)
1636
+ const memberId = res[1]
1637
+
1638
+ if (!msg.guild){
1639
+ optionError("invalid_option",option,location,value,"not_in_guild")
1640
+ }else{
1641
+ try{
1642
+ const member = await msg.guild.members.fetch(memberId)
1643
+ if (!member){
1644
+ optionError("invalid_option",option,location,value,"member_not_found")
1645
+ }else{
1646
+ //VALID GUILD MEMBER
1647
+ optionData.push({
1648
+ name:option.name,
1649
+ type:"guildmember",
1650
+ value:member
1651
+ })
1652
+ }
1653
+ }catch{
1654
+ optionError("invalid_option",option,location,value,"member_not_found")
1655
+ }
1656
+ }
1657
+ }else if (option.required){
1658
+ //REQUIRED => ERROR IF NOT EXISTING
1659
+ const invalidRegex = /^[^ ]+/
1660
+ const invalidRes = invalidRegex.exec(tempContent)
1661
+ if (invalidRes) optionError("invalid_option",option,location,invalidRes[0],"member_not_found")
1662
+ else optionError("missing_option",option,location)
1663
+ }
1664
+ //CHECK USER
1665
+ }else if (option.type == "user"){
1666
+ const userRegex = /^(?:<@)?([0-9]+)>?/
1667
+ const res = userRegex.exec(tempContent)
1668
+ if (res){
1669
+ const value = res[0]
1670
+ tempContent = tempContent.substring(value.length+1)
1671
+ const userId = res[1]
1672
+
1673
+ try{
1674
+ const user = await this.manager.client.users.fetch(userId)
1675
+ if (!user){
1676
+ optionError("invalid_option",option,location,value,"user_not_found")
1677
+ }else{
1678
+ //VALID USER
1679
+ optionData.push({
1680
+ name:option.name,
1681
+ type:"user",
1682
+ value:user
1683
+ })
1684
+ }
1685
+ }catch{
1686
+ optionError("invalid_option",option,location,value,"user_not_found")
1687
+ }
1688
+ }else if (option.required){
1689
+ //REQUIRED => ERROR IF NOT EXISTING
1690
+ const invalidRegex = /^[^ ]+/
1691
+ const invalidRes = invalidRegex.exec(tempContent)
1692
+ if (invalidRes) optionError("invalid_option",option,location,invalidRes[0],"user_not_found")
1693
+ else optionError("missing_option",option,location)
1694
+ }
1695
+ //CHECK MENTIONABLE
1696
+ }else if (option.type == "mentionable"){
1697
+ const mentionableRegex = /^<(@&?)([0-9]+)>/
1698
+ const res = mentionableRegex.exec(tempContent)
1699
+ if (res){
1700
+ const value = res[0]
1701
+ const type = (res[1] == "@&") ? "role" : "user"
1702
+ tempContent = tempContent.substring(value.length+1)
1703
+ const mentionableId = res[2]
1704
+
1705
+ if (!msg.guild){
1706
+ optionError("invalid_option",option,location,value,"not_in_guild")
1707
+ }else if (type == "role"){
1708
+ try {
1709
+ const role = await msg.guild.roles.fetch(mentionableId)
1710
+ if (!role){
1711
+ optionError("invalid_option",option,location,value,"mentionable_not_found")
1712
+ }else{
1713
+ //VALID ROLE
1714
+ optionData.push({
1715
+ name:option.name,
1716
+ type:"mentionable",
1717
+ value:role
1718
+ })
1719
+ }
1720
+ }catch{
1721
+ optionError("invalid_option",option,location,value,"mentionable_not_found")
1722
+ }
1723
+ }else if (type == "user"){
1724
+ try{
1725
+ const user = await this.manager.client.users.fetch(mentionableId)
1726
+ if (!user){
1727
+ optionError("invalid_option",option,location,value,"mentionable_not_found")
1728
+ }else{
1729
+ //VALID USER
1730
+ optionData.push({
1731
+ name:option.name,
1732
+ type:"mentionable",
1733
+ value:user
1734
+ })
1735
+ }
1736
+ }catch{
1737
+ optionError("invalid_option",option,location,value,"mentionable_not_found")
1738
+ }
1739
+ }
1740
+ }else if (option.required){
1741
+ //REQUIRED => ERROR IF NOT EXISTING
1742
+ const invalidRegex = /^[^ ]+/
1743
+ const invalidRes = invalidRegex.exec(tempContent)
1744
+ if (invalidRes) optionError("invalid_option",option,location,invalidRes[0],"mentionable_not_found")
1745
+ else optionError("missing_option",option,location)
1746
+ }
1747
+ }
1748
+ }
1749
+ return {valid:!optionInvalid,data:optionData}
1750
+ }
1751
+ /**Start listening to the discord.js client `messageCreate` event. */
1752
+ startListeningToInteractions(){
1753
+ this.manager.client.on("messageCreate",(msg) => {
1754
+ //return when not in main server or DM
1755
+ if (!this.manager.mainServer || (msg.guild && msg.guild.id != this.manager.mainServer.id)) return
1756
+ this.#checkMessage(msg)
1757
+ })
1758
+ }
1759
+ /**Check if optional values are only present at the end of the command. */
1760
+ #checkBuilderOptions(builder:ODTextCommandBuilder): {valid:boolean,reason:"required_after_optional"|"allowspaces_not_last"|null} {
1761
+ let optionalVisited = false
1762
+ let valid = true
1763
+ let reason: "required_after_optional"|"allowspaces_not_last"|null = null
1764
+ if (!builder.options) return {valid:true,reason:null}
1765
+ builder.options.forEach((opt,index,list) => {
1766
+ if (!opt.required) optionalVisited = true
1767
+ if (optionalVisited && opt.required){
1768
+ valid = false
1769
+ reason = "required_after_optional"
1770
+ }
1771
+
1772
+ if (opt.type == "string" && opt.allowSpaces && ((index+1) != list.length)){
1773
+ valid = false
1774
+ reason = "allowspaces_not_last"
1775
+ }
1776
+ })
1777
+
1778
+ return {valid,reason}
1779
+ }
1780
+ /**Callback on interaction from one of the registered text commands */
1781
+ onInteraction(commandPrefix:string,commandName:string|RegExp, callback:ODTextCommandInteractionCallback){
1782
+ this.#interactionListeners.push({
1783
+ prefix:commandPrefix,
1784
+ name:commandName,
1785
+ callback
1786
+ })
1787
+
1788
+ if (this.#interactionListeners.length > this.listenerLimit){
1789
+ this.#debug.console.log(new ODConsoleWarningMessage("Possible text command interaction memory leak detected!",[
1790
+ {key:"listeners",value:this.#interactionListeners.length.toString()}
1791
+ ]))
1792
+ }
1793
+ }
1794
+ /**Callback on error from all the registered text commands */
1795
+ onError(callback:ODTextCommandErrorCallback){
1796
+ this.#errorListeners.push(callback)
1797
+ }
1798
+
1799
+ add(data:ODTextCommand, overwrite?:boolean): boolean {
1800
+ const checkResult = this.#checkBuilderOptions(data.builder)
1801
+ if (!checkResult.valid && checkResult.reason == "required_after_optional") throw new ODSystemError("Invalid text command '"+data.id.value+"' => optional options are only allowed at the end of a command!")
1802
+ else if (!checkResult.valid && checkResult.reason == "allowspaces_not_last") throw new ODSystemError("Invalid text command '"+data.id.value+"' => string option with 'allowSpaces' is only allowed at the end of a command!")
1803
+ else return super.add(data,overwrite)
1804
+ }
1805
+ }
1806
+
1807
+ /**## ODContextMenuUniversalMenu `interface`
1808
+ * A universal template for a context menu.
1809
+ *
1810
+ * Why universal? Both **existing context menus** & **unregistered templates** can be converted to this type.
1811
+ */
1812
+ export interface ODContextMenuUniversalMenu {
1813
+ /**The type of this context menu. (required => `Message`|`User`) */
1814
+ type:discord.ApplicationCommandType.Message|discord.ApplicationCommandType.User,
1815
+ /**The name of this context menu. */
1816
+ name:string,
1817
+ /**All localized names of this context menu. */
1818
+ nameLocalizations:readonly ODSlashCommandUniversalTranslation[],
1819
+ /**The id of the guild this context menu is registered in. */
1820
+ guildId:string|null,
1821
+ /**Is this context menu for 18+ users only? */
1822
+ nsfw:boolean,
1823
+ /**A bitfield of the user permissions required to use this context menu. */
1824
+ defaultMemberPermissions:bigint,
1825
+ /**Is this context menu available in DM? */
1826
+ dmPermission:boolean,
1827
+ /**A list of contexts where you can install this context menu. */
1828
+ integrationTypes:readonly discord.ApplicationIntegrationType[],
1829
+ /**A list of contexts where you can use this context menu. */
1830
+ contexts:readonly discord.InteractionContextType[]
1831
+ }
1832
+
1833
+ /**## ODContextMenuBuilderMessage `interface`
1834
+ * The builder for message context menus.
1835
+ */
1836
+ export interface ODContextMenuBuilderMessage extends discord.MessageApplicationCommandData {
1837
+ /**This field is required in Open Discord for future compatibility. */
1838
+ integrationTypes:discord.ApplicationIntegrationType[],
1839
+ /**This field is required in Open Discord for future compatibility. */
1840
+ contexts:discord.InteractionContextType[]
1841
+ }
1842
+
1843
+ /**## ODContextMenuBuilderUser `interface`
1844
+ * The builder for user context menus.
1845
+ */
1846
+ export interface ODContextMenuBuilderUser extends discord.UserApplicationCommandData {
1847
+ /**This field is required in Open Discord for future compatibility. */
1848
+ integrationTypes:discord.ApplicationIntegrationType[],
1849
+ /**This field is required in Open Discord for future compatibility. */
1850
+ contexts:discord.InteractionContextType[]
1851
+ }
1852
+
1853
+ /**## ODContextMenuBuilderUser `interface`
1854
+ * The builder for context menus.
1855
+ */
1856
+ export type ODContextMenuBuilder = (ODContextMenuBuilderMessage|ODContextMenuBuilderUser)
1857
+
1858
+ /**## ODContextMenuComparator `class`
1859
+ * A utility class to compare existing context menu's with newly registered ones.
1860
+ */
1861
+ export class ODContextMenuComparator {
1862
+ /**Convert a `ODContextMenuBuilder` to a universal Open Discord context menu object for comparison. */
1863
+ convertBuilder(builder:ODContextMenuBuilder,guildId:string|null): ODContextMenuUniversalMenu|null {
1864
+ if (builder.type != discord.ApplicationCommandType.Message && builder.type != discord.ApplicationCommandType.User) return null
1865
+ const nameLoc = builder.nameLocalizations ?? {}
1866
+
1867
+ return {
1868
+ type:builder.type,
1869
+ name:builder.name,
1870
+ nameLocalizations:Object.keys(nameLoc).map((key) => {return {language:key as `${discord.Locale}`,value:nameLoc[key]}}),
1871
+ guildId:guildId,
1872
+ nsfw:builder.nsfw ?? false,
1873
+ defaultMemberPermissions:discord.PermissionsBitField.resolve(builder.defaultMemberPermissions ?? ["ViewChannel"]),
1874
+ dmPermission:(builder.contexts && builder.contexts.includes(discord.InteractionContextType.BotDM)) ?? false,
1875
+ integrationTypes:builder.integrationTypes ?? [discord.ApplicationIntegrationType.GuildInstall],
1876
+ contexts:builder.contexts ?? []
1877
+ }
1878
+ }
1879
+ /**Convert a `discord.ApplicationCommand` to a universal Open Discord context menu object for comparison. */
1880
+ convertMenu(cmd:discord.ApplicationCommand): ODContextMenuUniversalMenu|null {
1881
+ if (cmd.type != discord.ApplicationCommandType.Message && cmd.type != discord.ApplicationCommandType.User) return null
1882
+ const nameLoc = cmd.nameLocalizations ?? {}
1883
+
1884
+ return {
1885
+ type:cmd.type,
1886
+ name:cmd.name,
1887
+ nameLocalizations:Object.keys(nameLoc).map((key) => {return {language:key as `${discord.Locale}`,value:nameLoc[key]}}),
1888
+ guildId:cmd.guildId,
1889
+ nsfw:cmd.nsfw,
1890
+ defaultMemberPermissions:discord.PermissionsBitField.resolve(cmd.defaultMemberPermissions ?? ["ViewChannel"]),
1891
+ dmPermission:(cmd.contexts && cmd.contexts.includes(discord.InteractionContextType.BotDM)) ? true : false,
1892
+ integrationTypes:cmd.integrationTypes ?? [discord.ApplicationIntegrationType.GuildInstall],
1893
+ contexts:cmd.contexts ?? []
1894
+ }
1895
+ }
1896
+ /**Returns `true` when the 2 context menus are the same. */
1897
+ compare(ctxA:ODContextMenuUniversalMenu,ctxB:ODContextMenuUniversalMenu): boolean {
1898
+ if (ctxA.name != ctxB.name) return false
1899
+ if (ctxA.type != ctxB.type) return false
1900
+ if (ctxA.nsfw != ctxB.nsfw) return false
1901
+ if (ctxA.guildId != ctxB.guildId) return false
1902
+ if (ctxA.dmPermission != ctxB.dmPermission) return false
1903
+ if (ctxA.defaultMemberPermissions != ctxB.defaultMemberPermissions) return false
1904
+
1905
+ //nameLocalizations
1906
+ if (ctxA.nameLocalizations.length != ctxB.nameLocalizations.length) return false
1907
+ if (!ctxA.nameLocalizations.every((nameA) => {
1908
+ const nameB = ctxB.nameLocalizations.find((nameB) => nameB.language == nameA.language)
1909
+ if (!nameB || nameA.value != nameB.value) return false
1910
+ else return true
1911
+ })) return false
1912
+
1913
+ //contexts
1914
+ if (ctxA.contexts.length != ctxB.contexts.length) return false
1915
+ if (!ctxA.contexts.every((contextA) => {
1916
+ return ctxB.contexts.includes(contextA)
1917
+ })) return false
1918
+
1919
+ //integrationTypes
1920
+ if (ctxA.integrationTypes.length != ctxB.integrationTypes.length) return false
1921
+ if (!ctxA.integrationTypes.every((integrationA) => {
1922
+ return ctxB.integrationTypes.includes(integrationA)
1923
+ })) return false
1924
+
1925
+ return true
1926
+ }
1927
+ }
1928
+
1929
+ /**## ODContextMenuInteractionCallback `type`
1930
+ * Callback for the context menu interaction listener.
1931
+ */
1932
+ export type ODContextMenuInteractionCallback = (interaction:discord.ContextMenuCommandInteraction,cmd:ODContextMenu) => void
1933
+
1934
+ /**## ODContextMenuRegisteredResult `type`
1935
+ * The result which will be returned when getting all (un)registered user context menu's from the manager.
1936
+ */
1937
+ export type ODContextMenuRegisteredResult = {
1938
+ /**A list of all registered context menus. */
1939
+ registered:{
1940
+ /**The instance (`ODContextMenu`) from this context menu. */
1941
+ instance:ODContextMenu,
1942
+ /**The universal object/template/builder of this context menu. */
1943
+ menu:ODContextMenuUniversalMenu,
1944
+ /**Does this context menu require an update? */
1945
+ requiresUpdate:boolean
1946
+ }[],
1947
+ /**A list of all unregistered context menus. */
1948
+ unregistered:{
1949
+ /**The instance (`ODContextMenu`) from this context menu. */
1950
+ instance:ODContextMenu,
1951
+ /**The universal object/template/builder of this context menu. */
1952
+ menu:null,
1953
+ /**Does this context menu require an update? */
1954
+ requiresUpdate:true
1955
+ }[],
1956
+ /**A list of all unused context menus (not found in `ODContextMenuManager`). */
1957
+ unused:{
1958
+ /**The instance (`ODContextMenu`) from this context menu. */
1959
+ instance:null,
1960
+ /**The universal object/template/builder of this context menu. */
1961
+ menu:ODContextMenuUniversalMenu,
1962
+ /**Does this context menu require an update? */
1963
+ requiresUpdate:false
1964
+ }[]
1965
+ }
1966
+
1967
+ /**## ODContextMenuManager `class`
1968
+ * This is an Open Discord client context menu manager.
1969
+ *
1970
+ * It's responsible for managing all the context interactions from the client.
1971
+ *
1972
+ * Here, you can add & remove context interactions & the bot will do the (de)registering.
1973
+ */
1974
+ export class ODContextMenuManager extends ODManager<ODContextMenu> {
1975
+ /**Alias to Open Discord debugger. */
1976
+ #debug: ODDebugger
1977
+
1978
+ /**Refrerence to discord.js client. */
1979
+ manager: ODClientManager
1980
+ /**Discord.js application commands manager. */
1981
+ commandManager: discord.ApplicationCommandManager|null
1982
+ /**Collection of all interaction listeners. */
1983
+ #interactionListeners: {name:string|RegExp, callback:ODContextMenuInteractionCallback}[] = []
1984
+ /**Set the soft limit for maximum amount of listeners. A warning will be shown when there are more listeners than this limit. */
1985
+ listenerLimit: number = 100
1986
+ /**A utility class used to compare 2 context menus with each other. */
1987
+ comparator: ODContextMenuComparator = new ODContextMenuComparator()
1988
+
1989
+ constructor(debug:ODDebugger, manager:ODClientManager){
1990
+ super(debug,"context menu")
1991
+ this.#debug = debug
1992
+ this.manager = manager
1993
+ this.commandManager = (manager.client.application) ? manager.client.application.commands : null
1994
+ }
1995
+
1996
+ /**Get all registered & unregistered message context menu commands. */
1997
+ async getAllRegisteredMenus(guildId?:string): Promise<ODContextMenuRegisteredResult> {
1998
+ if (!this.commandManager) throw new ODSystemError("Couldn't get client application to register message context menus!")
1999
+
2000
+ const menus = (await this.commandManager.fetch({guildId})).toJSON()
2001
+ const registered: {instance:ODContextMenu, menu:ODContextMenuUniversalMenu, requiresUpdate:boolean}[] = []
2002
+ const unregistered: {instance:ODContextMenu, menu:null, requiresUpdate:true}[] = []
2003
+ const unused: {instance:null, menu:ODContextMenuUniversalMenu, requiresUpdate:false}[] = []
2004
+
2005
+ await this.loopAll((instance) => {
2006
+ if (guildId && instance.guildId != guildId) return
2007
+
2008
+ const index = menus.findIndex((menu) => menu.name == instance.name)
2009
+ const menu = menus[index]
2010
+ menus.splice(index,1)
2011
+ if (menu){
2012
+ //menu is registered (and may need to be updated)
2013
+ const universalBuilder = this.comparator.convertBuilder(instance.builder,instance.guildId)
2014
+ const universalMenu = this.comparator.convertMenu(menu)
2015
+
2016
+ //menu is not of the type 'message'|'user'
2017
+ if (!universalBuilder || !universalMenu) return
2018
+
2019
+ const didChange = !this.comparator.compare(universalBuilder,universalMenu)
2020
+ const requiresUpdate = didChange || (instance.requiresUpdate ? instance.requiresUpdate(universalMenu) : false)
2021
+ registered.push({instance,menu:universalMenu,requiresUpdate})
2022
+
2023
+ //menu is not registered
2024
+ }else unregistered.push({instance,menu:null,requiresUpdate:true})
2025
+ })
2026
+
2027
+ menus.forEach((menu) => {
2028
+ //menu does not exist in the manager (only append to unused when type == 'message'|'user')
2029
+ const universalCmd = this.comparator.convertMenu(menu)
2030
+ if (!universalCmd) return
2031
+ unused.push({instance:null,menu:universalCmd,requiresUpdate:false})
2032
+ })
2033
+
2034
+ return {registered,unregistered,unused}
2035
+ }
2036
+ /**Create all context menus that are not registered yet.*/
2037
+ async createNewMenus(instances:ODContextMenu[],progress?:ODManualProgressBar){
2038
+ if (!this.manager.ready) throw new ODSystemError("Client isn't ready yet! Unable to register context menus!")
2039
+ if (instances.length > 0 && progress){
2040
+ progress.max = instances.length
2041
+ progress.start()
2042
+ }
2043
+
2044
+ for (const instance of instances){
2045
+ await this.createMenu(instance)
2046
+ this.#debug.debug("Created new context menu",[
2047
+ {key:"id",value:instance.id.value},
2048
+ {key:"name",value:instance.name},
2049
+ {key:"type",value:(instance.builder.type == discord.ApplicationCommandType.Message) ? "message-context" : "user-context"}
2050
+ ])
2051
+ if (progress) progress.increase(1)
2052
+ }
2053
+ }
2054
+ /**Update all context menus that are already registered. */
2055
+ async updateExistingMenus(instances:ODContextMenu[],progress?:ODManualProgressBar){
2056
+ if (!this.manager.ready) throw new ODSystemError("Client isn't ready yet! Unable to register context menus!")
2057
+ if (instances.length > 0 && progress){
2058
+ progress.max = instances.length
2059
+ progress.start()
2060
+ }
2061
+
2062
+ for (const instance of instances){
2063
+ await this.createMenu(instance)
2064
+ this.#debug.debug("Updated existing context menu",[
2065
+ {key:"id",value:instance.id.value},
2066
+ {key:"name",value:instance.name},
2067
+ {key:"type",value:(instance.builder.type == discord.ApplicationCommandType.Message) ? "message-context" : "user-context"}
2068
+ ])
2069
+ if (progress) progress.increase(1)
2070
+ }
2071
+ }
2072
+ /**Remove all context menus that are registered but unused by Open Discord. */
2073
+ async removeUnusedMenus(instances:ODContextMenuUniversalMenu[],guildId?:string,progress?:ODManualProgressBar){
2074
+ if (!this.manager.ready) throw new ODSystemError("Client isn't ready yet! Unable to register context menus!")
2075
+ if (!this.commandManager) throw new ODSystemError("Couldn't get client application to register context menus!")
2076
+ if (instances.length > 0 && progress){
2077
+ progress.max = instances.length
2078
+ progress.start()
2079
+ }
2080
+
2081
+ const menus = await this.commandManager.fetch({guildId})
2082
+
2083
+ for (const instance of instances){
2084
+ const menu = menus.find((menu) => menu.name == instance.name)
2085
+ if (menu){
2086
+ try {
2087
+ await menu.delete()
2088
+ this.#debug.debug("Removed existing context menu",[
2089
+ {key:"name",value:menu.name},
2090
+ {key:"guildId",value:guildId ?? "/"},
2091
+ {key:"type",value:(instance.type == discord.ApplicationCommandType.Message) ? "message-context" : "user-context"}
2092
+ ])
2093
+ }catch(err){
2094
+ process.emit("uncaughtException",err)
2095
+ throw new ODSystemError("Failed to delete context menu '"+menu.name+"'!")
2096
+ }
2097
+ }
2098
+ if (progress) progress.increase(1)
2099
+ }
2100
+ }
2101
+ /**Create a context menu. **(SYSTEM ONLY)** => Use `ODContextMenuManager` for registering context menu's the default way! */
2102
+ async createMenu(menu:ODContextMenu){
2103
+ if (!this.commandManager) throw new ODSystemError("Couldn't get client application to register context menu's!")
2104
+ try {
2105
+ await this.commandManager.create(menu.builder,(menu.guildId ?? undefined))
2106
+ }catch(err){
2107
+ process.emit("uncaughtException",err)
2108
+ throw new ODSystemError("Failed to register context menu '"+menu.name+"'!")
2109
+ }
2110
+ }
2111
+ /**Start listening to the discord.js client `interactionCreate` event. */
2112
+ startListeningToInteractions(){
2113
+ this.manager.client.on("interactionCreate",(interaction) => {
2114
+ //return when not in main server or DM
2115
+ if (!this.manager.mainServer || (interaction.guild && interaction.guild.id != this.manager.mainServer.id)) return
2116
+
2117
+ if (!interaction.isContextMenuCommand()) return
2118
+ const menu = this.getFiltered((menu) => menu.name == interaction.commandName)[0]
2119
+ if (!menu) return
2120
+
2121
+ this.#interactionListeners.forEach((listener) => {
2122
+ if (typeof listener.name == "string" && (interaction.commandName != listener.name)) return
2123
+ else if (listener.name instanceof RegExp && !listener.name.test(interaction.commandName)) return
2124
+
2125
+ //this is a valid listener
2126
+ listener.callback(interaction,menu)
2127
+ })
2128
+ })
2129
+ }
2130
+ /**Callback on interaction from one or multiple context menu's. */
2131
+ onInteraction(menuName:string|RegExp, callback:ODContextMenuInteractionCallback){
2132
+ this.#interactionListeners.push({
2133
+ name:menuName,
2134
+ callback
2135
+ })
2136
+
2137
+ if (this.#interactionListeners.length > this.listenerLimit){
2138
+ this.#debug.console.log("Possible context menu interaction memory leak detected!","warning",[
2139
+ {key:"listeners",value:this.#interactionListeners.length.toString()}
2140
+ ])
2141
+ }
2142
+ }
2143
+ }
2144
+
2145
+ /**## ODContextMenuUpdateFunction `type`
2146
+ * The function responsible for updating context menu's when they already exist.
2147
+ */
2148
+ export type ODContextMenuUpdateFunction = (menu:ODContextMenuUniversalMenu) => boolean
2149
+
2150
+ /**## ODContextMenu `class`
2151
+ * This is an Open Discord context menu.
2152
+ *
2153
+ * When registered, you can listen for this context menu using the `ODContextResponder`. The advantages of using this class for creating a context menu are:
2154
+ * - automatic registration in discord.js
2155
+ * - error reporting to the user when the bot fails to respond
2156
+ * - plugins can extend this context menu
2157
+ * - the bot won't re-register the context menu when it already exists (except when requested)!
2158
+ *
2159
+ * And more!
2160
+ */
2161
+ export class ODContextMenu extends ODManagerData {
2162
+ /**The discord.js builder for this context menu. */
2163
+ builder: ODContextMenuBuilder
2164
+ /**The id of the guild this context menu is for. `null` when not set. */
2165
+ guildId: string|null
2166
+ /**Function to check if the context menu requires to be updated (when it already exists). */
2167
+ requiresUpdate: ODContextMenuUpdateFunction|null = null
2168
+
2169
+ constructor(id:ODValidId, builder:ODContextMenuBuilder, requiresUpdate?:ODContextMenuUpdateFunction, guildId?:string){
2170
+ super(id)
2171
+ if (builder.type != discord.ApplicationCommandType.Message && builder.type != discord.ApplicationCommandType.User) throw new ODSystemError("ApplicationCommandData is required to be the 'Message'|'User' type!")
2172
+
2173
+ this.builder = builder
2174
+ this.guildId = guildId ?? null
2175
+ this.requiresUpdate = requiresUpdate ?? null
2176
+ }
2177
+
2178
+ /**The name of this context menu. */
2179
+ get name(): string {
2180
+ return this.builder.name
2181
+ }
2182
+ set name(name:string){
2183
+ this.builder.name = name
2184
+ }
2185
+ }
2186
+
2187
+ /**## ODAutocompleteInteractionCallback `type`
2188
+ * Callback for the autocomplete interaction listener.
2189
+ */
2190
+ export type ODAutocompleteInteractionCallback = (interaction:discord.AutocompleteInteraction) => void
2191
+
2192
+ /**## ODAutocompleteManager `class`
2193
+ * This is an Open Discord client autocomplete interaction manager.
2194
+ *
2195
+ * It's responsible for managing all the autocomplete interactions from the client.
2196
+ */
2197
+ export class ODAutocompleteManager {
2198
+ /**Alias to Open Discord debugger. */
2199
+ #debug: ODDebugger
2200
+
2201
+ /**Refrerence to discord.js client. */
2202
+ manager: ODClientManager
2203
+ /**Discord.js application commands manager. */
2204
+ commandManager: discord.ApplicationCommandManager|null
2205
+ /**Collection of all interaction listeners. */
2206
+ #interactionListeners: {cmdName:string|RegExp, optName:string|RegExp, callback:ODAutocompleteInteractionCallback}[] = []
2207
+ /**Set the soft limit for maximum amount of listeners. A warning will be shown when there are more listeners than this limit. */
2208
+ listenerLimit: number = 100
2209
+
2210
+ constructor(debug:ODDebugger, manager:ODClientManager){
2211
+ this.#debug = debug
2212
+ this.manager = manager
2213
+ this.commandManager = (manager.client.application) ? manager.client.application.commands : null
2214
+ }
2215
+
2216
+ /**Start listening to the discord.js client `interactionCreate` event. */
2217
+ startListeningToInteractions(){
2218
+ this.manager.client.on("interactionCreate",(interaction) => {
2219
+ //return when not in main server or DM
2220
+ if (!this.manager.mainServer || (interaction.guild && interaction.guild.id != this.manager.mainServer.id)) return
2221
+
2222
+ if (!interaction.isAutocomplete()) return
2223
+ this.#interactionListeners.forEach((listener) => {
2224
+
2225
+ if (typeof listener.cmdName == "string" && (interaction.commandName != listener.cmdName)) return
2226
+ else if (listener.cmdName instanceof RegExp && !listener.cmdName.test(interaction.commandName)) return
2227
+ if (typeof listener.optName == "string" && (interaction.options.getFocused(true).name != listener.optName)) return
2228
+ else if (listener.optName instanceof RegExp && !listener.optName.test(interaction.options.getFocused(true).name)) return
2229
+
2230
+ //this is a valid listener
2231
+ listener.callback(interaction)
2232
+ })
2233
+ })
2234
+ }
2235
+ /**Callback on interaction from one or multiple autocompletes. */
2236
+ onInteraction(cmdName:string|RegExp,optName:string|RegExp,callback:ODAutocompleteInteractionCallback){
2237
+ this.#interactionListeners.push({
2238
+ cmdName,optName,callback
2239
+ })
2240
+
2241
+ if (this.#interactionListeners.length > this.listenerLimit){
2242
+ this.#debug.console.log("Possible autocomplete interaction memory leak detected!","warning",[
2243
+ {key:"listeners",value:this.#interactionListeners.length.toString()}
2244
+ ])
2245
+ }
2246
+ }
2247
+ }