iobroker.discord
Version:
ioBroker Discord bot integration
1,141 lines (1,140 loc) • 48.6 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var __decorateClass = (decorators, target, key, kind) => {
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
for (var i = decorators.length - 1, decorator; i >= 0; i--)
if (decorator = decorators[i])
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
if (kind && result) __defProp(target, key, result);
return result;
};
var commands_exports = {};
__export(commands_exports, {
DiscordAdapterSlashCommands: () => DiscordAdapterSlashCommands
});
module.exports = __toCommonJS(commands_exports);
var import_node_util = require("node:util");
var import_autobind_decorator = require("autobind-decorator");
var import_discord = require("discord.js");
var import_builders = require("@discordjs/builders");
var import_rest = require("@discordjs/rest");
var import_v10 = require("discord-api-types/v10");
var import_i18n = require("./lib/i18n");
var import_utils = require("./lib/utils");
class DiscordAdapterSlashCommands {
/**
* Reference to the adapter instance.
*/
adapter;
/**
* Discord REST api interface.
*/
rest = new import_rest.REST({ version: "10" });
/**
* Command name for the get state command.
*/
cmdGetStateName = "iob-get";
/**
* Command name for the set state command.
*/
cmdSetStateName = "iob-set";
/**
* Possible choices for get state command state-option.
*/
cmdGetStateChoices = [];
/**
* Possible choices for set state command state-option.
*/
cmdSetStateChoices = [];
/**
* Collection of the known (registered) custom commands and their option choices.
*/
customCommands = new import_discord.Collection();
/**
* If commands are fully registered including their permissions.
*/
registerCommandsDone = false;
/**
* The last registered commands.
* Used to check if something changed an we need to register the commands again.
*/
lastCommandsJson = null;
/**
* Collection of configurations for objects with commands enabled.
*/
commandObjectConfig = new import_discord.Collection();
/**
* Collection of the last seen interactions of custom commands (not iob-get/-set)
* or other interactions which are not directly handled by the adapter.
*
* Need to cache this here since there seems to be no way to get an interaction
* by ID from discord.js.
*/
lastInteractions = new import_discord.Collection();
/**
* Timeout to trigger the delayed registration of the slash commands.
*/
triggerDelayedRegisterSlashCommandsTimeout = null;
/**
* Set of well known values that will be interperted as true.
* This is extended by some localized strings at runtime.
* Used to determine true values from iob-set slash commands.
*/
wellKnownbooleanTrueValues = /* @__PURE__ */ new Set(["true", "on", "yes", "1"]);
constructor(adapter) {
this.adapter = adapter;
}
/**
* When the adapter is Ready.
* Called by `adapter.onReady()` after some basic checks and setup.
*/
// eslint-disable-next-line @typescript-eslint/require-await
async onReady() {
if (this.adapter.config.cmdGetStateName) {
if (/^[a-z][0-9a-z-_]{1,32}$/.exec(this.adapter.config.cmdGetStateName)) {
this.cmdGetStateName = this.adapter.config.cmdGetStateName;
} else {
this.adapter.log.warn(`Invalid custom get state command name '${this.adapter.config.cmdGetStateName}' provied! Using default 'iob-get'.`);
}
}
if (this.adapter.config.cmdSetStateName) {
this.cmdSetStateName = this.adapter.config.cmdSetStateName;
if (/^[a-z][0-9a-z-_]{1,32}$/.exec(this.adapter.config.cmdSetStateName)) {
this.cmdSetStateName = this.adapter.config.cmdSetStateName;
} else {
this.adapter.log.warn(`Invalid custom set state command name '${this.adapter.config.cmdSetStateName}' provied! Using default 'iob-set'.`);
}
}
this.rest.setToken(this.adapter.config.token);
if (!this.adapter.config.enableCommands) {
return;
}
this.wellKnownbooleanTrueValues.add(import_i18n.i18n.getString("true")).add(import_i18n.i18n.getString("on")).add(import_i18n.i18n.getString("yes"));
if (!this.adapter.client) {
throw new Error("Tried to setup interaction handler for commands, but client is not initialized!");
}
this.adapter.client.on("interactionCreate", this.onInteractionCreate);
}
/**
* Register the commands on discord, if enabled.
*/
async registerSlashCommands() {
this.registerCommandsDone = false;
if (!this.adapter.client?.user) {
throw new Error("Discord client not available");
}
if (this.adapter.unloaded) return;
if (!this.adapter.config.enableCommands) {
this.adapter.log.debug("Commands not enabled");
for (const [, guild] of this.adapter.client.guilds.cache) {
await this.removeGuildCommands(guild);
}
await this.removeGlobalCommands();
return;
}
const commands = [];
const numGet = this.commandObjectConfig.filter((c) => c.get === true).size;
const numSet = this.commandObjectConfig.filter((c) => c.set === true).size;
if (numGet > 0) {
const cmdGet = new import_builders.SlashCommandBuilder().setName(this.cmdGetStateName).setDescription(import_i18n.i18n.getString("Get an ioBroker state value"));
cmdGet.addStringOption((opt) => opt.setName("state").setDescription(import_i18n.i18n.getString("The ioBroker state to get")).setRequired(true).setAutocomplete(true));
commands.push(cmdGet);
}
if (numSet > 0) {
const cmdSet = new import_builders.SlashCommandBuilder().setName(this.cmdSetStateName).setDescription(import_i18n.i18n.getString("Set an ioBroker state value"));
cmdSet.addStringOption((opt) => opt.setName("state").setDescription(import_i18n.i18n.getString("The ioBroker state to set")).setRequired(true).setAutocomplete(true));
cmdSet.addStringOption((opt) => {
return opt.setName("value").setDescription(import_i18n.i18n.getString("The value to set")).setRequired(true);
});
commands.push(cmdSet);
}
this.cmdGetStateChoices = [];
this.cmdSetStateChoices = [];
for (const [, objCfg] of this.commandObjectConfig) {
if (objCfg.get) {
this.cmdGetStateChoices.push({
name: objCfg.name,
value: objCfg.alias
});
}
if (objCfg.set) {
this.cmdSetStateChoices.push({
name: objCfg.name,
value: objCfg.alias
});
}
}
const sortFn = (a, b) => {
if (a.name > b.name) return 1;
if (a.name < b.name) return -1;
return 0;
};
this.cmdGetStateChoices.sort(sortFn);
this.cmdSetStateChoices.sort(sortFn);
this.customCommands.clear();
if (this.adapter.config.enableCustomCommands) {
loopCustomCommands:
for (const customCommandCfg of this.adapter.config.customCommands) {
if (!/^[a-z][0-9a-z-_]{1,32}$/.exec(customCommandCfg.name) || customCommandCfg.description.length === 0 || customCommandCfg.description.length > 100) {
this.adapter.log.warn(`Custom command "${customCommandCfg.name}" has an invalid name or description configured!`);
continue;
}
if (customCommandCfg.name === this.cmdGetStateName) {
this.adapter.log.warn(`Custom command "${customCommandCfg.name}" is configured but conflicts with default get command! The command will be ignored.`);
continue;
}
if (customCommandCfg.name === this.cmdSetStateName) {
this.adapter.log.warn(`Custom command "${customCommandCfg.name}" is configured but conflicts with default set command! The command will be ignored.`);
continue;
}
if (this.customCommands.has(customCommandCfg.name)) {
this.adapter.log.warn(`Custom command "${customCommandCfg.name}" is configured multiple times! The command will be ignored.`);
continue;
}
const cmdCustom = new import_builders.SlashCommandBuilder().setName(customCommandCfg.name).setDescription(customCommandCfg.description);
const optionsChoices = new import_discord.Collection();
const cmdOpts = /* @__PURE__ */ new Set();
if (Array.isArray(customCommandCfg.options)) {
if (customCommandCfg.options.length > 25) {
this.adapter.log.warn(`Custom command "${customCommandCfg.name}" has more than 25 options configured! Only the first 25 options will be used.`);
customCommandCfg.options.splice(25);
}
} else {
customCommandCfg.options = [];
}
await this.setupCustomCommandIobObjects(customCommandCfg);
for (const customCommandCfgOpt of customCommandCfg.options) {
if (!/^[a-z][0-9a-z-_]{1,32}$/.exec(customCommandCfgOpt.name) || customCommandCfgOpt.description.length === 0 || customCommandCfgOpt.description.length > 100) {
this.adapter.log.warn(`Custom command "${customCommandCfg.name}" option "${customCommandCfgOpt.name}" has an invalid name or description configured!`);
continue loopCustomCommands;
}
if (cmdOpts.has(customCommandCfgOpt.name)) {
this.adapter.log.warn(`Custom command "${customCommandCfg.name}" option "${customCommandCfgOpt.name}" is configured multiple times! The command will be ignored.`);
continue loopCustomCommands;
}
cmdOpts.add(customCommandCfgOpt.name);
switch (customCommandCfgOpt.type) {
case "string": {
let choices = [];
try {
const val = (await this.adapter.getStateAsync(`slashCommands.${customCommandCfg.name}.option-${customCommandCfgOpt.name}.choices`))?.val ?? "[]";
if (typeof val !== "string") {
this.adapter.log.warn(`Value of ${this.adapter.namespace}.slashCommands.${customCommandCfg.name}.option-${customCommandCfgOpt.name}.choices is not a string!`);
} else {
choices = JSON.parse(val);
}
} catch (err) {
this.adapter.log.warn(`Could not parse JSON from ${this.adapter.namespace}.slashCommands.${customCommandCfg.name}.option-${customCommandCfgOpt.name}.choices! ${err}`);
}
optionsChoices.set(customCommandCfgOpt.name, choices.map((choice) => {
if (typeof choice === "string" && choice.length >= 1 && choice.length <= 100) {
return { name: choice, value: choice };
} else if (typeof choice === "object" && typeof choice.value === "string" && typeof choice.name === "string" && choice.name.length >= 1 && choice.name.length <= 100 && choice.value.length <= 100) {
return { name: choice.name, value: choice.value };
} else {
this.adapter.log.warn(`Choice ${JSON.stringify(choice)} is not valid for ${this.adapter.namespace}.slashCommands.${customCommandCfg.name}.option-${customCommandCfgOpt.name}.choices and will be ignored!`);
return null;
}
}).filter((choice) => choice !== null));
cmdCustom.addStringOption((opt) => {
opt.setName(customCommandCfgOpt.name).setDescription(customCommandCfgOpt.description).setRequired(!!customCommandCfgOpt.required);
if (choices.length > 0) {
opt.setAutocomplete(true);
}
return opt;
});
break;
}
case "number":
cmdCustom.addNumberOption((opt) => opt.setName(customCommandCfgOpt.name).setDescription(customCommandCfgOpt.description).setRequired(!!customCommandCfgOpt.required));
break;
case "boolean":
cmdCustom.addBooleanOption((opt) => opt.setName(customCommandCfgOpt.name).setDescription(customCommandCfgOpt.description).setRequired(!!customCommandCfgOpt.required));
break;
case "user":
cmdCustom.addUserOption((opt) => opt.setName(customCommandCfgOpt.name).setDescription(customCommandCfgOpt.description).setRequired(!!customCommandCfgOpt.required));
break;
case "role":
cmdCustom.addRoleOption((opt) => opt.setName(customCommandCfgOpt.name).setDescription(customCommandCfgOpt.description).setRequired(!!customCommandCfgOpt.required));
break;
case "channel":
cmdCustom.addChannelOption((opt) => opt.setName(customCommandCfgOpt.name).setDescription(customCommandCfgOpt.description).setRequired(!!customCommandCfgOpt.required));
break;
case "mentionable":
cmdCustom.addMentionableOption((opt) => opt.setName(customCommandCfgOpt.name).setDescription(customCommandCfgOpt.description).setRequired(!!customCommandCfgOpt.required));
break;
default:
this.adapter.log.warn(`Custom command "${customCommandCfg.name}" option "${customCommandCfgOpt.name} has an invalid type set"!`);
continue loopCustomCommands;
}
}
commands.push(cmdCustom);
this.customCommands.set(customCommandCfg.name, optionsChoices);
}
const objListSlashCommands = await this.adapter.getObjectListAsync({
startkey: `${this.adapter.namespace}.slashCommands.`,
endkey: `${this.adapter.namespace}.slashCommands.\u9999`
});
const re = new RegExp(`^${this.adapter.name}\\.${this.adapter.instance}\\.slashCommands\\.([^.]+)$`);
for (const item of objListSlashCommands.rows) {
const m = item.id.match(re);
if (m) {
const cmdName = m[1];
if (!this.customCommands.has(cmdName)) {
this.adapter.log.debug(`Custom slash command ${cmdName} is not configured - deleting objects`);
await this.adapter.delObjectAsyncCached(`slashCommands.${cmdName}`, { recursive: true });
}
}
}
}
if (commands.length === 0) {
if (this.adapter.config.enableCustomCommands) {
this.adapter.log.warn("Commands are enabled but not configured for any state object and no custom commands are configured! Use the custom configuration of a state object to activate commands on it or add custom commands in the adapter instance configuration.");
} else {
this.adapter.log.warn("Commands are enabled but not configured for any state object! Use the custom configuration of a state object to activate commands on it.");
}
}
const commandsJson = commands.map((cmd) => cmd.toJSON());
if (!(0, import_node_util.isDeepStrictEqual)(commandsJson, this.lastCommandsJson)) {
this.adapter.log.debug("Commands needs to be updated");
for (const [, guild] of this.adapter.client.guilds.cache) {
try {
if (this.adapter.config.commandsGlobal) {
await this.removeGuildCommands(guild);
} else {
await this.rest.put(import_v10.Routes.applicationGuildCommands(this.adapter.client.user.id, guild.id), { body: commandsJson });
this.adapter.log.info(`Registered commands for server ${guild.name} (id:${guild.id}) (get: ${numGet}, set: ${numSet}, custom: ${this.customCommands.size})`);
}
} catch (err) {
if (err instanceof import_discord.DiscordAPIError && err.message === "Missing Access") {
this.adapter.log.warn(`Error registering commands for server ${guild.name} (id:${guild.id}). Seams like the bot is missing the 'applications.commands' scope on the server. ${err}`);
} else {
this.adapter.log.warn(`Error registering commands for server ${guild.name} (id:${guild.id}): ${err}`);
}
}
}
try {
if (this.adapter.config.commandsGlobal) {
await this.rest.put(import_v10.Routes.applicationCommands(this.adapter.client.user.id), { body: commandsJson });
this.adapter.log.info(`Registered global commands (get: ${numGet}, set: ${numSet}, custom: ${this.customCommands.size})`);
} else {
await this.removeGlobalCommands();
}
} catch (err) {
if (err instanceof import_discord.DiscordAPIError && err.message === "Missing Access") {
this.adapter.log.warn(`Error registering global commands. Seams like the bot is missing the 'applications.commands' scope on the server. ${err}`);
} else {
this.adapter.log.warn(`Error registering global commands: ${err}`);
}
}
this.lastCommandsJson = commandsJson;
} else {
this.adapter.log.debug("Commands seams to be up to date");
}
this.registerCommandsDone = true;
}
/**
* Setup an ioBroker object for discord slash commands.
* @param objId ID of the ioBroker object to set up.
* @param cfg Command configuration for the ioBroker object or null to remove a possibly existing configuration.
*/
setupCommandObject(objId, cfg) {
if (cfg) {
const conflictingAlias = this.commandObjectConfig.find((coc) => coc.alias === cfg?.alias && coc.id !== cfg.id);
if (conflictingAlias) {
this.adapter.log.warn(`Command alias ${cfg.alias} of object ${cfg.id} already in use by object ${conflictingAlias.id}! ${cfg.id} will be ignored.`);
cfg = null;
}
}
if (!cfg) {
if (this.commandObjectConfig.has(objId)) {
this.commandObjectConfig.delete(objId);
this.triggerDelayedRegisterSlashCommands();
}
return;
}
const currentCfg = this.commandObjectConfig.get(objId);
if (!(0, import_node_util.isDeepStrictEqual)(cfg, currentCfg)) {
this.adapter.log.debug(`Update command configuration for ${objId}: ${JSON.stringify(cfg)}`);
this.commandObjectConfig.set(objId, cfg);
this.triggerDelayedRegisterSlashCommands();
}
}
/**
* Initialize a delayed registration of the slash commands.
* Calls `registerSlashCommands()` five seconds after the last call of this method.
* If called again within the five seconds the timeout starts again.
*
* This is used to handle object changes better and concat multiple changed
* object configurations into a single API call.
*
* If the initial custom objects setup of the adapter isn't done, this method
* does nothing since the command registration is called during this explicit.
*/
triggerDelayedRegisterSlashCommands() {
if (!this.adapter.initialCustomObjectSetupDone) return;
if (this.triggerDelayedRegisterSlashCommandsTimeout) {
this.adapter.clearTimeout(this.triggerDelayedRegisterSlashCommandsTimeout);
}
this.adapter.setTimeout(() => {
this.triggerDelayedRegisterSlashCommandsTimeout = null;
this.adapter.log.debug("Starting delayed slash commands registration...");
void this.registerSlashCommands();
}, 5e3);
}
/**
* Send a reply to a custom slash command.
* @param interactionId The ID of the interaction to reply to. The interactions needs to be cached in this instance.
* @param msg The message to reply with. May be a simple string, a MessageOptions object or a stringified JSON MessageOptions object.
* @returns Promise which resolves with the the ID of the reply message if the reply is sent.
* @throws Error if the reply could not be sent for some reason (i.e. some check failed).
*/
async sendCmdCustomReply(interactionId, msg) {
const interaction = this.lastInteractions.get(interactionId);
if (!interaction) {
throw new Error(`No current interaction with ID ${interactionId} found for reply!`);
}
let cscTxt = "";
if (interaction.isCommand()) {
const { commandName } = interaction;
const cmdCfg = this.adapter.config.customCommands.find((c) => c.name === commandName);
if (!cmdCfg) {
throw new Error(`No configuration for custom slash command ${commandName} of interaction ${interactionId} found for reply!`);
}
cscTxt = ` of custom slash command ${commandName}`;
}
if (!interaction.isRepliable()) {
throw new Error(`Interaction ${interactionId}${cscTxt} is not repliable!`);
}
if (typeof msg === "string") {
try {
msg = this.adapter.parseStringifiedMessageOptions(msg);
} catch (err) {
throw new Error(`Reply to interaction ${interactionId}${cscTxt} is invalid: ${err}`);
}
}
const replyMsg = await interaction.editReply(msg);
return replyMsg.id;
}
/**
* Write a summay of all currently for commands configured objects to the log.
*/
logConfiguredCommandObjects() {
this.adapter.log.info("Configured state objects for discord slash commands:");
for (const [, cmdObjCfg] of this.commandObjectConfig) {
this.adapter.log.info(` |- ${cmdObjCfg.id} - alias:${cmdObjCfg.alias}, name:${cmdObjCfg.name}, get:${cmdObjCfg.get}, set:${cmdObjCfg.set}`);
}
this.adapter.log.info("---");
}
/**
* Remove registered global commands if any.
*/
async removeGlobalCommands() {
if (!this.adapter.client?.user) {
throw new Error("Discord client not available");
}
try {
const globalCommands = await this.adapter.client.application?.commands.fetch();
if (this.adapter.unloaded) return;
if (globalCommands && globalCommands.size > 0) {
this.adapter.log.debug(`Currently ${globalCommands.size} global commands registered. Removing them...`);
await this.rest.put(import_v10.Routes.applicationCommands(this.adapter.client.user.id), { body: [] });
this.adapter.log.info(`Removed global commands cause commands they are not used anymore.`);
}
} catch (err) {
if (err instanceof import_discord.DiscordAPIError && err.message === "Missing Access") {
this.adapter.log.warn(`Error while removing registered global commands. Seams like the bot is missing the 'applications.commands' scope on the server. ${err}`);
} else {
this.adapter.log.warn(`Error while removing registered global commands: ${err}`);
}
}
}
/**
* Remove registered guild commands if any.
* @param guild The guild.
*/
async removeGuildCommands(guild) {
if (!this.adapter.client?.user) {
throw new Error("Discord client not available");
}
try {
const guildCommands = await guild.commands.fetch();
if (this.adapter.unloaded) return;
if (guildCommands.size > 0) {
this.adapter.log.debug(`Currently ${guildCommands.size} commands registered for server ${guild.name}. Removing them...`);
await this.rest.put(import_v10.Routes.applicationGuildCommands(this.adapter.client.user.id, guild.id), { body: [] });
this.adapter.log.info(`Removed commands for server ${guild.name} cause commands they are not used anymore.`);
}
} catch (err) {
if (err instanceof import_discord.DiscordAPIError && err.message === "Missing Access") {
this.adapter.log.warn(`Error while removing registered commands for server ${guild.name} (id:${guild.id}). Seams like the bot is missing the 'applications.commands' scope on the server. ${err}`);
} else {
this.adapter.log.warn(`Error while removing registered commands for server ${guild.name} (id:${guild.id}): ${err}`);
}
}
}
/**
* Setup the ioBroker object tree for the configured custom commands.
*/
async setupCustomCommandIobObjects(cmdCfg) {
const cmdName = cmdCfg.name;
await this.adapter.extendObjectCached(`slashCommands.${cmdName}`, {
type: "channel",
common: {
name: cmdCfg.description
},
native: {}
});
await Promise.all([
this.adapter.extendObjectCached(`slashCommands.${cmdName}.json`, {
type: "state",
common: {
name: import_i18n.i18n.getStringOrTranslated("JSON data"),
role: "json",
type: "string",
read: true,
write: false,
def: ""
},
native: {}
}),
this.adapter.extendObjectCached(`slashCommands.${cmdName}.timestamp`, {
type: "state",
common: {
name: import_i18n.i18n.getStringOrTranslated("Last use timestamp"),
role: "date",
type: "number",
read: true,
write: false,
def: 0
},
native: {}
}),
this.adapter.extendObjectCached(`slashCommands.${cmdName}.sendReply`, {
type: "state",
common: {
name: import_i18n.i18n.getStringOrTranslated("Send reply"),
role: "text",
type: "string",
read: false,
write: true,
def: ""
},
native: {}
}),
this.adapter.extendObjectCached(`slashCommands.${cmdName}.interactionId`, {
type: "state",
common: {
name: import_i18n.i18n.getStringOrTranslated("Interaction ID"),
role: "text",
type: "string",
read: true,
write: false,
def: ""
},
native: {}
}),
this.adapter.extendObjectCached(`slashCommands.${cmdName}.userId`, {
type: "state",
common: {
name: import_i18n.i18n.getStringOrTranslated("User ID"),
role: "text",
type: "string",
read: true,
write: false,
def: ""
},
native: {}
}),
this.adapter.extendObjectCached(`slashCommands.${cmdName}.userTag`, {
type: "state",
common: {
name: import_i18n.i18n.getStringOrTranslated("User tag"),
role: "text",
type: "string",
read: true,
write: false,
def: ""
},
native: {}
}),
this.adapter.extendObjectCached(`slashCommands.${cmdName}.userName`, {
type: "state",
common: {
name: import_i18n.i18n.getStringOrTranslated("User name"),
role: "text",
type: "string",
read: true,
write: false,
def: ""
},
native: {}
}),
this.adapter.extendObjectCached(`slashCommands.${cmdName}.serverId`, {
type: "state",
common: {
name: import_i18n.i18n.getStringOrTranslated("Server ID"),
role: "text",
type: "string",
read: true,
write: false,
def: ""
},
native: {}
}),
this.adapter.extendObjectCached(`slashCommands.${cmdName}.channelId`, {
type: "state",
common: {
name: import_i18n.i18n.getStringOrTranslated("Channel ID"),
role: "text",
type: "string",
read: true,
write: false,
def: ""
},
native: {}
})
]);
const proms = [];
for (const cmdCfgOpt of cmdCfg.options) {
await this.adapter.extendObjectCached(`slashCommands.${cmdName}.option-${cmdCfgOpt.name}`, {
type: "channel",
common: {
name: import_i18n.i18n.getStringOrTranslated("Option %s", cmdCfgOpt.description)
},
native: {}
});
let role;
let type;
let def;
switch (cmdCfgOpt.type) {
case "number":
role = "value";
type = "number";
def = null;
break;
case "boolean":
role = "indicator";
type = "boolean";
def = null;
break;
default:
role = "text";
type = "string";
def = "";
}
proms.push(this.adapter.extendObjectCached(`slashCommands.${cmdName}.option-${cmdCfgOpt.name}.value`, {
type: "state",
common: {
name: import_i18n.i18n.getStringOrTranslated("Option %s value", cmdCfgOpt.description),
role,
type,
read: true,
write: false,
def
},
native: {}
}));
if (cmdCfgOpt.type === "string") {
proms.push(this.adapter.extendObjectCached(`slashCommands.${cmdName}.option-${cmdCfgOpt.name}.choices`, {
type: "state",
common: {
name: import_i18n.i18n.getStringOrTranslated("Option %s choices", cmdCfgOpt.description),
role: "json",
type: "string",
read: true,
write: true,
def: "[]"
},
native: {}
}));
} else {
proms.push(this.adapter.delObjectAsyncCached(`slashCommands.${cmdName}.option-${cmdCfgOpt.name}.choices`));
}
}
const objListOptions = await this.adapter.getObjectListAsync({
startkey: `${this.adapter.namespace}.slashCommands.${cmdName}.option-`,
endkey: `${this.adapter.namespace}.slashCommands.${cmdName}.option-\u9999`
});
const reOptionsName = new RegExp(`^${this.adapter.name}\\.${this.adapter.instance}\\.slashCommands\\.${cmdName}\\.option-([^.]+)$`);
for (const item of objListOptions.rows) {
const m = item.id.match(reOptionsName);
if (m) {
const oldoptName = m[1];
if (!cmdCfg.options.find((o) => o.name === oldoptName)) {
proms.push(this.adapter.delObjectAsyncCached(`slashCommands.${cmdName}.option-${oldoptName}`, { recursive: true }));
this.customCommands.get(cmdName)?.delete(oldoptName);
}
}
}
await Promise.all(proms);
}
async onInteractionCreate(interaction) {
if (interaction.isChatInputCommand()) {
void this.handleCommandInteraction(interaction);
} else if (interaction.isAutocomplete()) {
void this.handleAutocompleteInteraction(interaction);
} else {
this.lastInteractions.set(interaction.id, interaction);
if (interaction.isRepliable() && !interaction.deferred) {
await interaction.deferReply({ ephemeral: this.adapter.config.commandRepliesEphemeral });
}
}
if (this.adapter.config.enableRawStates) {
const interactionJson = interaction.toJSON();
if (interaction.isChatInputCommand()) {
interactionJson.options = interaction.options.data;
}
void this.adapter.setState("raw.interactionJson", JSON.stringify(interactionJson, (_key, value) => typeof value === "bigint" ? value.toString() : value), true);
}
const outdatedTs = Date.now() - 15 * 6e4;
const removedInteractions = this.lastInteractions.sweep((ia) => ia.createdTimestamp < outdatedTs);
if (removedInteractions > 0) {
this.adapter.log.debug(`Removed ${removedInteractions} outdated interactions from cache`);
}
}
/**
* Handle command interactions.
*/
async handleCommandInteraction(interaction) {
const { commandName, user } = interaction;
if (!this.registerCommandsDone) {
this.adapter.log.warn(`Got command ${commandName} but command registration is not done yet.`);
return;
}
this.adapter.log.debug(`Got command ${commandName} ${JSON.stringify(interaction.options.data)}`);
const authCheckTarget = interaction.member instanceof import_discord.GuildMember ? interaction.member : user;
if (commandName === this.cmdGetStateName) {
if (this.adapter.checkUserAuthorization(authCheckTarget, { getStates: true })) {
await this.handleCmdGetState(interaction);
} else {
this.adapter.log.warn(`User ${(0, import_utils.userNameOrTag)(user)} (id:${user.id}) is not authorized to call /${commandName} commands!`);
await interaction.reply({
content: import_i18n.i18n.getString("You are not authorized to call this command!"),
ephemeral: true
});
}
} else if (commandName === this.cmdSetStateName) {
if (this.adapter.checkUserAuthorization(authCheckTarget, { setStates: true })) {
await this.handleCmdSetState(interaction);
} else {
this.adapter.log.warn(`User ${(0, import_utils.userNameOrTag)(user)} (id:${user.id}) is not authorized to call /${commandName} commands!`);
await interaction.reply({
content: import_i18n.i18n.getString("You are not authorized to call this command!"),
ephemeral: true
});
}
} else if (this.customCommands.has(commandName)) {
if (this.adapter.checkUserAuthorization(authCheckTarget, { useCustomCommands: true })) {
await this.handleCmdCustom(interaction);
} else {
this.adapter.log.warn(`User ${(0, import_utils.userNameOrTag)(user)} (id:${user.id}) is not authorized to call /${commandName} commands!`);
await interaction.reply({
content: import_i18n.i18n.getString("You are not authorized to call this command!"),
ephemeral: true
});
}
} else {
this.adapter.log.warn(`Got unknown command ${commandName}!`);
await interaction.editReply(import_i18n.i18n.getString("Unknown command!"));
}
}
/**
* Handle autocomplete for command interactions.
*/
async handleAutocompleteInteraction(interaction) {
const { commandName, user } = interaction;
const focused = interaction.options.getFocused(true);
let focusedValue;
if (typeof focused.value !== "string") {
return await interaction.respond([]);
} else {
focusedValue = focused.value.toLowerCase();
}
const authCheckTarget = interaction.member instanceof import_discord.GuildMember ? interaction.member : user;
let choices;
if (commandName === this.cmdGetStateName) {
if (!this.adapter.checkUserAuthorization(authCheckTarget, { getStates: true })) {
return await interaction.respond([]);
}
choices = this.cmdGetStateChoices;
} else if (commandName === this.cmdSetStateName) {
if (!this.adapter.checkUserAuthorization(authCheckTarget, { setStates: true })) {
return await interaction.respond([]);
}
choices = this.cmdSetStateChoices;
} else if (this.customCommands.has(commandName)) {
if (!this.adapter.checkUserAuthorization(authCheckTarget, { useCustomCommands: true })) {
return await interaction.respond([]);
}
choices = this.customCommands.get(commandName)?.get(focused.name) ?? [];
} else {
return await interaction.respond([]);
}
const matchedChoices = choices.filter((choice) => {
return choice.name.normalize("NFKC").toLowerCase().includes(focusedValue) || choice.value.normalize("NFKC").toLowerCase().includes(focusedValue);
});
matchedChoices.splice(25);
return await interaction.respond(matchedChoices);
}
/**
* Try to get the ioBroker object and CommandObjectConfig for a given object alias.
* The object will be checked if it's a valid state object.
*
* In case of an error, a reply will be sent to the interaction.
* @param objAlias The alias of the object.
* @param interaction The interaction for replies on errors.
* @returns Array containing the object and the config or null and null.
*/
async getObjectAndCfgFromAlias(objAlias, interaction) {
const cfg = this.commandObjectConfig.find((coc) => coc.alias === objAlias);
if (!cfg) {
if (interaction.replied) {
await interaction.editReply({
content: import_i18n.i18n.getString("Object `%s` not found!", objAlias ?? "")
});
} else {
await interaction.reply({
content: import_i18n.i18n.getString("Object `%s` not found!", objAlias ?? ""),
ephemeral: true
});
}
return [null, null];
}
const obj = await this.adapter.getForeignObjectAsync(cfg.id);
if (!obj) {
if (interaction.replied) {
await interaction.editReply({
content: import_i18n.i18n.getString("Object `%s` not found!", cfg.id)
});
} else {
await interaction.reply({
content: import_i18n.i18n.getString("Object `%s` not found!", cfg.id),
ephemeral: true
});
}
return [null, null];
}
if (obj.type !== "state") {
if (interaction.replied) {
await interaction.editReply({
content: import_i18n.i18n.getString("Object `%s` is not of type state!", cfg.id)
});
} else {
await interaction.reply({
content: import_i18n.i18n.getString("Object `%s` is not of type state!", cfg.id),
ephemeral: true
});
}
return [null, null];
}
return [obj, cfg];
}
/**
* Handler for "get state" slash commands.
* @param interaction The interaction which triggered this.
*/
async handleCmdGetState(interaction) {
const objAlias = interaction.options.get("state", true).value;
const [obj, cfg] = await this.getObjectAndCfgFromAlias(objAlias, interaction);
if (!obj || !cfg) {
return;
}
const objCustom = obj.common.custom?.[this.adapter.namespace];
if (!objCustom?.commandsAllowGet) {
await interaction.reply({
content: import_i18n.i18n.getString("Get not allowed for state `%s`!", cfg.id),
ephemeral: true
});
return;
}
const state = await this.adapter.getForeignStateAsync(cfg.id);
if (!state) {
await interaction.reply({
content: import_i18n.i18n.getString("State `%s` not found!", cfg.id),
ephemeral: true
});
return;
}
if (!interaction.deferred) {
await interaction.deferReply({ ephemeral: this.adapter.config.commandRepliesEphemeral });
}
let val = "";
let msgOpts;
const unit = obj.common.unit ? ` ${obj.common.unit}` : "";
const ack = objCustom.commandsShowAckFalse && !state.ack ? ` (_${import_i18n.i18n.getString("not acknowledged")}_)` : "";
if (obj.common.role === "date" && (obj.common.type === "string" && typeof state.val === "string" || obj.common.type === "number" && typeof state.val === "number")) {
const d = new Date(state.val);
val = d.toLocaleString(import_i18n.i18n.language, { dateStyle: "full", timeStyle: "long" });
} else if (obj.common.type === "string" && objCustom.commandsStringSendAsFile && typeof state.val === "string") {
const b64data = (0, import_utils.getBufferAndNameFromBase64String)(state.val);
if (b64data) {
msgOpts = {
content: `${cfg.name}${ack}:`,
files: [{
attachment: b64data.buffer,
name: b64data.name
}]
};
val = "file:base64";
} else {
if (state.val.startsWith("file://")) {
state.val = state.val.slice(7);
}
msgOpts = {
content: `${cfg.name}${ack}:`,
files: [{
attachment: state.val,
name: (0, import_utils.getBasenameFromFilePathOrUrl)(state.val)
}]
};
val = `file:${state.val}`;
}
} else {
switch (obj.common.type) {
case "boolean":
if (state.val) {
val = objCustom.commandsBooleanValueTrue ?? import_i18n.i18n.getString("true");
} else {
val = objCustom.commandsBooleanValueFalse ?? import_i18n.i18n.getString("false");
}
break;
case "number": {
const decimals = objCustom.commandsNumberDecimals ?? 0;
if (typeof state.val === "number") {
val = state.val.toFixed(decimals);
} else if (state.val === null) {
val = "_NULL_";
} else {
val = state.val.toString() || "NaN";
}
if (import_i18n.i18n.isFloatComma) {
val = val.replace(".", ",");
}
break;
}
default:
if (typeof state.val === "string") {
val = state.val;
} else if (state.val === null) {
val = "_NULL_";
} else {
val = state.val.toString();
}
}
}
this.adapter.log.debug(`Get command for ${cfg.id} - ${val}${unit}${ack}`);
try {
if (msgOpts) {
await interaction.editReply(msgOpts);
} else {
await interaction.editReply(`${cfg.name}: ${val}${unit}${ack}`);
}
} catch (err) {
this.adapter.log.warn(`Error sending interaction reply for /${this.cmdGetStateName} command! ${err}`);
}
}
/**
* Handler for "set state" slash commands.
* @param interaction The interaction which triggered this.
*/
async handleCmdSetState(interaction) {
const objAlias = interaction.options.get("state", true).value;
const [obj, cfg] = await this.getObjectAndCfgFromAlias(objAlias, interaction);
if (!obj || !cfg) {
return;
}
const objCustom = obj.common.custom?.[this.adapter.namespace];
if (!objCustom?.commandsAllowSet) {
await interaction.reply({
content: import_i18n.i18n.getString("Set not allowed for state `%s`!", cfg.id),
ephemeral: true
});
return;
}
let valueStr = interaction.options.get("value")?.value;
if (typeof valueStr !== "string") {
await interaction.reply({
content: import_i18n.i18n.getString("No value provided!"),
ephemeral: true
});
return;
}
if (!interaction.deferred) {
await interaction.deferReply({ ephemeral: this.adapter.config.commandRepliesEphemeral });
}
valueStr = valueStr.trim();
const unit = obj.common.unit ? ` ${obj.common.unit}` : "";
let value;
let valueReply;
switch (obj.common.type) {
case "boolean":
valueStr = valueStr.toLowerCase();
if (valueStr === objCustom.commandsBooleanValueTrue?.toLowerCase() || this.wellKnownbooleanTrueValues.has(valueStr)) {
value = true;
valueReply = objCustom.commandsBooleanValueTrue ?? import_i18n.i18n.getString("true");
} else {
value = false;
valueReply = objCustom.commandsBooleanValueFalse ?? import_i18n.i18n.getString("false");
}
break;
case "number":
if (import_i18n.i18n.isFloatComma) {
valueStr = valueStr.replace(",", ".");
}
value = parseFloat(valueStr);
if (isNaN(value)) {
await interaction.editReply(import_i18n.i18n.getString("The given value is not a number!"));
return;
}
valueReply = value.toString();
if (import_i18n.i18n.isFloatComma) {
valueReply = valueReply.replace(".", ",");
}
if (typeof obj.common.min === "number" && value < obj.common.min) {
let min = obj.common.min.toString();
if (import_i18n.i18n.isFloatComma) {
min = min.replace(".", ",");
}
await interaction.editReply(import_i18n.i18n.getString("Value %s is below the allowed minimum of %s!", `${valueReply}${unit}`, `${min}${unit}`));
return;
}
if (typeof obj.common.max === "number" && value > obj.common.max) {
let max = obj.common.max.toString();
if (import_i18n.i18n.isFloatComma) {
max = max.replace(".", ",");
}
await interaction.editReply(import_i18n.i18n.getString("Value %s is above the allowed maximum of %s!", `${valueReply}${unit}`, `${max}${unit}`));
return;
}
break;
default:
value = valueStr;
valueReply = valueStr;
}
this.adapter.log.debug(`Set command for ${cfg.id} - ${value}${unit}`);
try {
await this.adapter.setForeignStateAsync(cfg.id, value, !!objCustom.commandsSetWithAck);
} catch (err) {
this.adapter.log.warn(`Error while setting state ${cfg.id} to ${value}! ${err}`);
await interaction.editReply(import_i18n.i18n.getString("Error while setting the state value!"));
return;
}
await interaction.editReply(`${cfg.name}: ${valueReply}${unit}`);
}
/**
* Handler for custom slash commands.
* @param interaction The interaction which triggered this.
*/
async handleCmdCustom(interaction) {
const {
commandName,
channelId,
guildId,
user,
options
} = interaction;
const cmdCfg = this.adapter.config.customCommands.find((c) => c.name === commandName);
if (!cmdCfg) return;
if (!interaction.deferred) {
await interaction.deferReply({ ephemeral: this.adapter.config.commandRepliesEphemeral });
}
this.lastInteractions.set(interaction.id, interaction);
const proms = [];
const json = {
interactionId: interaction.id,
commandName,
channelId,
serverId: guildId ?? null,
user: {
id: user.id,
tag: user.tag,
name: user.username,
displayName: interaction.member instanceof import_discord.GuildMember ? interaction.member.displayName : user.username
},
timestamp: interaction.createdTimestamp,
options: {}
};
for (const optCfg of cmdCfg.options) {
const opt = options.data.find((o) => o.name === optCfg.name);
if (opt) {
json.options[optCfg.name] = {
value: opt.value ?? null,
type: import_discord.ApplicationCommandOptionType[opt.type]
};
if (opt.user instanceof import_discord.User) {
json.options[optCfg.name].user = {
id: opt.user.id,
tag: opt.user.tag,
name: opt.user.username,
bot: opt.user.bot
};
}
if (opt.member instanceof import_discord.GuildMember) {
json.options[optCfg.name].member = {
id: opt.member.id,
tag: opt.member.user.tag,
name: opt.member.user.username,
displayName: opt.member.displayName,
roles: opt.member.roles.cache.map((r) => ({ id: r.id, name: r.name }))
};
}
if (opt.role instanceof import_discord.Role) {
json.options[optCfg.name].role = {
id: opt.role.id,
name: opt.role.name
};
}
if (opt.channel instanceof import_discord.GuildChannel) {
json.options[optCfg.name].channel = {
id: opt.channel.id,
name: opt.channel.name,
type: import_discord.ChannelType[opt.channel.type],
lastMessageId: opt.channel.type === import_discord.ChannelType.GuildText || opt.channel.type === import_discord.ChannelType.GuildVoice ? opt.channel.lastMessageId : null
};
}
} else {
json.options[optCfg.name] = {
value: null,
type: null
};
}
proms.push(this.adapter.setState(`slashCommands.${commandName}.option-${optCfg.name}.value`, json.options[optCfg.name].value, true));
}
await Promise.all([
this.adapter.setState(`slashCommands.${commandName}.interactionId`, interaction.id, true),
this.adapter.setState(`slashCommands.${commandName}.channelId`, channelId, true),
this.adapter.setState(`slashCommands.${commandName}.serverId`, guildId ?? null, true),
this.adapter.setState(`slashCommands.${commandName}.userId`, user.id, true),
this.adapter.setState(`slashCommands.${commandName}.userTag`, user.tag, true),
this.adapter.setState(`slashCommands.${commandName}.userName`, user.username, true),
this.adapter.setState(`slashCommands.${commandName}.timestamp`, interaction.createdTimestamp, true),
this.adapter.setState(`slashCommands.${commandName}.json`, JSON.stringify(json), true),
...proms
]);
}
}
__decorateClass([
import_autobind_decorator.boundMethod
], DiscordAdapterSlashCommands.prototype, "onInteractionCreate", 1);
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
DiscordAdapterSlashCommands
});
//# sourceMappingURL=commands.js.map