From 2ea38cd7a52d4c4affe2ad63d09b61af5af75111 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Fri, 20 Jun 2025 18:20:27 +0200 Subject: [PATCH] (Open Discord) Added support for context menu's --- src/core/api/defaults/client.ts | 47 ++- src/core/api/defaults/event.ts | 8 +- src/core/api/defaults/progressbar.ts | 3 + src/core/api/modules/client.ts | 391 +++++++++++++++++++++++- src/core/api/modules/defaults.ts | 12 + src/data/framework/commandLoader.ts | 9 + src/data/framework/eventLoader.ts | 6 + src/data/framework/progressBarLoader.ts | 9 + src/index.ts | 32 ++ 9 files changed, 513 insertions(+), 4 deletions(-) diff --git a/src/core/api/defaults/client.ts b/src/core/api/defaults/client.ts index f9b8e50..620e4ef 100644 --- a/src/core/api/defaults/client.ts +++ b/src/core/api/defaults/client.ts @@ -2,7 +2,7 @@ //DEFAULT CLIENT MODULE /////////////////////////////////////// import { ODValidId } from "../modules/base" -import { ODClientManager, ODSlashCommand, ODTextCommand, ODSlashCommandManager, ODTextCommandManager, ODSlashCommandInteractionCallback, ODTextCommandInteractionCallback } from "../modules/client" +import { ODClientManager, ODSlashCommand, ODTextCommand, ODSlashCommandManager, ODTextCommandManager, ODSlashCommandInteractionCallback, ODTextCommandInteractionCallback, ODContextMenu, ODContextMenuManager, ODContextMenuInteractionCallback } from "../modules/client" /** (CONTRIBUTOR GUIDE) HOW TO ADD NEW COMMANDS? * - Register the command in loadAllSlashCommands() & loadAllTextCommands() in (./src/data/framework/commandLoader.ts) @@ -23,6 +23,7 @@ import { ODClientManager, ODSlashCommand, ODTextCommand, ODSlashCommandManager, export class ODClientManager_Default extends ODClientManager { declare slashCommands: ODSlashCommandManager_Default declare textCommands: ODTextCommandManager_Default + declare contextMenus: ODContextMenuManager_Default } /**## ODSlashCommandManagerIds_Default `interface` @@ -152,4 +153,48 @@ export class ODTextCommandManager_Default extends ODTextCommandManager { onInteraction(commandPrefix:string, commandName:string|RegExp, callback:ODTextCommandInteractionCallback): void { return super.onInteraction(commandPrefix,commandName,callback) } +} + +/**## ODContextMenuManagerIds_Default `interface` + * This interface is a list of ids available in the `ODContextMenuManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODContextMenuManagerIds_Default { + "opendiscord:test-menu":ODContextMenu +} + +/**## ODContextMenuManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODContextMenuManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `opendiscord.client.contextMenus`! + */ +export class ODContextMenuManager_Default extends ODContextMenuManager { + get(id:ContextMenuId): ODContextMenuManagerIds_Default[ContextMenuId] + get(id:ODValidId): ODContextMenu|null + + get(id:ODValidId): ODContextMenu|null { + return super.get(id) + } + + remove(id:ContextMenuId): ODContextMenuManagerIds_Default[ContextMenuId] + remove(id:ODValidId): ODContextMenu|null + + remove(id:ODValidId): ODContextMenu|null { + return super.remove(id) + } + + exists(id:keyof ODContextMenuManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } + + onInteraction(menuName:keyof ODContextMenuManagerIds_Default, callback:ODContextMenuInteractionCallback): void + onInteraction(menuName:string|RegExp, callback:ODContextMenuInteractionCallback): void + + onInteraction(menuName:string|RegExp, callback:ODContextMenuInteractionCallback): void { + return super.onInteraction(menuName,callback) + } } \ No newline at end of file diff --git a/src/core/api/defaults/event.ts b/src/core/api/defaults/event.ts index 1833156..1ea6607 100644 --- a/src/core/api/defaults/event.ts +++ b/src/core/api/defaults/event.ts @@ -19,7 +19,7 @@ import { ODFlagManager_Default } from "./flag" import { ODSessionManager_Default } from "./session" import { ODLanguageManager_Default } from "./language" import { ODCheckerFunctionManager_Default, ODCheckerManager_Default, ODCheckerRenderer_Default, ODCheckerTranslationRegister_Default } from "./checker" -import { ODClientManager_Default, ODSlashCommandManager_Default, ODTextCommandManager_Default } from "./client" +import { ODClientManager_Default, ODContextMenuManager_Default, ODSlashCommandManager_Default, ODTextCommandManager_Default } from "./client" import { ODBuilderManager_Default, ODButtonManager_Default, ODDropdownManager_Default, ODEmbedManager_Default, ODFileManager_Default, ODMessageManager_Default, ODModalManager_Default } from "./builder" import { ODButtonResponderManager_Default, ODCommandResponderManager_Default, ODDropdownResponderManager_Default, ODModalResponderManager_Default, ODResponderManager_Default } from "./responder" import { ODActionManager_Default } from "./action" @@ -128,6 +128,12 @@ export interface ODEventIds_Default { "onSlashCommandRegister": ODEvent_Default<(slash:ODSlashCommandManager_Default, client:ODClientManager_Default) => ODPromiseVoid> "afterSlashCommandsRegistered": ODEvent_Default<(slash:ODSlashCommandManager_Default, client:ODClientManager_Default) => ODPromiseVoid> + //client context menus + "onContextMenuLoad": ODEvent_Default<(menu:ODContextMenuManager_Default, client:ODClientManager_Default) => ODPromiseVoid> + "afterContextMenusLoaded": ODEvent_Default<(menu:ODContextMenuManager_Default, client:ODClientManager_Default) => ODPromiseVoid> + "onContextMenuRegister": ODEvent_Default<(menu:ODContextMenuManager_Default, client:ODClientManager_Default) => ODPromiseVoid> + "afterContextMenusRegistered": ODEvent_Default<(menu:ODContextMenuManager_Default, client:ODClientManager_Default) => ODPromiseVoid> + //client text commands "onTextCommandLoad": ODEvent_Default<(text:ODTextCommandManager_Default, client:ODClientManager_Default,) => ODPromiseVoid> "afterTextCommandsLoaded": ODEvent_Default<(text:ODTextCommandManager_Default, client:ODClientManager_Default) => ODPromiseVoid> diff --git a/src/core/api/defaults/progressbar.ts b/src/core/api/defaults/progressbar.ts index 2d1b3f1..f353472 100644 --- a/src/core/api/defaults/progressbar.ts +++ b/src/core/api/defaults/progressbar.ts @@ -134,6 +134,9 @@ export interface ODProgressBarManagerIds_Default { "opendiscord:slash-command-remove":ODManualProgressBar, "opendiscord:slash-command-create":ODManualProgressBar, "opendiscord:slash-command-update":ODManualProgressBar, + "opendiscord:context-menu-remove":ODManualProgressBar, + "opendiscord:context-menu-create":ODManualProgressBar, + "opendiscord:context-menu-update":ODManualProgressBar, } /**## ODProgressBarManager_Default `default_class` diff --git a/src/core/api/modules/client.ts b/src/core/api/modules/client.ts index 9d0ea07..456248a 100644 --- a/src/core/api/modules/client.ts +++ b/src/core/api/modules/client.ts @@ -76,12 +76,15 @@ export class ODClientManager { slashCommands: ODSlashCommandManager /**The text command manager is responsible for all text commands & their events inside the bot. */ textCommands: ODTextCommandManager + /**The context menu manager is responsible for all context menus & their events inside the bot. */ + contextMenus: ODContextMenuManager constructor(debug:ODDebugger){ this.#debug = debug this.activity = new ODClientActivityManager(this.#debug,this) this.slashCommands = new ODSlashCommandManager(this.#debug,this) this.textCommands = new ODTextCommandManager(this.#debug,this) + this.contextMenus = new ODContextMenuManager(this.#debug,this) } /**Initiate the `client` variable & add the intents & partials to the bot. */ @@ -145,9 +148,10 @@ export class ODClientManager { this.client.once("ready",async () => { this.ready = true - //set slashCommandManager to client applicationCommandManager - if (!this.client.application) throw new ODSystemError("Couldn't get client application! Unable to register slash commands!") + //set slashCommandManager & contextMenuManager to client applicationCommandManager + if (!this.client.application) throw new ODSystemError("Couldn't get client application for slashCommand & contextMenu managers!") this.slashCommands.commandManager = this.client.application.commands + this.contextMenus.commandManager = this.client.application.commands if (this.readyListener) await this.readyListener() resolve(true) @@ -522,6 +526,9 @@ export interface ODSlashCommandBuilder extends discord.ChatInputApplicationComma contexts:discord.InteractionContextType[] } +/**## ODSlashCommandComparator `class` + * A utility class to compare existing slash commands with newly registered ones. + */ export class ODSlashCommandComparator { /**Convert a `discord.ApplicationCommandOptionChoiceData` to a universal Open Ticket slash command option choice object for comparison. */ #convertOptionChoice(choice:discord.ApplicationCommandOptionChoiceData): ODSlashCommandUniversalOptionChoice { @@ -1788,4 +1795,384 @@ export class ODTextCommandManager extends ODManager { 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!") else return super.add(data,overwrite) } +} + +/**## ODContextMenuUniversalMenu `interface` + * A universal template for a context menu. + * + * Why universal? Both **existing context menus** & **unregistered templates** can be converted to this type. + */ +export interface ODContextMenuUniversalMenu { + /**The type of this context menu. (required => `Message`|`User`) */ + type:discord.ApplicationCommandType.Message|discord.ApplicationCommandType.User, + /**The name of this context menu. */ + name:string, + /**All localized names of this context menu. */ + nameLocalizations:readonly ODSlashCommandUniversalTranslation[], + /**The id of the guild this context menu is registered in. */ + guildId:string|null, + /**Is this context menu for 18+ users only? */ + nsfw:boolean, + /**A bitfield of the user permissions required to use this context menu. */ + defaultMemberPermissions:bigint, + /**Is this context menu available in DM? */ + dmPermission:boolean, + /**A list of contexts where you can install this context menu. */ + integrationTypes:readonly discord.ApplicationIntegrationType[], + /**A list of contexts where you can use this context menu. */ + contexts:readonly discord.InteractionContextType[] +} + +/**## ODContextMenuBuilderMessage `interface` + * The builder for message context menus. + */ +export interface ODContextMenuBuilderMessage extends discord.MessageApplicationCommandData { + /**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[] +} + +/**## ODContextMenuBuilderUser `interface` + * The builder for user context menus. + */ +export interface ODContextMenuBuilderUser extends discord.UserApplicationCommandData { + /**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[] +} + +/**## ODContextMenuBuilderUser `interface` + * The builder for context menus. + */ +export type ODContextMenuBuilder = (ODContextMenuBuilderMessage|ODContextMenuBuilderUser) + +/**## ODContextMenuComparator `class` + * A utility class to compare existing context menu's with newly registered ones. + */ +export class ODContextMenuComparator { + /**Convert a `ODContextMenuBuilder` to a universal Open Ticket context menu object for comparison. */ + convertBuilder(builder:ODContextMenuBuilder,guildId:string|null): ODContextMenuUniversalMenu|null { + if (builder.type != discord.ApplicationCommandType.Message && builder.type != discord.ApplicationCommandType.User) return null + const nameLoc = builder.nameLocalizations ?? {} + + return { + type:builder.type, + name:builder.name, + nameLocalizations:Object.keys(nameLoc).map((key) => {return {language:key as `${discord.Locale}`,value:nameLoc[key]}}), + guildId:guildId, + nsfw:builder.nsfw ?? false, + 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 context menu object for comparison. */ + convertMenu(cmd:discord.ApplicationCommand): ODContextMenuUniversalMenu|null { + if (cmd.type != discord.ApplicationCommandType.Message && cmd.type != discord.ApplicationCommandType.User) return null + const nameLoc = cmd.nameLocalizations ?? {} + + return { + type:cmd.type, + name:cmd.name, + nameLocalizations:Object.keys(nameLoc).map((key) => {return {language:key as `${discord.Locale}`,value:nameLoc[key]}}), + guildId:cmd.guildId, + nsfw:cmd.nsfw, + 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 context menus are the same. */ + compare(ctxA:ODContextMenuUniversalMenu,ctxB:ODContextMenuUniversalMenu): boolean { + if (ctxA.name != ctxB.name) return false + if (ctxA.type != ctxB.type) return false + if (ctxA.nsfw != ctxB.nsfw) return false + if (ctxA.guildId != ctxB.guildId) return false + if (ctxA.dmPermission != ctxB.dmPermission) return false + if (ctxA.defaultMemberPermissions != ctxB.defaultMemberPermissions) return false + + //nameLocalizations + if (ctxA.nameLocalizations.length != ctxB.nameLocalizations.length) return false + if (!ctxA.nameLocalizations.every((nameA) => { + const nameB = ctxB.nameLocalizations.find((nameB) => nameB.language == nameA.language) + if (!nameB || nameA.value != nameB.value) return false + else return true + })) return false + + //contexts + if (ctxA.contexts.length != ctxB.contexts.length) return false + if (!ctxA.contexts.every((contextA) => { + return ctxB.contexts.includes(contextA) + })) return false + + //integrationTypes + if (ctxA.integrationTypes.length != ctxB.integrationTypes.length) return false + if (!ctxA.integrationTypes.every((integrationA) => { + return ctxB.integrationTypes.includes(integrationA) + })) return false + + return true + } +} + +/**## ODContextMenuInteractionCallback `type` + * Callback for the message context menu interaction listener. + */ +export type ODContextMenuInteractionCallback = (interaction:discord.ContextMenuCommandInteraction,cmd:ODContextMenu) => void + +/**## ODContextMenuRegisteredResult `type` + * The result which will be returned when getting all (un)registered user context menu's from the manager. + */ +export type ODContextMenuRegisteredResult = { + /**A list of all registered message context menus. */ + registered:{ + /**The instance (`ODContextMenu`) from this message context menu. */ + instance:ODContextMenu, + /**The universal object/template/builder of this message context menu. */ + menu:ODContextMenuUniversalMenu, + /**Does this message context menu require an update? */ + requiresUpdate:boolean + }[], + /**A list of all unregistered message context menus. */ + unregistered:{ + /**The instance (`ODContextMenu`) from this message context menu. */ + instance:ODContextMenu, + /**The universal object/template/builder of this message context menu. */ + menu:null, + /**Does this message context menu require an update? */ + requiresUpdate:true + }[], + /**A list of all unused message context menus (not found in `ODContextMenuManager`). */ + unused:{ + /**The instance (`ODContextMenu`) from this message context menu. */ + instance:null, + /**The universal object/template/builder of this message context menu. */ + menu:ODContextMenuUniversalMenu, + /**Does this context menu require an update? */ + requiresUpdate:false + }[] +} + +/**## ODContextMenuManager `class` + * This is an Open Ticket client message context menu manager. + * + * It's responsible for managing all the message context interactions from the client. + * + * Here, you can add & remove message context interactions & the bot will do the (de)registering. + */ +export class ODContextMenuManager extends ODManager { + /**Alias to Open Ticket debugger. */ + #debug: ODDebugger + + /**Refrerence to discord.js client. */ + manager: ODClientManager + /**Discord.js application commands manager. */ + commandManager: discord.ApplicationCommandManager|null + /**Collection of all interaction listeners. */ + #interactionListeners: {name:string|RegExp, callback:ODContextMenuInteractionCallback}[] = [] + /**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 context menus with each other. */ + comparator: ODContextMenuComparator = new ODContextMenuComparator() + + constructor(debug:ODDebugger, manager:ODClientManager){ + super(debug,"context menu") + this.#debug = debug + this.manager = manager + this.commandManager = (manager.client.application) ? manager.client.application.commands : null + } + + /**Get all registered & unregistered message context menu commands. */ + async getAllRegisteredMenus(guildId?:string): Promise { + if (!this.commandManager) throw new ODSystemError("Couldn't get client application to register message context menus!") + + const menus = (await this.commandManager.fetch({guildId})).toJSON() + const registered: {instance:ODContextMenu, menu:ODContextMenuUniversalMenu, requiresUpdate:boolean}[] = [] + const unregistered: {instance:ODContextMenu, menu:null, requiresUpdate:true}[] = [] + const unused: {instance:null, menu:ODContextMenuUniversalMenu, requiresUpdate:false}[] = [] + + await this.loopAll((instance) => { + if (guildId && instance.guildId != guildId) return + + const index = menus.findIndex((menu) => menu.name == instance.name) + const menu = menus[index] + menus.splice(index,1) + if (menu){ + //menu is registered (and may need to be updated) + const universalBuilder = this.comparator.convertBuilder(instance.builder,instance.guildId) + const universalMenu = this.comparator.convertMenu(menu) + + //menu is not of the type 'message'|'user' + if (!universalBuilder || !universalMenu) return + + const didChange = !this.comparator.compare(universalBuilder,universalMenu) + const requiresUpdate = didChange || (instance.requiresUpdate ? instance.requiresUpdate(universalMenu) : false) + registered.push({instance,menu:universalMenu,requiresUpdate}) + + //menu is not registered + }else unregistered.push({instance,menu:null,requiresUpdate:true}) + }) + + menus.forEach((menu) => { + //menu does not exist in the manager (only append to unused when type == 'message'|'user') + const universalCmd = this.comparator.convertMenu(menu) + if (!universalCmd) return + unused.push({instance:null,menu:universalCmd,requiresUpdate:false}) + }) + + return {registered,unregistered,unused} + } + /**Create all context menus that are not registered yet.*/ + async createNewMenus(instances:ODContextMenu[],progress?:ODManualProgressBar){ + if (!this.manager.ready) throw new ODSystemError("Client isn't ready yet! Unable to register context menus!") + if (instances.length > 0 && progress){ + progress.max = instances.length + progress.start() + } + + for (const instance of instances){ + await this.createMenu(instance) + this.#debug.debug("Created new context menu",[ + {key:"id",value:instance.id.value}, + {key:"name",value:instance.name}, + {key:"type",value:(instance.builder.type == discord.ApplicationCommandType.Message) ? "message-context" : "user-context"} + ]) + if (progress) progress.increase(1) + } + } + /**Update all context menus that are already registered. */ + async updateExistingMenus(instances:ODContextMenu[],progress?:ODManualProgressBar){ + if (!this.manager.ready) throw new ODSystemError("Client isn't ready yet! Unable to register context menus!") + if (instances.length > 0 && progress){ + progress.max = instances.length + progress.start() + } + + for (const instance of instances){ + await this.createMenu(instance) + this.#debug.debug("Updated existing context menu",[ + {key:"id",value:instance.id.value}, + {key:"name",value:instance.name}, + {key:"type",value:(instance.builder.type == discord.ApplicationCommandType.Message) ? "message-context" : "user-context"} + ]) + if (progress) progress.increase(1) + } + } + /**Remove all context menus that are registered but unused by Open Ticket. */ + async removeUnusedMenus(instances:ODContextMenuUniversalMenu[],guildId?:string,progress?:ODManualProgressBar){ + if (!this.manager.ready) throw new ODSystemError("Client isn't ready yet! Unable to register context menus!") + if (!this.commandManager) throw new ODSystemError("Couldn't get client application to register context menus!") + if (instances.length > 0 && progress){ + progress.max = instances.length + progress.start() + } + + const menus = await this.commandManager.fetch({guildId}) + + for (const instance of instances){ + const menu = menus.find((menu) => menu.name == instance.name) + if (menu){ + try { + await menu.delete() + this.#debug.debug("Removed existing context menu",[ + {key:"name",value:menu.name}, + {key:"guildId",value:guildId ?? "/"}, + {key:"type",value:(instance.type == discord.ApplicationCommandType.Message) ? "message-context" : "user-context"} + ]) + }catch(err){ + process.emit("uncaughtException",err) + throw new ODSystemError("Failed to delete context menu '"+menu.name+"'!") + } + } + if (progress) progress.increase(1) + } + } + /**Create a context menu. **(SYSTEM ONLY)** => Use `ODContextMenuManager` for registering context menu's the default way! */ + async createMenu(menu:ODContextMenu){ + if (!this.commandManager) throw new ODSystemError("Couldn't get client application to register context menu's!") + try { + await this.commandManager.create(menu.builder,(menu.guildId ?? undefined)) + }catch(err){ + process.emit("uncaughtException",err) + throw new ODSystemError("Failed to register context menu '"+menu.name+"'!") + } + } + /**Start listening to the discord.js client `interactionCreate` event. */ + startListeningToInteractions(){ + this.manager.client.on("interactionCreate",(interaction) => { + //return when not in main server or DM + if (!this.manager.mainServer || (interaction.guild && interaction.guild.id != this.manager.mainServer.id)) return + + if (!interaction.isContextMenuCommand()) return + const menu = this.getFiltered((menu) => menu.name == interaction.commandName)[0] + if (!menu) return + + this.#interactionListeners.forEach((listener) => { + if (typeof listener.name == "string" && (interaction.commandName != listener.name)) return + else if (listener.name instanceof RegExp && !listener.name.test(interaction.commandName)) return + + //this is a valid listener + listener.callback(interaction,menu) + }) + }) + } + /**Callback on interaction from one or multiple context menu's. */ + onInteraction(menuName:string|RegExp, callback:ODContextMenuInteractionCallback){ + this.#interactionListeners.push({ + name:menuName, + callback + }) + + if (this.#interactionListeners.length > this.listenerLimit){ + this.#debug.console.log("Possible context menu interaction memory leak detected!","warning",[ + {key:"listeners",value:this.#interactionListeners.length.toString()} + ]) + } + } +} + +/**## ODContextMenuUpdateFunction `type` + * The function responsible for updating context menu's when they already exist. + */ +export type ODContextMenuUpdateFunction = (menu:ODContextMenuUniversalMenu) => boolean + +/**## ODContextMenu `class` + * This is an Open Ticket context menu. + * + * When registered, you can listen for this context menu using the `ODContextResponder`. The advantages of using this class for creating a context menu are: + * - automatic registration in discord.js + * - error reporting to the user when the bot fails to respond + * - plugins can extend this context menu + * - the bot won't re-register the context menu when it already exists (except when requested)! + * + * And more! + */ +export class ODContextMenu extends ODManagerData { + /**The discord.js builder for this context menu. */ + builder: ODContextMenuBuilder + /**The id of the guild this context menu is for. `null` when not set. */ + guildId: string|null + /**Function to check if the context menu requires to be updated (when it already exists). */ + requiresUpdate: ODContextMenuUpdateFunction|null = null + + constructor(id:ODValidId, builder:ODContextMenuBuilder, requiresUpdate?:ODContextMenuUpdateFunction, guildId?:string){ + super(id) + if (builder.type != discord.ApplicationCommandType.Message && builder.type != discord.ApplicationCommandType.User) throw new ODSystemError("ApplicationCommandData is required to be the 'Message'|'User' type!") + + this.builder = builder + this.guildId = guildId ?? null + this.requiresUpdate = requiresUpdate ?? null + } + + /**The name of this context menu. */ + get name(): string { + return this.builder.name + } + set name(name:string){ + this.builder.name = name + } } \ No newline at end of file diff --git a/src/core/api/modules/defaults.ts b/src/core/api/modules/defaults.ts index 63a9d68..02029fa 100644 --- a/src/core/api/modules/defaults.ts +++ b/src/core/api/modules/defaults.ts @@ -91,6 +91,14 @@ export interface ODDefaults { 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 context menus. */ + contextMenuLoading:boolean, + /**Load the default Open Ticket context menu registerer (register menus in discord). */ + contextMenuRegistering:boolean, + /**When enabled, the bot is forced to re-register all context menus in the server. This can be used in case of a auto-update malfunction. */ + forceContextMenuRegistration:boolean, + /**When enabled, the bot is allowed to unregister all context menus which aren't used in Open Ticket. Disable this if you don't want to use the Open Ticket `ODContextMenu` classes. */ + allowContextMenuRemoval:boolean, /**Load the default Open Ticket text commands. */ textCommandLoading:boolean, @@ -277,6 +285,10 @@ export class ODDefaultsManager { slashCommandRegistering:true, forceSlashCommandRegistration:false, allowSlashCommandRemoval:true, + contextMenuLoading:true, + contextMenuRegistering:true, + forceContextMenuRegistration:false, + allowContextMenuRemoval:true, textCommandLoading:true, questionLoading:true, diff --git a/src/data/framework/commandLoader.ts b/src/data/framework/commandLoader.ts index a854dbf..b24d09b 100644 --- a/src/data/framework/commandLoader.ts +++ b/src/data/framework/commandLoader.ts @@ -1094,4 +1094,13 @@ export const loadAllTextCommands = async () => { } ] })) +} + +export const loadAllContextMenus = async () => { + const menus = opendiscord.client.contextMenus + const generalConfig = opendiscord.configs.get("opendiscord:general") + if (!generalConfig) return + + const act = discord.ApplicationCommandType + if (!generalConfig.data.slashCommands) return } \ No newline at end of file diff --git a/src/data/framework/eventLoader.ts b/src/data/framework/eventLoader.ts index 2124a1e..6aa4111 100644 --- a/src/data/framework/eventLoader.ts +++ b/src/data/framework/eventLoader.ts @@ -82,6 +82,12 @@ export const loadAllEvents = () => { "onSlashCommandRegister", "afterSlashCommandsRegistered", + //client context menus + "onContextMenuLoad", + "afterContextMenusLoaded", + "onContextMenuRegister", + "afterContextMenusRegistered", + //client text commands "onTextCommandLoad", "afterTextCommandsLoaded", diff --git a/src/data/framework/progressBarLoader.ts b/src/data/framework/progressBarLoader.ts index 27795f4..63eacb1 100644 --- a/src/data/framework/progressBarLoader.ts +++ b/src/data/framework/progressBarLoader.ts @@ -64,4 +64,13 @@ export const loadAllProgressBars = async () => { //SLASH COMMAND UPDATE (doesn't have correct amount yet) opendiscord.progressbars.add(new api.ODManualProgressBar("opendiscord:slash-command-update",fractRenderer.withAdditionalSettings({filledBarColor:"openticket"}),0,"max",null,"Commands Updated")) + + //CONTEXT MENU REMOVE (doesn't have correct amount yet) + opendiscord.progressbars.add(new api.ODManualProgressBar("opendiscord:context-menu-remove",fractRenderer.withAdditionalSettings({filledBarColor:"red"}),0,"max",null,"Context Menus Removed")) + + //CONTEXT MENU CREATE (doesn't have correct amount yet) + opendiscord.progressbars.add(new api.ODManualProgressBar("opendiscord:context-menu-create",fractRenderer.withAdditionalSettings({filledBarColor:"green"}),0,"max",null,"Context Menus Created")) + + //CONTEXT MENU UPDATE (doesn't have correct amount yet) + opendiscord.progressbars.add(new api.ODManualProgressBar("opendiscord:context-menu-update",fractRenderer.withAdditionalSettings({filledBarColor:"openticket"}),0,"max",null,"Context Menus Updated")) } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 7f368f0..dbd42eb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -456,6 +456,38 @@ const main = async () => { await opendiscord.events.get("afterSlashCommandsRegistered").emit([opendiscord.client.slashCommands,opendiscord.client]) } + //load context menus + opendiscord.log("Loading context menus...","system") + if (opendiscord.defaults.getDefault("contextMenuLoading")){ + await (await import("./data/framework/commandLoader.js")).loadAllContextMenus() + } + await opendiscord.events.get("onContextMenuLoad").emit([opendiscord.client.contextMenus,opendiscord.client]) + await opendiscord.events.get("afterContextMenusLoaded").emit([opendiscord.client.contextMenus,opendiscord.client]) + + //register context menus (create, update & remove) + if (opendiscord.defaults.getDefault("forceContextMenuRegistration")) opendiscord.log("Forcing all context menus to be re-registered...","system") + opendiscord.log("Registering context menus... (this can take up to a minute)","system") + await opendiscord.events.get("onContextMenuRegister").emit([opendiscord.client.contextMenus,opendiscord.client]) + if (opendiscord.defaults.getDefault("contextMenuRegistering")){ + //get all context menus that are already registered in the bot + const menus = await opendiscord.client.contextMenus.getAllRegisteredMenus() + const removableMenus = menus.unused.map((menu) => menu.menu) + const newMenus = menus.unregistered.map((menu) => menu.instance) + const updatableMenus = menus.registered.filter((menu) => menu.requiresUpdate || opendiscord.defaults.getDefault("forceContextMenuRegistration")).map((menu) => menu.instance) + + //init progress bars + const removeProgress = opendiscord.progressbars.get("opendiscord:context-menu-remove") + const createProgress = opendiscord.progressbars.get("opendiscord:context-menu-create") + const updateProgress = opendiscord.progressbars.get("opendiscord:context-menu-update") + + //remove unused menus, create new menus & update existing menus + if (opendiscord.defaults.getDefault("allowContextMenuRemoval")) await opendiscord.client.contextMenus.removeUnusedMenus(removableMenus,undefined,removeProgress) + await opendiscord.client.contextMenus.createNewMenus(newMenus,createProgress) + await opendiscord.client.contextMenus.updateExistingMenus(updatableMenus,updateProgress) + + await opendiscord.events.get("afterContextMenusRegistered").emit([opendiscord.client.contextMenus,opendiscord.client]) + } + //load text commands opendiscord.log("Loading text commands...","system") if (opendiscord.defaults.getDefault("allowDumpCommand")){