@open-discord-bots/framework 0.3.14 → 0.3.15

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