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