@sapphire/framework
Version:
Discord bot framework built for advanced and amazing bots.
412 lines (410 loc) • 18.1 kB
JavaScript
import { __name } from '../../../chunk-PAWJFY3S.mjs';
import { container } from '@sapphire/pieces';
import { isNullishOrEmpty } from '@sapphire/utilities';
import { ApplicationCommandType } from 'discord-api-types/v10';
import { Collection } from 'discord.js';
import { InternalRegistryAPIType, RegisterBehavior } from '../../types/Enums.mjs';
import { getDefaultBehaviorWhenNotIdentical, getDefaultGuildIds, allGuildIdsToFetchCommandsFor } from './ApplicationCommandRegistries.mjs';
import { getCommandDifferences, getCommandDifferencesFast } from './computeDifferences.mjs';
import { normalizeChatInputCommand, normalizeContextMenuCommand, convertApplicationCommandToApiData } from './normalizeInputs.mjs';
var _ApplicationCommandRegistry = class _ApplicationCommandRegistry {
constructor(commandName) {
/**
* A set of all chat input command names and ids that point to this registry.
* You should not use this field directly, but instead use {@link ApplicationCommandRegistry.globalChatInputCommandIds}
*/
this.chatInputCommands = /* @__PURE__ */ new Set();
/**
* A set of all context menu command names and ids that point to this registry.
* You should not use this field directly, but instead use {@link ApplicationCommandRegistry.globalContextMenuCommandIds}
*/
this.contextMenuCommands = /* @__PURE__ */ new Set();
/**
* The guild ids that we need to fetch the commands for.
*/
this.guildIdsToFetch = /* @__PURE__ */ new Set();
/**
* The global slash command id for this command.
* @deprecated This field will only show the first global command id registered for this registry.
* Use {@link ApplicationCommandRegistry.globalChatInputCommandIds} instead.
*/
this.globalCommandId = null;
/**
* A set of all registered and valid global chat input command ids that point to this registry.
*/
this.globalChatInputCommandIds = /* @__PURE__ */ new Set();
/**
* A set of all registered and valid global context menu command ids that point to this registry.
*/
this.globalContextMenuCommandIds = /* @__PURE__ */ new Set();
/**
* The guild command ids for this command.
* @deprecated This field will only show the first guild command id registered for this registry per guild.
* Use {@link ApplicationCommandRegistry.guildIdToChatInputCommandIds} and {@link ApplicationCommandRegistry.guildIdToContextMenuCommandIds} instead.
*/
this.guildCommandIds = new Collection();
/**
* A map of guild ids to a set of registered and valid chat input command ids that point to this registry.
*/
this.guildIdToChatInputCommandIds = new Collection();
/**
* A map of guild ids to a set of registered and valid context menu command ids that point to this registry.
*/
this.guildIdToContextMenuCommandIds = new Collection();
this.apiCalls = [];
this.commandName = commandName;
}
get command() {
return container.stores.get("commands").get(this.commandName);
}
registerChatInputCommand(command, options) {
const builtData = normalizeChatInputCommand(command);
this.chatInputCommands.add(builtData.name);
const guildIdsToRegister = this.getGuildIdsToRegister(options);
const registerOptions = {
registerCommandIfMissing: true,
behaviorWhenNotIdentical: getDefaultBehaviorWhenNotIdentical(),
guildIds: guildIdsToRegister,
...options ?? {}
};
this.apiCalls.push({
builtData,
registerOptions,
type: InternalRegistryAPIType.ChatInput
});
if (options?.idHints) {
for (const hint of options.idHints) {
this.chatInputCommands.add(hint);
}
}
this.processGuildIds(guildIdsToRegister);
return this;
}
registerContextMenuCommand(command, options) {
const builtData = normalizeContextMenuCommand(command);
this.contextMenuCommands.add(builtData.name);
const guildIdsToRegister = this.getGuildIdsToRegister(options);
const registerOptions = {
registerCommandIfMissing: true,
behaviorWhenNotIdentical: getDefaultBehaviorWhenNotIdentical(),
guildIds: guildIdsToRegister,
...options ?? {}
};
this.apiCalls.push({
builtData,
registerOptions,
type: InternalRegistryAPIType.ContextMenu
});
if (options?.idHints) {
for (const hint of options.idHints) {
this.contextMenuCommands.add(hint);
}
}
this.processGuildIds(guildIdsToRegister);
return this;
}
addChatInputCommandNames(...names) {
const flattened = names.flat(Infinity);
for (const command of flattened) {
this.debug(`Registering name "${command}" to internal chat input map`);
this.warn(
`Registering the chat input command "${command}" using a name is not recommended.`,
'Please use the "addChatInputCommandIds" method instead with a command id.'
);
this.chatInputCommands.add(command);
}
return this;
}
addContextMenuCommandNames(...names) {
const flattened = names.flat(Infinity);
for (const command of flattened) {
this.debug(`Registering name "${command}" to internal context menu map`);
this.warn(
`Registering the context menu command "${command}" using a name is not recommended.`,
'Please use the "addContextMenuCommandIds" method instead with a command id.'
);
this.contextMenuCommands.add(command);
}
return this;
}
addChatInputCommandIds(...commandIds) {
const flattened = commandIds.flat(Infinity);
for (const entry of flattened) {
try {
BigInt(entry);
this.debug(`Registering id "${entry}" to internal chat input map`);
} catch {
this.debug(`Registering name "${entry}" to internal chat input map`);
this.warn(
`Registering the chat input command "${entry}" using a name *and* trying to bypass this warning by calling "addChatInputCommandIds" is not recommended.`,
'Please use the "addChatInputCommandIds" method with a valid command id instead.'
);
}
this.chatInputCommands.add(entry);
}
return this;
}
addContextMenuCommandIds(...commandIds) {
const flattened = commandIds.flat(Infinity);
for (const entry of flattened) {
try {
BigInt(entry);
this.debug(`Registering id "${entry}" to internal context menu map`);
} catch {
this.debug(`Registering name "${entry}" to internal context menu map`);
this.warn(
`Registering the context menu command "${entry}" using a name *and* trying to bypass this warning by calling "addContextMenuCommandIds" is not recommended.`,
'Please use the "addContextMenuCommandIds" method with a valid command id instead.'
);
}
this.contextMenuCommands.add(entry);
}
return this;
}
async runAPICalls(applicationCommands, globalCommands, guildCommands) {
if (this.apiCalls.length === 0) {
this.trace("No API calls to run, and no command to register");
return;
}
if (getDefaultBehaviorWhenNotIdentical() === RegisterBehavior.BulkOverwrite) {
throw new RangeError(
`"runAPICalls" was called for "${this.commandName}" but the defaultBehaviorWhenNotIdentical is "BulkOverwrite". This should not happen.`
);
}
this.debug(`Preparing to process ${this.apiCalls.length} possible command registrations / updates...`);
const results = await Promise.allSettled(
this.apiCalls.map((call) => this.handleAPICall(applicationCommands, globalCommands, guildCommands, call))
);
const errored = results.filter((result) => result.status === "rejected");
if (errored.length) {
this.error(`Received ${errored.length} errors while processing command registrations / updates`);
for (const error of errored) {
this.error(error.reason.stack ?? error.reason);
}
}
}
handleIdAddition(type, id, guildId) {
switch (type) {
case InternalRegistryAPIType.ChatInput: {
this.addChatInputCommandIds(id);
if (guildId) {
this.guildIdToChatInputCommandIds.ensure(guildId, () => /* @__PURE__ */ new Set()).add(id);
} else {
this.globalChatInputCommandIds.add(id);
}
break;
}
case InternalRegistryAPIType.ContextMenu: {
this.addContextMenuCommandIds(id);
if (guildId) {
this.guildIdToContextMenuCommandIds.ensure(guildId, () => /* @__PURE__ */ new Set()).add(id);
} else {
this.globalContextMenuCommandIds.add(id);
}
break;
}
}
if (guildId) {
if (!this.guildCommandIds.has(guildId)) {
this.guildCommandIds.set(guildId, id);
}
} else {
this.globalCommandId ??= id;
}
}
getGuildIdsToRegister(options) {
let guildIdsToRegister = void 0;
if (!isNullishOrEmpty(options?.guildIds)) {
guildIdsToRegister = options.guildIds;
} else if (!isNullishOrEmpty(getDefaultGuildIds())) {
guildIdsToRegister = getDefaultGuildIds();
}
return guildIdsToRegister;
}
processGuildIds(guildIdsToRegister) {
if (!isNullishOrEmpty(guildIdsToRegister)) {
for (const id of guildIdsToRegister) {
this.guildIdsToFetch.add(id);
allGuildIdsToFetchCommandsFor.add(id);
}
}
}
async handleAPICall(commandsManager, globalCommands, allGuildsCommands, apiCall) {
const { builtData, registerOptions } = apiCall;
const commandName = builtData.name;
const behaviorIfNotEqual = registerOptions.behaviorWhenNotIdentical ?? getDefaultBehaviorWhenNotIdentical();
const findCallback = /* @__PURE__ */ __name((entry) => {
if (apiCall.type === InternalRegistryAPIType.ChatInput && entry.type !== ApplicationCommandType.ChatInput) return false;
if (apiCall.type === InternalRegistryAPIType.ContextMenu) {
if (entry.type === ApplicationCommandType.ChatInput) return false;
if (apiCall.builtData.type !== entry.type) return false;
}
const isInIdHint = registerOptions.idHints?.includes(entry.id);
return typeof isInIdHint === "boolean" ? isInIdHint || entry.name === commandName : entry.name === commandName;
}, "findCallback");
let type;
switch (apiCall.type) {
case InternalRegistryAPIType.ChatInput:
type = "chat input";
break;
case InternalRegistryAPIType.ContextMenu:
switch (apiCall.builtData.type) {
case ApplicationCommandType.Message:
type = "message context menu";
break;
case ApplicationCommandType.User:
type = "user context menu";
break;
default:
type = "unknown-type context menu";
}
break;
default:
type = "unknown";
}
if (!registerOptions.guildIds?.length) {
const globalCommand = globalCommands.find(findCallback);
if (globalCommand) {
this.debug(`Checking if command "${commandName}" is identical with global ${type} command with id "${globalCommand.id}"`);
this.handleIdAddition(apiCall.type, globalCommand.id);
await this.handleCommandPresent(globalCommand, builtData, behaviorIfNotEqual, null);
} else if (registerOptions.registerCommandIfMissing ?? true) {
this.debug(`Creating new global ${type} command with name "${commandName}"`);
await this.createMissingCommand(commandsManager, builtData, type);
} else {
this.debug(`Doing nothing about missing global ${type} command with name "${commandName}"`);
}
return;
}
for (const guildId of registerOptions.guildIds) {
const guildCommands = allGuildsCommands.get(guildId);
if (!guildCommands) {
this.debug(`There are no commands for guild with id "${guildId}". Will create ${type} command "${commandName}".`);
await this.createMissingCommand(commandsManager, builtData, type, guildId);
continue;
}
const existingGuildCommand = guildCommands.find(findCallback);
if (existingGuildCommand) {
this.debug(`Checking if guild ${type} command "${commandName}" is identical to command "${existingGuildCommand.id}"`);
this.handleIdAddition(apiCall.type, existingGuildCommand.id, guildId);
await this.handleCommandPresent(existingGuildCommand, builtData, behaviorIfNotEqual, guildId);
} else if (registerOptions.registerCommandIfMissing ?? true) {
this.debug(`Creating new guild ${type} command with name "${commandName}" for guild "${guildId}"`);
await this.createMissingCommand(commandsManager, builtData, type, guildId);
} else {
this.debug(`Doing nothing about missing guild ${type} command with name "${commandName}" for guild "${guildId}"`);
}
}
}
async handleCommandPresent(applicationCommand, apiData, behaviorIfNotEqual, guildId) {
if (behaviorIfNotEqual === RegisterBehavior.BulkOverwrite) {
this.debug(
`Command "${this.commandName}" has the behaviorIfNotEqual set to "BulkOverwrite" which is invalid. Using defaultBehaviorWhenNotIdentical instead`
);
behaviorIfNotEqual = getDefaultBehaviorWhenNotIdentical();
if (behaviorIfNotEqual === RegisterBehavior.BulkOverwrite) {
throw new Error(
`Invalid behaviorIfNotEqual value ("BulkOverwrite") for command "${this.commandName}", and defaultBehaviorWhenNotIdentical is also "BulkOverwrite". This should not happen.`
);
}
}
let differences = [];
if (behaviorIfNotEqual === RegisterBehavior.VerboseOverwrite) {
const now = Date.now();
differences = [...getCommandDifferences(convertApplicationCommandToApiData(applicationCommand), apiData, guildId !== null)];
const later = Date.now() - now;
this.debug(`Took ${later}ms to process differences via computing differences`);
if (!differences.length) {
this.debug(
`${guildId ? "Guild command" : "Command"} "${apiData.name}" is identical to command "${applicationCommand.name}" (${applicationCommand.id})`
);
return;
}
}
if (behaviorIfNotEqual === RegisterBehavior.Overwrite || behaviorIfNotEqual === RegisterBehavior.LogToConsole) {
const now = Date.now();
const areThereDifferences = getCommandDifferencesFast(convertApplicationCommandToApiData(applicationCommand), apiData, guildId !== null);
const later = Date.now() - now;
this.debug(`Took ${later}ms to process differences via fast compute differences`);
if (!areThereDifferences) {
this.debug(
`${guildId ? "Guild command" : "Command"} "${apiData.name}" is identical to command "${applicationCommand.name}" (${applicationCommand.id})`
);
return;
}
}
this.logCommandDifferencesFound(applicationCommand, behaviorIfNotEqual === RegisterBehavior.LogToConsole, differences);
if (behaviorIfNotEqual === RegisterBehavior.LogToConsole) {
return;
}
try {
await applicationCommand.edit(apiData);
this.debug(`Updated command ${applicationCommand.name} (${applicationCommand.id}) with new api data`);
} catch (error) {
this.error(`Failed to update command ${applicationCommand.name} (${applicationCommand.id})`, error);
}
}
logCommandDifferencesFound(applicationCommand, logAsWarn, differences) {
const finalMessage = [];
const pad = " ".repeat(5);
for (const difference of differences) {
finalMessage.push(
[
`\u2514\u2500\u2500 At path: ${difference.key}`,
//
`${pad}\u251C\u2500\u2500 Received: ${difference.original}`,
`${pad}\u2514\u2500\u2500 Expected: ${difference.expected}`,
""
].join("\n")
);
}
const finalMessageNewLine = finalMessage.length ? "\n" : "";
const header = `Found differences for command "${applicationCommand.name}" (${applicationCommand.id}) versus provided api data.${finalMessageNewLine}`;
logAsWarn ? this.warn(header, ...finalMessage) : this.debug(header, ...finalMessage);
}
async createMissingCommand(commandsManager, apiData, type, guildId) {
try {
const result = await commandsManager.create(apiData, guildId);
this.info(
`Successfully created ${type}${guildId ? " guild" : ""} command "${apiData.name}" with id "${result.id}". You should add the id to the "idHints" property of the register method you used!`
);
switch (apiData.type) {
case void 0:
case ApplicationCommandType.ChatInput: {
this.handleIdAddition(InternalRegistryAPIType.ChatInput, result.id, guildId);
break;
}
case ApplicationCommandType.Message:
case ApplicationCommandType.User: {
this.handleIdAddition(InternalRegistryAPIType.ContextMenu, result.id, guildId);
break;
}
}
} catch (err) {
this.error(
`Failed to register${guildId ? " guild" : ""} application command with name "${apiData.name}"${guildId ? ` for guild "${guildId}"` : ""}`,
err
);
}
}
info(message, ...other) {
container.logger.info(`ApplicationCommandRegistry[${this.commandName}] ${message}`, ...other);
}
error(message, ...other) {
container.logger.error(`ApplicationCommandRegistry[${this.commandName}] ${message}`, ...other);
}
warn(message, ...other) {
container.logger.warn(`ApplicationCommandRegistry[${this.commandName}] ${message}`, ...other);
}
debug(message, ...other) {
container.logger.debug(`ApplicationCommandRegistry[${this.commandName}] ${message}`, ...other);
}
trace(message, ...other) {
container.logger.trace(`ApplicationCommandRegistry[${this.commandName}] ${message}`, ...other);
}
};
__name(_ApplicationCommandRegistry, "ApplicationCommandRegistry");
var ApplicationCommandRegistry = _ApplicationCommandRegistry;
export { ApplicationCommandRegistry };
//# sourceMappingURL=ApplicationCommandRegistry.mjs.map
//# sourceMappingURL=ApplicationCommandRegistry.mjs.map