MAJOR: Full rewrite of slash cmd loading system
The system of detecting, comparing & registering slash cmds has been rewritten and updated for future compatibility. This will break all current plugins (so please wait until they are updated) => Increased speed => All cmd params are now used when comparing => Future compability & removed deprecated functions => New universal utility interfaces for slash commands => And more!
This commit is contained in:
+415
-134
@@ -402,11 +402,354 @@ export class ODClientActivityManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODSlashCommandUniversalTranslation `interface`
|
||||
* A universal template for a slash command translation. (used in names & descriptions)
|
||||
*
|
||||
* Why universal? Both **existing slash commands** & **unregistered templates** can be converted to this type.
|
||||
*/
|
||||
export interface ODSlashCommandUniversalTranslation {
|
||||
/**The language code or locale of this language. */
|
||||
language:`${discord.Locale}`,
|
||||
/**The translation of the name in this language. */
|
||||
value:string
|
||||
}
|
||||
|
||||
/**## ODSlashCommandUniversalOptionChoice `interface`
|
||||
* A universal template for a slash command option choice. (used in `string` options)
|
||||
*
|
||||
* Why universal? Both **existing slash commands** & **unregistered templates** can be converted to this type.
|
||||
*/
|
||||
export interface ODSlashCommandUniversalOptionChoice {
|
||||
/**The name of this choice. */
|
||||
name:string,
|
||||
/**All localized names of this choice. */
|
||||
nameLocalizations:readonly ODSlashCommandUniversalTranslation[],
|
||||
/**The value of this choice. */
|
||||
value:string
|
||||
}
|
||||
|
||||
/**## ODSlashCommandUniversalOption `interface`
|
||||
* A universal template for a slash command option.
|
||||
*
|
||||
* Why universal? Both **existing slash commands** & **unregistered templates** can be converted to this type.
|
||||
*/
|
||||
export interface ODSlashCommandUniversalOption {
|
||||
/**The type of this option. */
|
||||
type:discord.ApplicationCommandOptionType,
|
||||
/**The name of this option. */
|
||||
name:string,
|
||||
/**All localized names of this option. */
|
||||
nameLocalizations:readonly ODSlashCommandUniversalTranslation[],
|
||||
/**The description of this option. */
|
||||
description:string,
|
||||
/**All localized descriptions of this option. */
|
||||
descriptionLocalizations:readonly ODSlashCommandUniversalTranslation[],
|
||||
/**Is this option required? */
|
||||
required:boolean,
|
||||
|
||||
/**Is autocomplete enabled in this option? */
|
||||
autocomplete:boolean|null,
|
||||
/**Choices for this option (only when type is `string`) */
|
||||
choices:ODSlashCommandUniversalOptionChoice[],
|
||||
/**A list of sub-options for this option (only when type is `subCommand` or `subCommandGroup`) */
|
||||
options:readonly ODSlashCommandUniversalOption[],
|
||||
/**A list of allowed channel types for this option (only when type is `channel`) */
|
||||
channelTypes:readonly discord.ChannelType[],
|
||||
/**The minimum amount required for this option (only when type is `number` or `integer`) */
|
||||
minValue:number|null,
|
||||
/**The maximum amount required for this option (only when type is `number` or `integer`) */
|
||||
maxValue:number|null,
|
||||
/**The minimum length required for this option (only when type is `string`) */
|
||||
minLength:number|null,
|
||||
/**The maximum length required for this option (only when type is `string`) */
|
||||
maxLength:number|null
|
||||
}
|
||||
|
||||
/**## ODSlashCommandUniversalCommand `interface`
|
||||
* A universal template for a slash command.
|
||||
*
|
||||
* Why universal? Both **existing slash commands** & **unregistered templates** can be converted to this type.
|
||||
*/
|
||||
export interface ODSlashCommandUniversalCommand {
|
||||
/**The type of this command. (required => `ChatInput`) */
|
||||
type:discord.ApplicationCommandType.ChatInput,
|
||||
/**The name of this command. */
|
||||
name:string,
|
||||
/**All localized names of this command. */
|
||||
nameLocalizations:readonly ODSlashCommandUniversalTranslation[],
|
||||
/**The description of this command. */
|
||||
description:string,
|
||||
/**All localized descriptions of this command. */
|
||||
descriptionLocalizations:readonly ODSlashCommandUniversalTranslation[],
|
||||
/**The id of the guild this command is registered in. */
|
||||
guildId:string|null,
|
||||
/**Is this command for 18+ users only? */
|
||||
nsfw:boolean,
|
||||
/**A list of options for this command. */
|
||||
options:readonly ODSlashCommandUniversalOption[],
|
||||
/**A bitfield of the user permissions required to use this command. */
|
||||
defaultMemberPermissions:bigint,
|
||||
/**Is this command available in DM? */
|
||||
dmPermission:boolean,
|
||||
/**A list of contexts where you can install this command. */
|
||||
integrationTypes:readonly discord.ApplicationIntegrationType[],
|
||||
/**A list of contexts where you can use this command. */
|
||||
contexts:readonly discord.InteractionContextType[]
|
||||
}
|
||||
|
||||
/**## ODSlashCommandBuilder `interface`
|
||||
* The builder for slash commands. Here you can add options to the command.
|
||||
*/
|
||||
export interface ODSlashCommandBuilder extends discord.ChatInputApplicationCommandData {
|
||||
/**@deprecated `dmPermission` is deprecated. Use `context` instead! (Not using contexts might result in the slash command being re-registered on every startup!) */
|
||||
dmPermission?:boolean
|
||||
/**This field is required in Open Ticket for future compatibility. */
|
||||
integrationTypes:discord.ApplicationIntegrationType[],
|
||||
/**This field is required in Open Ticket for future compatibility. */
|
||||
contexts:discord.InteractionContextType[]
|
||||
}
|
||||
|
||||
export class ODSlashCommandComparator {
|
||||
/**Convert a `discord.ApplicationCommandOptionChoiceData<string>` to a universal Open Ticket slash command option choice object for comparison. */
|
||||
#convertOptionChoice(choice:discord.ApplicationCommandOptionChoiceData<string>): ODSlashCommandUniversalOptionChoice {
|
||||
const nameLoc = choice.nameLocalizations ?? {}
|
||||
return {
|
||||
name:choice.name,
|
||||
nameLocalizations:Object.keys(nameLoc).map((key) => {return {language:key as `${discord.Locale}`,value:nameLoc[key]}}),
|
||||
value:choice.value
|
||||
}
|
||||
}
|
||||
/**Convert a `discord.ApplicationCommandOptionData` to a universal Open Ticket slash command option object for comparison. */
|
||||
#convertBuilderOption(option:discord.ApplicationCommandOptionData): ODSlashCommandUniversalOption {
|
||||
const nameLoc = option.nameLocalizations ?? {}
|
||||
const descLoc = option.descriptionLocalizations ?? {}
|
||||
return {
|
||||
type:option.type,
|
||||
name:option.name,
|
||||
nameLocalizations:Object.keys(nameLoc).map((key) => {return {language:key as `${discord.Locale}`,value:nameLoc[key]}}),
|
||||
description:option.description,
|
||||
descriptionLocalizations:Object.keys(descLoc).map((key) => {return {language:key as `${discord.Locale}`,value:descLoc[key]}}),
|
||||
required:(option.type != discord.ApplicationCommandOptionType.SubcommandGroup && option.type != discord.ApplicationCommandOptionType.Subcommand && option.required) ? true : false,
|
||||
|
||||
autocomplete:option.autocomplete ?? false,
|
||||
choices:(option.type == discord.ApplicationCommandOptionType.String && !option.autocomplete && option.choices) ? option.choices.map((choice) => this.#convertOptionChoice(choice)) : [],
|
||||
options:((option.type == discord.ApplicationCommandOptionType.SubcommandGroup || option.type == discord.ApplicationCommandOptionType.Subcommand) && option.options) ? option.options.map((opt) => this.#convertBuilderOption(opt)) : [],
|
||||
channelTypes:(option.type == discord.ApplicationCommandOptionType.Channel && option.channelTypes) ? option.channelTypes : [],
|
||||
minValue:(option.type == discord.ApplicationCommandOptionType.Number && option.minValue) ? option.minValue : null,
|
||||
maxValue:(option.type == discord.ApplicationCommandOptionType.Number && option.maxValue) ? option.maxValue : null,
|
||||
minLength:(option.type == discord.ApplicationCommandOptionType.String && option.minLength) ? option.minLength : null,
|
||||
maxLength:(option.type == discord.ApplicationCommandOptionType.String && option.maxLength) ? option.maxLength : null
|
||||
}
|
||||
}
|
||||
/**Convert a `discord.ApplicationCommandOption` to a universal Open Ticket slash command option object for comparison. */
|
||||
#convertCommandOption(option:discord.ApplicationCommandOption): ODSlashCommandUniversalOption {
|
||||
const nameLoc = option.nameLocalizations ?? {}
|
||||
const descLoc = option.descriptionLocalizations ?? {}
|
||||
|
||||
return {
|
||||
type:option.type,
|
||||
name:option.name,
|
||||
nameLocalizations:Object.keys(nameLoc).map((key) => {return {language:key as `${discord.Locale}`,value:nameLoc[key]}}),
|
||||
description:option.description,
|
||||
descriptionLocalizations:Object.keys(descLoc).map((key) => {return {language:key as `${discord.Locale}`,value:descLoc[key]}}),
|
||||
required:(option.type != discord.ApplicationCommandOptionType.SubcommandGroup && option.type != discord.ApplicationCommandOptionType.Subcommand && option.required) ? true : false,
|
||||
|
||||
autocomplete:option.autocomplete ?? false,
|
||||
choices:(option.type == discord.ApplicationCommandOptionType.String && !option.autocomplete && option.choices) ? option.choices.map((choice) => this.#convertOptionChoice(choice)) : [],
|
||||
options:((option.type == discord.ApplicationCommandOptionType.SubcommandGroup || option.type == discord.ApplicationCommandOptionType.Subcommand) && option.options) ? option.options.map((opt) => this.#convertBuilderOption(opt)) : [],
|
||||
channelTypes:(option.type == discord.ApplicationCommandOptionType.Channel && option.channelTypes) ? option.channelTypes : [],
|
||||
minValue:(option.type == discord.ApplicationCommandOptionType.Number && option.minValue) ? option.minValue : null,
|
||||
maxValue:(option.type == discord.ApplicationCommandOptionType.Number && option.maxValue) ? option.maxValue : null,
|
||||
minLength:(option.type == discord.ApplicationCommandOptionType.String && option.minLength) ? option.minLength : null,
|
||||
maxLength:(option.type == discord.ApplicationCommandOptionType.String && option.maxLength) ? option.maxLength : null
|
||||
}
|
||||
}
|
||||
/**Convert a `ODSlashCommandBuilder` to a universal Open Ticket slash command object for comparison. */
|
||||
convertBuilder(builder:ODSlashCommandBuilder,guildId:string|null): ODSlashCommandUniversalCommand {
|
||||
if (builder.type != discord.ApplicationCommandType.ChatInput) throw new ODSystemError("ODSlashCommandComparator:convertBuilder() is not supported for other types than 'ChatInput'!")
|
||||
const nameLoc = builder.nameLocalizations ?? {}
|
||||
const descLoc = builder.descriptionLocalizations ?? {}
|
||||
return {
|
||||
type:1,
|
||||
name:builder.name,
|
||||
nameLocalizations:Object.keys(nameLoc).map((key) => {return {language:key as `${discord.Locale}`,value:nameLoc[key]}}),
|
||||
description:builder.description,
|
||||
descriptionLocalizations:Object.keys(descLoc).map((key) => {return {language:key as `${discord.Locale}`,value:descLoc[key]}}),
|
||||
guildId:guildId,
|
||||
nsfw:builder.nsfw ?? false,
|
||||
options:builder.options ? builder.options.map((opt) => this.#convertBuilderOption(opt)) : [],
|
||||
defaultMemberPermissions:discord.PermissionsBitField.resolve(builder.defaultMemberPermissions ?? ["ViewChannel"]),
|
||||
dmPermission:(builder.contexts && builder.contexts.includes(discord.InteractionContextType.BotDM)) ?? false,
|
||||
integrationTypes:builder.integrationTypes ?? [discord.ApplicationIntegrationType.GuildInstall],
|
||||
contexts:builder.contexts ?? []
|
||||
}
|
||||
}
|
||||
/**Convert a `discord.ApplicationCommand` to a universal Open Ticket slash command object for comparison. */
|
||||
convertCommand(cmd:discord.ApplicationCommand): ODSlashCommandUniversalCommand {
|
||||
if (cmd.type != discord.ApplicationCommandType.ChatInput) throw new ODSystemError("ODSlashCommandComparator:convertCommand() is not supported for other types than 'ChatInput'!")
|
||||
const nameLoc = cmd.nameLocalizations ?? {}
|
||||
const descLoc = cmd.descriptionLocalizations ?? {}
|
||||
return {
|
||||
type:1,
|
||||
name:cmd.name,
|
||||
nameLocalizations:Object.keys(nameLoc).map((key) => {return {language:key as `${discord.Locale}`,value:nameLoc[key]}}),
|
||||
description:cmd.description,
|
||||
descriptionLocalizations:Object.keys(descLoc).map((key) => {return {language:key as `${discord.Locale}`,value:descLoc[key]}}),
|
||||
guildId:cmd.guildId,
|
||||
nsfw:cmd.nsfw,
|
||||
options:cmd.options ? cmd.options.map((opt) => this.#convertCommandOption(opt)) : [],
|
||||
defaultMemberPermissions:discord.PermissionsBitField.resolve(cmd.defaultMemberPermissions ?? ["ViewChannel"]),
|
||||
dmPermission:(cmd.contexts && cmd.contexts.includes(discord.InteractionContextType.BotDM)) ? true : false,
|
||||
integrationTypes:cmd.integrationTypes ?? [discord.ApplicationIntegrationType.GuildInstall],
|
||||
contexts:cmd.contexts ?? []
|
||||
}
|
||||
}
|
||||
/**Returns `true` when the 2 slash command options are the same. */
|
||||
compareOption(optA:ODSlashCommandUniversalOption,optB:ODSlashCommandUniversalOption): boolean {
|
||||
if (optA.name != optB.name) return false
|
||||
if (optA.description != optB.description) return false
|
||||
if (optA.type != optB.type) return false
|
||||
if (optA.required != optB.required) return false
|
||||
if (optA.autocomplete != optB.autocomplete) return false
|
||||
if (optA.minValue != optB.minValue) return false
|
||||
if (optA.maxValue != optB.maxValue) return false
|
||||
if (optA.minLength != optB.minLength) return false
|
||||
if (optA.maxLength != optB.maxLength) return false
|
||||
|
||||
//nameLocalizations
|
||||
if (optA.nameLocalizations.length != optB.nameLocalizations.length) return false
|
||||
if (!optA.nameLocalizations.every((nameA) => {
|
||||
const nameB = optB.nameLocalizations.find((nameB) => nameB.language == nameA.language)
|
||||
if (!nameB || nameA.value != nameB.value) return false
|
||||
else return true
|
||||
})) return false
|
||||
|
||||
//descriptionLocalizations
|
||||
if (optA.descriptionLocalizations.length != optB.descriptionLocalizations.length) return false
|
||||
if (!optA.descriptionLocalizations.every((descA) => {
|
||||
const descB = optB.descriptionLocalizations.find((descB) => descB.language == descA.language)
|
||||
if (!descB || descA.value != descB.value) return false
|
||||
else return true
|
||||
})) return false
|
||||
|
||||
//choices
|
||||
if (optA.choices.length != optB.choices.length) return false
|
||||
if (!optA.choices.every((choiceA,index) => {
|
||||
const choiceB = optB.choices[index]
|
||||
if (choiceA.name != choiceB.name) return false
|
||||
if (choiceA.value != choiceB.value) return false
|
||||
|
||||
//nameLocalizations
|
||||
if (choiceA.nameLocalizations.length != choiceB.nameLocalizations.length) return false
|
||||
if (!choiceA.nameLocalizations.every((nameA) => {
|
||||
const nameB = choiceB.nameLocalizations.find((nameB) => nameB.language == nameA.language)
|
||||
if (!nameB || nameA.value != nameB.value) return false
|
||||
else return true
|
||||
})) return false
|
||||
|
||||
return true
|
||||
})) return false
|
||||
|
||||
//channelTypes
|
||||
if (optA.channelTypes.length != optB.channelTypes.length) return false
|
||||
if (!optA.channelTypes.every((typeA) => {
|
||||
return optB.channelTypes.includes(typeA)
|
||||
})) return false
|
||||
|
||||
//options
|
||||
if (optA.options.length != optB.options.length) return false
|
||||
if (!optA.options.every((subOptA,index) => {
|
||||
return this.compareOption(subOptA,optB.options[index])
|
||||
})) return false
|
||||
|
||||
return true
|
||||
}
|
||||
/**Returns `true` when the 2 slash commands are the same. */
|
||||
compare(cmdA:ODSlashCommandUniversalCommand,cmdB:ODSlashCommandUniversalCommand): boolean {
|
||||
if (cmdA.name != cmdB.name) return false
|
||||
if (cmdA.description != cmdB.description) return false
|
||||
if (cmdA.type != cmdB.type) return false
|
||||
if (cmdA.nsfw != cmdB.nsfw) return false
|
||||
if (cmdA.guildId != cmdB.guildId) return false
|
||||
if (cmdA.dmPermission != cmdB.dmPermission) return false
|
||||
if (cmdA.defaultMemberPermissions != cmdB.defaultMemberPermissions) return false
|
||||
|
||||
//nameLocalizations
|
||||
if (cmdA.nameLocalizations.length != cmdB.nameLocalizations.length) return false
|
||||
if (!cmdA.nameLocalizations.every((nameA) => {
|
||||
const nameB = cmdB.nameLocalizations.find((nameB) => nameB.language == nameA.language)
|
||||
if (!nameB || nameA.value != nameB.value) return false
|
||||
else return true
|
||||
})) return false
|
||||
|
||||
//descriptionLocalizations
|
||||
if (cmdA.descriptionLocalizations.length != cmdB.descriptionLocalizations.length) return false
|
||||
if (!cmdA.descriptionLocalizations.every((descA) => {
|
||||
const descB = cmdB.descriptionLocalizations.find((descB) => descB.language == descA.language)
|
||||
if (!descB || descA.value != descB.value) return false
|
||||
else return true
|
||||
})) return false
|
||||
|
||||
//contexts
|
||||
if (cmdA.contexts.length != cmdB.contexts.length) return false
|
||||
if (!cmdA.contexts.every((contextA) => {
|
||||
return cmdB.contexts.includes(contextA)
|
||||
})) return false
|
||||
|
||||
//integrationTypes
|
||||
if (cmdA.integrationTypes.length != cmdB.integrationTypes.length) return false
|
||||
if (!cmdA.integrationTypes.every((integrationA) => {
|
||||
return cmdB.integrationTypes.includes(integrationA)
|
||||
})) return false
|
||||
|
||||
//options
|
||||
if (cmdA.options.length != cmdB.options.length) return false
|
||||
if (!cmdA.options.every((optA,index) => {
|
||||
return this.compareOption(optA,cmdB.options[index])
|
||||
})) return false
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODSlashCommandInteractionCallback `type`
|
||||
* Callback for the slash command interaction listener.
|
||||
*/
|
||||
export type ODSlashCommandInteractionCallback = (interaction:discord.ChatInputCommandInteraction,cmd:ODSlashCommand) => void
|
||||
|
||||
/**## ODSlashCommandRegisteredResult `type`
|
||||
* The result which will be returned when getting all (un)registered slash commands from the manager.
|
||||
*/
|
||||
export type ODSlashCommandRegisteredResult = {
|
||||
/**A list of all registered commands. */
|
||||
registered:{
|
||||
/**The instance (`ODSlashCommand`) from this command. */
|
||||
instance:ODSlashCommand,
|
||||
/**The (universal) slash command object/template of this command. */
|
||||
cmd:ODSlashCommandUniversalCommand,
|
||||
/**Does this command require an update? */
|
||||
requiresUpdate:boolean
|
||||
}[],
|
||||
/**A list of all unregistered commands. */
|
||||
unregistered:{
|
||||
/**The instance (`ODSlashCommand`) from this command. */
|
||||
instance:ODSlashCommand,
|
||||
/**The (universal) slash command object/template of this command. */
|
||||
cmd:null,
|
||||
/**Does this command require an update? */
|
||||
requiresUpdate:true
|
||||
}[],
|
||||
/**A list of all unused commands (not found in `ODSlashCommandManager`). */
|
||||
unused:{
|
||||
/**The instance (`ODSlashCommand`) from this command. */
|
||||
instance:null,
|
||||
/**The (universal) slash command object/template of this command. */
|
||||
cmd:ODSlashCommandUniversalCommand,
|
||||
/**Does this command require an update? */
|
||||
requiresUpdate:false
|
||||
}[]
|
||||
}
|
||||
|
||||
/**## ODSlashCommandManager `class`
|
||||
* This is an open ticket client slash manager.
|
||||
*
|
||||
@@ -418,7 +761,7 @@ export class ODSlashCommandManager extends ODManager<ODSlashCommand> {
|
||||
/**Alias to open ticket debugger. */
|
||||
#debug: ODDebugger
|
||||
|
||||
/**Copy of discord.js client. */
|
||||
/**Refrerence to discord.js client. */
|
||||
manager: ODClientManager
|
||||
/**Discord.js application commands manager. */
|
||||
commandManager: discord.ApplicationCommandManager|null
|
||||
@@ -426,6 +769,8 @@ export class ODSlashCommandManager extends ODManager<ODSlashCommand> {
|
||||
#interactionListeners: {name:string|RegExp, callback:ODSlashCommandInteractionCallback}[] = []
|
||||
/**Set the soft limit for maximum amount of listeners. A warning will be shown when there are more listeners than this limit. */
|
||||
listenerLimit: number = 100
|
||||
/**A utility class used to compare 2 slash commands with each other. */
|
||||
comparator: ODSlashCommandComparator = new ODSlashCommandComparator()
|
||||
|
||||
constructor(debug:ODDebugger, manager:ODClientManager){
|
||||
super(debug,"slash command")
|
||||
@@ -434,41 +779,83 @@ export class ODSlashCommandManager extends ODManager<ODSlashCommand> {
|
||||
this.commandManager = (manager.client.application) ? manager.client.application.commands : null
|
||||
}
|
||||
|
||||
/**Create all commands that don't exist yet. (global by default OR guildId for per-server) */
|
||||
async createNewCommands(guildId?:string){
|
||||
if (!this.manager.ready) throw new ODSystemError("Client isn't ready yet! Unable to register slash commands!")
|
||||
const cmds = await this.#existsCmd(guildId)
|
||||
/**Get all registered & unregistered slash commands. */
|
||||
async getAllRegisteredCommands(guildId?:string): Promise<ODSlashCommandRegisteredResult> {
|
||||
if (!this.commandManager) throw new ODSystemError("Couldn't get client application to register slash commands!")
|
||||
|
||||
let i = 0
|
||||
while (i < cmds.nonExisting.length){
|
||||
await this.#createCmd(cmds.nonExisting[i])
|
||||
this.#debug.debug("Created new slash command",[{key:"id",value:cmds.nonExisting[i].id.value},{key:"name",value:cmds.nonExisting[i].name}])
|
||||
i++
|
||||
}
|
||||
const cmds = (await this.commandManager.fetch({guildId})).toJSON()
|
||||
const registered: {instance:ODSlashCommand, cmd:ODSlashCommandUniversalCommand, requiresUpdate:boolean}[] = []
|
||||
const unregistered: {instance:ODSlashCommand, cmd:null, requiresUpdate:true}[] = []
|
||||
const unused: {instance:null, cmd:ODSlashCommandUniversalCommand, requiresUpdate:false}[] = []
|
||||
|
||||
await this.loopAll((instance) => {
|
||||
if (guildId && instance.guildId != guildId) return
|
||||
|
||||
const index = cmds.findIndex((cmd) => cmd.name == instance.name)
|
||||
const cmd = cmds[index]
|
||||
cmds.splice(index,1)
|
||||
if (cmd){
|
||||
//command is registered (and may need to be updated)
|
||||
const universalBuilder = this.comparator.convertBuilder(instance.builder,instance.guildId)
|
||||
const universalCmd = this.comparator.convertCommand(cmd)
|
||||
const didChange = !this.comparator.compare(universalBuilder,universalCmd)
|
||||
const requiresUpdate = didChange || (instance.requiresUpdate ? instance.requiresUpdate(universalCmd) : false)
|
||||
registered.push({instance,cmd:universalCmd,requiresUpdate})
|
||||
|
||||
//command is not registered
|
||||
}else unregistered.push({instance,cmd:null,requiresUpdate:true})
|
||||
})
|
||||
|
||||
cmds.forEach((cmd) => {
|
||||
//command does not exist in the manager
|
||||
unused.push({instance:null,cmd:this.comparator.convertCommand(cmd),requiresUpdate:false})
|
||||
})
|
||||
|
||||
return {registered,unregistered,unused}
|
||||
}
|
||||
/**Update all commands that already exist. (global by default OR guildId for per-server) */
|
||||
async updateExistingCommands(guildId?:string,force?:boolean){
|
||||
/**Create all commands that are not registered yet.*/
|
||||
async createNewCommands(instances:ODSlashCommand[]){
|
||||
if (!this.manager.ready) throw new ODSystemError("Client isn't ready yet! Unable to register slash commands!")
|
||||
const cmds = await this.#existsCmd(guildId)
|
||||
const filtered = cmds.existing.filter((cmd) => (cmd.requiresUpdate == true || force))
|
||||
|
||||
for (const cmd of filtered){
|
||||
await this.#createCmd(cmd.cmd)
|
||||
this.#debug.debug("Updated existing slash command",[
|
||||
{key:"id",value:cmd.cmd.id.value},
|
||||
{key:"name",value:cmd.cmd.name},
|
||||
{key:"force",value:force ? "true" : "false"}
|
||||
for (const instance of instances){
|
||||
await this.createCmd(instance)
|
||||
this.#debug.debug("Created new slash command",[
|
||||
{key:"id",value:instance.id.value},
|
||||
{key:"name",value:instance.name}
|
||||
])
|
||||
}
|
||||
}
|
||||
/**Remove all commands that exist but aren't used by open ticket. (global by default OR guildId for per-server) */
|
||||
async removeUnusedCommands(guildId?:string){
|
||||
/**Update all commands that are already registered. */
|
||||
async updateExistingCommands(instances:ODSlashCommand[]){
|
||||
if (!this.manager.ready) throw new ODSystemError("Client isn't ready yet! Unable to register slash commands!")
|
||||
const cmds = (await this.#unusedCmd(guildId)).map((cmd) => cmd.name)
|
||||
await this.#removeCmd(cmds,guildId)
|
||||
|
||||
for (const instance of instances){
|
||||
await this.createCmd(instance)
|
||||
this.#debug.debug("Updated existing slash command",[{key:"id",value:instance.id.value},{key:"name",value:instance.name}])
|
||||
}
|
||||
}
|
||||
/**Create a slash command from an `ODSlashCommand` class */
|
||||
async #createCmd(cmd:ODSlashCommand){
|
||||
/**Remove all commands that are registered but unused by Open Ticket. */
|
||||
async removeUnusedCommands(instances:ODSlashCommandUniversalCommand[],guildId?:string){
|
||||
if (!this.manager.ready) throw new ODSystemError("Client isn't ready yet! Unable to register slash commands!")
|
||||
if (!this.commandManager) throw new ODSystemError("Couldn't get client application to register slash commands!")
|
||||
|
||||
const cmds = await this.commandManager.fetch({guildId})
|
||||
|
||||
for (const instance of instances){
|
||||
const cmd = cmds.find((cmd) => cmd.name == instance.name)
|
||||
if (cmd){
|
||||
try {
|
||||
await cmd.delete()
|
||||
this.#debug.debug("Removed existing slash command",[{key:"name",value:cmd.name},{key:"guildId",value:guildId ?? "/"}])
|
||||
}catch(err){
|
||||
process.emit("uncaughtException",err)
|
||||
throw new ODSystemError("Failed to delete slash command '/"+cmd.name+"'!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**Create a slash command. **(SYSTEM ONLY)** => Use `ODSlashCommandManager` for registering commands the default way! */
|
||||
async createCmd(cmd:ODSlashCommand){
|
||||
if (!this.commandManager) throw new ODSystemError("Couldn't get client application to register slash commands!")
|
||||
try {
|
||||
await this.commandManager.create(cmd.builder,(cmd.guildId ?? undefined))
|
||||
@@ -477,107 +864,6 @@ export class ODSlashCommandManager extends ODManager<ODSlashCommand> {
|
||||
throw new ODSystemError("Failed to register slash command '/"+cmd.name+"'!")
|
||||
}
|
||||
}
|
||||
/**Remove slash cmds by name */
|
||||
async #removeCmd(names:string[],guildId?:string){
|
||||
if (!this.commandManager) throw new ODSystemError("Couldn't get client application to register slash commands!")
|
||||
|
||||
const cmds = await this.commandManager.fetch({guildId})
|
||||
|
||||
cmds?.forEach((cmd) => {
|
||||
if (!names.includes(cmd.name)) return
|
||||
cmd.delete()
|
||||
this.#debug.debug("Removed existing slash command",[{key:"name",value:cmd.name}])
|
||||
})
|
||||
}
|
||||
/**Get all existing & non-existing slash commands. */
|
||||
async #existsCmd(guildId?:string){
|
||||
if (!this.commandManager) throw new ODSystemError("Couldn't get client application to register slash commands!")
|
||||
|
||||
const cmds = await this.commandManager.fetch({guildId})
|
||||
const existing: {cmd:ODSlashCommand, requiresUpdate:boolean}[] = []
|
||||
const nonExisting: ODSlashCommand[] = []
|
||||
|
||||
await this.loopAll((cmd) => {
|
||||
if (guildId && cmd.guildId != guildId) return
|
||||
const result = cmds.find((cmddata) => cmddata.name == cmd.name)
|
||||
if (result){
|
||||
//command already exists (and may need to be updated)
|
||||
const requiresUpdate = cmd.requiresUpdate ? cmd.requiresUpdate({
|
||||
name:result.name,
|
||||
description:result.description,
|
||||
options:result.options as readonly discord.ApplicationCommandOptionData[],
|
||||
nsfw:result.nsfw,
|
||||
dmPermission:result.dmPermission ?? undefined,
|
||||
defaultMemberPermissions:result.defaultMemberPermissions,
|
||||
nameLocalizations:result.nameLocalizations ?? undefined,
|
||||
descriptionLocalizations:result.descriptionLocalizations ?? undefined
|
||||
}) : false
|
||||
existing.push({cmd,requiresUpdate})
|
||||
|
||||
//command doesn't exists yet
|
||||
}else nonExisting.push(cmd)
|
||||
})
|
||||
|
||||
return {existing,nonExisting}
|
||||
}
|
||||
/**Get all unused slash commands. */
|
||||
async #unusedCmd(guildId?:string){
|
||||
if (!this.commandManager) throw new ODSystemError("Couldn't get client application to register slash commands!")
|
||||
|
||||
const cmds = await this.commandManager.fetch({guildId})
|
||||
const unused: discord.ApplicationCommand[] = []
|
||||
|
||||
const allCommands = this.getAll()
|
||||
cmds.forEach((cmddata) => {
|
||||
if (guildId && cmddata.guildId != guildId) return
|
||||
|
||||
if (!allCommands.find((cmd) => {
|
||||
if (cmd.name != cmddata.name) return false
|
||||
if (cmd.builder.description != cmddata.description) return false
|
||||
return this.#checkUnusedOptions(cmddata.options,cmd.builder.options ?? [])
|
||||
|
||||
})) unused.push(cmddata)
|
||||
})
|
||||
|
||||
return unused
|
||||
}
|
||||
/**Returns `true` if the options are the same in both arrays. It will work recursively! */
|
||||
#checkUnusedOptions(currentOptions:readonly discord.ApplicationCommandOption[], newOptions:readonly discord.ApplicationCommandOptionData[]){
|
||||
if ((!newOptions || newOptions.length == 0) && currentOptions.length == 0) return true
|
||||
else if (newOptions && newOptions.length != currentOptions.length) return false
|
||||
|
||||
if (!currentOptions.every((currentOpt,index) => {
|
||||
const newOpt = newOptions[index]
|
||||
if (newOpt.name != currentOpt.name) return false
|
||||
if (newOpt.type != currentOpt.type) return false
|
||||
if (newOpt.description != currentOpt.description) return false
|
||||
|
||||
if (newOpt.type == discord.ApplicationCommandOptionType.Number && currentOpt.type == discord.ApplicationCommandOptionType.Number){
|
||||
//check for min & max values when type is number
|
||||
if (newOpt.minValue != currentOpt.minValue) return false
|
||||
if (newOpt.maxValue != currentOpt.maxValue) return false
|
||||
}
|
||||
if (newOpt.type == discord.ApplicationCommandOptionType.String && currentOpt.type == discord.ApplicationCommandOptionType.String){
|
||||
//check for min & max length values when type is string
|
||||
if (newOpt.minLength != currentOpt.minLength) return false
|
||||
if (newOpt.maxLength != currentOpt.maxLength) return false
|
||||
}
|
||||
|
||||
//check for required normal options
|
||||
if (currentOpt.type != discord.ApplicationCommandOptionType.SubcommandGroup && currentOpt.type != discord.ApplicationCommandOptionType.Subcommand && newOpt.type != discord.ApplicationCommandOptionType.SubcommandGroup && newOpt.type != discord.ApplicationCommandOptionType.Subcommand){
|
||||
if (currentOpt.required != newOpt.required) return false
|
||||
|
||||
//check for sub-options in subcommands
|
||||
}else if ((currentOpt.type == discord.ApplicationCommandOptionType.SubcommandGroup && newOpt.type == discord.ApplicationCommandOptionType.SubcommandGroup) || (currentOpt.type == discord.ApplicationCommandOptionType.Subcommand && newOpt.type == discord.ApplicationCommandOptionType.Subcommand)){
|
||||
if (currentOpt.options && newOpt.options){
|
||||
return this.#checkUnusedOptions(currentOpt.options,newOpt.options)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})) return false
|
||||
return true
|
||||
}
|
||||
/**Start listening to the discord.js client `interactionCreate` event. */
|
||||
startListeningToInteractions(){
|
||||
this.manager.client.on("interactionCreate",(interaction) => {
|
||||
@@ -612,15 +898,10 @@ export class ODSlashCommandManager extends ODManager<ODSlashCommand> {
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODSlashCommandBuilder `interface`
|
||||
* The builder for slash commands. Here you can add options to the command.
|
||||
*/
|
||||
export interface ODSlashCommandBuilder extends discord.ChatInputApplicationCommandData {}
|
||||
|
||||
/**## ODSlashCommandUpdateFunction `type`
|
||||
* The function responsible for updating slash commands when they already exist.
|
||||
*/
|
||||
export type ODSlashCommandUpdateFunction = (current:ODSlashCommandBuilder) => boolean
|
||||
export type ODSlashCommandUpdateFunction = (command:ODSlashCommandUniversalCommand) => boolean
|
||||
|
||||
/**## ODSlashCommand `class`
|
||||
* This is an open ticket slash command.
|
||||
|
||||
@@ -83,6 +83,8 @@ export interface ODDefaults {
|
||||
slashCommandRegistering:boolean,
|
||||
/**When enabled, the bot is forced to re-register all slash commands in the server. This can be used in case of a auto-update malfunction. */
|
||||
forceSlashCommandRegistration:boolean,
|
||||
/**When enabled, the bot is allowed to unregister all slash commands which aren't used in Open Ticket. Disable this if you don't want to use the Open Ticket `ODSlashCommand` classes. */
|
||||
allowSlashCommandRemoval:boolean,
|
||||
/**Load the default open ticket text commands. */
|
||||
textCommandLoading:boolean,
|
||||
|
||||
@@ -265,6 +267,7 @@ export class ODDefaultsManager {
|
||||
slashCommandLoading:true,
|
||||
slashCommandRegistering:true,
|
||||
forceSlashCommandRegistration:false,
|
||||
allowSlashCommandRemoval:true,
|
||||
textCommandLoading:true,
|
||||
|
||||
questionLoading:true,
|
||||
|
||||
+6
-8
@@ -394,15 +394,13 @@ const main = async () => {
|
||||
openticket.log("Registering slash commands... (this can take up to 2 minutes)","system")
|
||||
await openticket.events.get("onSlashCommandRegister").emit([openticket.client.slashCommands,openticket.client])
|
||||
if (openticket.defaults.getDefault("slashCommandRegistering")){
|
||||
//GLOBAL
|
||||
await openticket.client.slashCommands.removeUnusedCommands() //remove all commands that aren't used
|
||||
await openticket.client.slashCommands.createNewCommands() //create all new commands that don't exist yet
|
||||
await openticket.client.slashCommands.updateExistingCommands(undefined,openticket.defaults.getDefault("forceSlashCommandRegistration")) //update all commands that need to be re-registered
|
||||
//get all commands that are already registered in the bot
|
||||
const cmds = await openticket.client.slashCommands.getAllRegisteredCommands()
|
||||
|
||||
//DEFAULT SERVER
|
||||
await openticket.client.slashCommands.removeUnusedCommands(serverId) //remove all commands that aren't used
|
||||
await openticket.client.slashCommands.createNewCommands(serverId) //create all new commands that don't exist yet
|
||||
await openticket.client.slashCommands.updateExistingCommands(serverId) //update all commands that need to be re-registered
|
||||
//remove unused cmds, create new cmds & update existing cmds
|
||||
if (openticket.defaults.getDefault("allowSlashCommandRemoval")) await openticket.client.slashCommands.removeUnusedCommands(cmds.unused.map((cmd) => cmd.cmd))
|
||||
await openticket.client.slashCommands.createNewCommands(cmds.unregistered.map((cmd) => cmd.instance))
|
||||
await openticket.client.slashCommands.updateExistingCommands(cmds.registered.filter((cmd) => cmd.requiresUpdate || openticket.defaults.getDefault("forceSlashCommandRegistration")).map((cmd) => cmd.instance))
|
||||
|
||||
await openticket.events.get("afterSlashCommandsRegistered").emit([openticket.client.slashCommands,openticket.client])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user