UNPKG

iobroker.discord

Version:
1,296 lines 109 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; 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 main_exports = {}; module.exports = __toCommonJS(main_exports); var import_register = require("source-map-support/register"); var import_promises = require("node:timers/promises"); var import_node_util = require("node:util"); var import_autobind_decorator = require("autobind-decorator"); var import_adapter_core = require("@iobroker/adapter-core"); var import_discord = require("discord.js"); var import_commands = require("./commands"); var import_definitions = require("./lib/definitions"); var import_i18n = require("./lib/i18n"); var import_utils = require("./lib/utils"); var import_notification_manager = require("./lib/notification-manager"); const LOGIN_WAIT_TIMES = [ 0, // none - first try! 5e3, // 5 sek 1e4, // 10 sek 3e4, // 30 sek 6e4, // 1 min 12e4, // 2 min 12e4, // 2 min 3e5, // 5 min 3e5, // 5 min 3e5, // 5 min 6e5 // 10 min ]; class DiscordAdapter extends import_adapter_core.Adapter { /** * Instance of the discord client. */ client = null; /** * Flag if the adapter is unloaded or is unloading. * Used to check this in some async operations. */ unloaded = false; /** * Flag if the initial setup of the custom object configurations is done or not. * While not done, custom object configuration changes will not trigger a * slash commands registration automatically. */ initialCustomObjectSetupDone = false; /** * Local cache for `info.connection` state. */ infoConnected = false; /** * Set of state IDs where received discord messages will be stored to. * Used to identify target states for received discord messages. */ messageReceiveStates = /* @__PURE__ */ new Set(); /** * Set user IDs known to set up. * Used to check if the user objects are created on some events. */ knownUsers = /* @__PURE__ */ new Set(); /** * Set of objects from this instance with text2command enabled. */ text2commandObjects = /* @__PURE__ */ new Set(); /** * Cache for `extendObjectCache(...)` calls to extend objects only when changed. */ extendObjectCache = new import_discord.Collection(); /** * Cache for `.json` states. */ jsonStateCache = new import_discord.Collection(); /** * Instance of the slash commands handler class. */ discordSlashCommands; /** * Flag if we are currently in shard error state from discord.js. * `false` currently not on error state, A `string` containing the error name in * case of en error. */ isShardError = false; constructor(options = {}) { super({ ...options, name: "discord" }); this.discordSlashCommands = new import_commands.DiscordAdapterSlashCommands(this); this.on("ready", this.onReady); this.on("stateChange", this.onStateChange); this.on("objectChange", this.onObjectChange); this.on("message", this.onMessage); this.on("unload", this.onUnload); } /** * Try to detect and parse stringified JSON MessageOptions. * * If the `content` starts/ends with curly braces if will be treated as * stringified JSON. Then the JSON will be parsed and some basic checks will * be run against the parsed object. * * Otherwise the content will be treated as a simple string and wrapped into * a `MessageOptions` object. * @param content The stringified content to be parsed. * @returns A `MessageOptions` object. * @throws An error if parsing JSON or a check failed. */ parseStringifiedMessageOptions(content) { let mo; if (content.startsWith("{") && content.endsWith("}")) { this.log.debug(`Content seems to be json`); try { mo = JSON.parse(content); } catch (_err) { throw new Error(`Content seems to be json but cannot be parsed!`); } if (!mo?.files && !mo.content || mo.files && !Array.isArray(mo.files) || mo.embeds && !Array.isArray(mo.embeds)) { throw new Error(`Content is json but seems to be invalid!`); } } else { mo = { content }; } return mo; } /** * Check if a user or guild member is authorized to do something. * For guild members their roles will also be checked. * @param user The User or GuildMember to check. * @param required Object containing the required flags. If not provided the check returns if the user in the list of authorized users. * @returns `true` if the user is authorized or authorization is not enabled, `false` otherwise */ checkUserAuthorization(user, required) { if (!this.config.enableAuthorization) { return true; } let given = this.config.authorizedUsers.find((au) => au.userId === user.id); if (this.config.authorizedServerRoles.length > 0 && user instanceof import_discord.GuildMember) { for (const [, role] of user.roles.cache) { const roleGiven = this.config.authorizedServerRoles.find((ar) => ar.serverAndRoleId === `${user.guild.id}|${role.id}`); if (roleGiven) { if (!given) { given = roleGiven; } else { given = { getStates: given.getStates || roleGiven.getStates, setStates: given.setStates || roleGiven.setStates, useCustomCommands: given.useCustomCommands || roleGiven.useCustomCommands, useText2command: given.useText2command || roleGiven.useText2command }; } } } } if (!given) { return false; } if (!required) { return true; } if (required.getStates && !given.getStates || required.setStates && !given.setStates || required.useCustomCommands && !given.useCustomCommands || required.useText2command && !given.useText2command) { return false; } return true; } /** * Internal replacemend for `extendObject(...)` which compares the given * object for each `id` against a cached version and only calls na original * `extendObject(...)` if the object changed. * Using this, the object gets only updated if * a) it's the first call for this `id` or * b) the object needs to be changed. */ async extendObjectCached(id, objPart, options) { const cachedObj = this.extendObjectCache.get(id); if ((0, import_node_util.isDeepStrictEqual)(cachedObj, objPart)) { return { id }; } let ret; if (options) { ret = await this.extendObject(id, objPart, options); } else { ret = await this.extendObject(id, objPart); } this.extendObjectCache.set(id, objPart); return ret; } /** * Internal replacement for `delObjectAsync(...)` which also removes the local * cache entry for the given `id`. */ async delObjectAsyncCached(id, options) { if (options?.recursive) { this.extendObjectCache.filter((_obj, id2) => id2.startsWith(id)).each((_obj, id2) => this.extendObjectCache.delete(id2)); } else { this.extendObjectCache.delete(id); } return await this.delObjectAsync(id, options); } async onReady() { await this.setInfoConnectionState(false, true); this.log.debug(`Version of discord.js: ${import_discord.version}`); const systemConfig = await this.getForeignObjectAsync("system.config"); import_i18n.i18n.language = systemConfig?.common.language ?? "en"; import_i18n.i18n.isFloatComma = systemConfig?.common.isFloatComma ?? false; if (typeof this.config.token !== "string" || !/^[0-9a-zA-Z-_]{24,}\.[0-9a-zA-Z-_]{6}\.[0-9a-zA-Z-_]{27,}$/.exec(this.config.token)) { this.log.error(`No or invalid token!`); return; } if (!Array.isArray(this.config.authorizedUsers)) { this.config.authorizedUsers = []; } if (!Array.isArray(this.config.authorizedServerRoles)) { this.config.authorizedServerRoles = []; } if (!this.config.enableAuthorization) { this.log.info("Authorization is disabled, so any user is able to interact with the bot. You should only disable authorization if you trust all users on any server where the bot is on."); } if (this.config.enableAuthorization && this.config.authorizedUsers.length === 0 && this.config.authorizedServerRoles.length === 0) { this.log.info("Authorization is enabled but no authorized users are defined!"); } if (this.config.enableCustomCommands && !Array.isArray(this.config.customCommands)) { this.config.customCommands = []; } this.config.reactOnMentionsEmoji = this.config.reactOnMentionsEmoji?.trim() || "\u{1F44D}"; const botActivityTypeObj = await this.getObjectAsync("bot.activityType"); if (botActivityTypeObj?.common?.states?.PLAYING) { delete botActivityTypeObj.common.states.PLAYING; delete botActivityTypeObj.common.states.STREAMING; delete botActivityTypeObj.common.states.LISTENING; delete botActivityTypeObj.common.states.WATCHING; delete botActivityTypeObj.common.states.COMPETING; await this.setObjectAsync("bot.activityType", botActivityTypeObj); } if (this.config.enableRawStates) { await this.extendObject("raw", { type: "channel", common: { name: import_i18n.i18n.getStringOrTranslated("Raw data") }, native: {} }); await Promise.all([ this.extendObject("raw.interactionJson", { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Last interaction JSON data"), role: "json", type: "string", read: true, write: false, def: "" }, native: {} }), this.extendObject("raw.messageJson", { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Last message JSON data"), role: "json", type: "string", read: true, write: false, def: "" }, native: {} }) ]); } else { await this.delObjectAsync("raw", { recursive: true }); } if (this.config.enableCustomCommands) { await this.extendObject("slashCommands", { type: "channel", common: { name: import_i18n.i18n.getStringOrTranslated("Custom Discord slash commands") }, native: {} }); } else { await this.delObjectAsync("slashCommands", { recursive: true }); } this.client = new import_discord.Client({ intents: [ import_discord.GatewayIntentBits.Guilds, import_discord.GatewayIntentBits.GuildMembers, import_discord.GatewayIntentBits.GuildMessages, import_discord.GatewayIntentBits.GuildMessageReactions, import_discord.GatewayIntentBits.GuildMessageTyping, import_discord.GatewayIntentBits.GuildPresences, import_discord.GatewayIntentBits.GuildVoiceStates, import_discord.GatewayIntentBits.DirectMessages, import_discord.GatewayIntentBits.DirectMessageReactions, import_discord.GatewayIntentBits.DirectMessageTyping ], partials: [ import_discord.Partials.Channel // needed for DMs ] }); this.client.on("ready", this.onClientReady); if (this.log.level === "silly") { this.client.on("debug", (message) => this.log.silly(`discordjs: ${message}`)); } this.client.on("warn", (message) => this.log.warn(`Discord client warning: ${message}`)); this.client.on("error", (err) => this.log.error(`Discord client error: ${err.toString()}`)); this.client.on("rateLimit", (rateLimitData) => this.log.debug(`Discord client rate limit hit: ${JSON.stringify(rateLimitData)}`)); this.client.on("invalidRequestWarning", (invalidRequestWarningData) => this.log.warn(`Discord client invalid request warning: ${JSON.stringify(invalidRequestWarningData)}`)); this.client.on("invalidated", () => { this.log.warn("Discord client session invalidated"); void this.setInfoConnectionState(false); }); this.client.on("shardError", (err, shardId) => { let errorMsg; if (err instanceof AggregateError) { errorMsg = Array.from(new Set(err.errors.map((e) => e.code))).join(", "); } else { errorMsg = err.toString(); } if (this.isShardError !== errorMsg) { this.isShardError = errorMsg; this.log.warn(`Discord client websocket error (shardId:${shardId}): ${errorMsg}`); void this.setInfoConnectionState(false); } else { this.log.debug(`Discord client websocket error (shardId:${shardId}): ${errorMsg}`); } }); this.client.on("shardReady", (shardId) => { this.isShardError = false; this.log.info(`Discord client websocket connected (shardId:${shardId})`); void this.setInfoConnectionState(true); void this.setBotPresence(); }); this.client.on("shardResume", (shardId, replayedEvents) => this.log.debug(`Discord client websocket resume (shardId:${shardId} replayedEvents:${replayedEvents})`)); this.client.on("shardDisconnect", (event, shardId) => this.log.debug(`Discord client websocket disconnect (shardId:${shardId} code:${event.code})`)); this.client.on("shardReconnecting", (shardId) => this.log.debug(`Discord client websocket reconnecting (shardId:${shardId})`)); this.client.on("messageCreate", this.onClientMessageCreate); if (this.config.dynamicServerUpdates) { this.client.on("channelCreate", () => this.updateGuilds()); this.client.on("channelDelete", () => this.updateGuilds()); this.client.on("channelUpdate", () => this.updateGuilds()); this.client.on("guildCreate", () => this.updateGuilds()); this.client.on("guildDelete", () => this.updateGuilds()); this.client.on("guildUpdate", () => this.updateGuilds()); this.client.on("guildMemberAdd", () => this.updateGuilds()); this.client.on("guildMemberRemove", () => this.updateGuilds()); this.client.on("roleCreate", () => this.updateGuilds()); this.client.on("roleDelete", () => this.updateGuilds()); this.client.on("roleUpdate", () => this.updateGuilds()); this.client.on("userUpdate", () => this.updateGuilds()); } if (this.config.observeUserPresence) { this.client.on("presenceUpdate", (_oldPresence, newPresence) => this.updateUserPresence(newPresence.userId, newPresence)); } if (this.config.observeUserVoiceState) { this.client.on("voiceStateUpdate", this.onClientVoiceStateUpdate); } await this.discordSlashCommands.onReady(); this.subscribeStates("servers.*.channels.*.send"); this.subscribeStates("servers.*.channels.*.sendFile"); this.subscribeStates("servers.*.channels.*.sendReply"); this.subscribeStates("servers.*.channels.*.sendReaction"); this.subscribeStates("users.*.send"); this.subscribeStates("users.*.sendFile"); this.subscribeStates("users.*.sendReply"); this.subscribeStates("users.*.sendReaction"); this.subscribeStates("servers.*.members.*.voiceDisconnect"); this.subscribeStates("servers.*.members.*.voiceServerMute"); this.subscribeStates("servers.*.members.*.voiceServerDeaf"); this.subscribeStates("slashCommands.*.sendReply"); this.subscribeStates("slashCommands.*.option-*.choices"); this.subscribeStates("bot.*"); this.subscribeForeignObjects("*"); this.log.debug("Get all objects with custom config ..."); const view = await this.getObjectViewAsync("system", "custom", {}); if (view?.rows) { for (const item of view.rows) { await this.setupObjCustom(item.id, item.value?.[this.namespace]); } } this.log.debug("Getting all objects with custom config done"); this.initialCustomObjectSetupDone = true; const loginResult = await this.loginClient(); if (loginResult !== true) { if (loginResult.includes("TOKEN_INVALID")) { this.terminate("Invalid token, please check your configuration!", import_adapter_core.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); } else { this.terminate("No connection to Discord", import_adapter_core.EXIT_CODES.START_IMMEDIATELY_AFTER_STOP); } return; } await this.discordSlashCommands.registerSlashCommands(); } /** * Try to log in the discord client. * * This will also handle network related errors. * In case of networt related errors this will retry the login after some time. * The wait time before each retry will be increased for each try, as defined * in `LOGIN_WAIT_TIMES`. * @param tryNr Number of the login try. Should be `0` when login process is started and is increased internally in each try. * @returns Promise which resolves to `true` if logged in, or an error name/message otherwise. */ async loginClient(tryNr = 0) { if (!this.client || this.unloaded) { return "No Client"; } try { await this.client.login(this.config.token); return true; } catch (err) { if (err instanceof Error) { let errorMsg; if (err instanceof AggregateError) { errorMsg = Array.from(new Set(err.errors.map((e) => e.code))).join(", "); } else { errorMsg = err.toString(); } if (tryNr < 4) { this.log.info(`Discord login error: ${errorMsg}`); } else { this.log.warn(`Discord login error: ${errorMsg}`); } if (err.name === "AbortError" || err.name === "ConnectTimeoutError" || err.code === "EAI_AGAIN" || err instanceof AggregateError && err.errors.map((e) => e.code).includes("ENETUNREACH")) { tryNr++; if (tryNr >= LOGIN_WAIT_TIMES.length) { tryNr = LOGIN_WAIT_TIMES.length - 1; } this.log.info(`Wait ${LOGIN_WAIT_TIMES[tryNr] / 1e3} seconds before next login try (#${tryNr + 1}) ...`); await (0, import_promises.setTimeout)(LOGIN_WAIT_TIMES[tryNr], void 0, { ref: false }); return await this.loginClient(tryNr); } return err.name; } else { this.log.error("Unknown Discord login error"); return "Unknown Discord login error"; } } } async onClientReady() { if (!this.client?.user) { this.log.error("Discord client has no user!"); return; } this.log.info(`Logged in as ${this.client.user.tag}!`); this.log.debug(`User ID: ${this.client.user.id}`); if (this.config.botName) { if (this.client.user.username !== this.config.botName) { this.log.debug(`Update of bot name needed - current name: ${this.client.user.username} - configured name: ${this.config.botName}`); try { const proms = []; proms.push(this.client.user.setUsername(this.config.botName)); for (const [, guild] of this.client.guilds.cache) { const me = guild.members.cache.get(this.client.user.id); if (me) { proms.push(me.setNickname(this.config.botName)); } } await Promise.all(proms); this.log.debug(`Bot name updated`); } catch (err) { this.log.warn(`Error setting the bot name to "${this.config.botName}": ${err}`); } } else { this.log.debug("Bot name is up to date"); } } try { await this.updateGuilds(); } catch (err) { this.log.error(`Error while updating server information: ${err}`); } } /** * Update the guilds (servers), channels and users seen by the discord bot. * This will create/update all dynamic objects for all servers and users if needed. */ async updateGuilds() { if (!this.client?.user) { throw new Error("Client not loaded"); } const allServersUsers = new import_discord.Collection(); const knownServersAndChannelsIds = /* @__PURE__ */ new Set(); if (this.unloaded) return; const guilds = await this.client.guilds.fetch(); if (this.unloaded) return; for (const [, guildBase] of guilds) { if (this.unloaded) return; let guild; try { guild = await guildBase.fetch(); } catch (err) { this.log.warn(`Could not fetch guild information for guild "${guildBase.name}" id:${guildBase.id}`); this.log.debug(`Error: ${err}`); continue; } if (this.unloaded) return; knownServersAndChannelsIds.add(`${this.namespace}.servers.${guild.id}`); await this.extendObjectCached(`servers.${guild.id}`, { type: "channel", common: { name: guild.name }, native: {} }); await Promise.all([ this.extendObjectCached(`servers.${guild.id}.members`, { type: "channel", common: { name: import_i18n.i18n.getStringOrTranslated("Members") }, native: {} }), this.extendObjectCached(`servers.${guild.id}.channels`, { type: "channel", common: { name: import_i18n.i18n.getStringOrTranslated("Channels") }, native: {} }) ]); const guildMembers = await guild.members.fetch(); if (this.unloaded) return; for (const [, member] of guildMembers) { if (member.user.id !== this.client.user.id) { allServersUsers.set(member.user.id, { user: member.user, presence: member.presence }); } await this.extendObjectCached(`servers.${guild.id}.members.${member.id}`, { type: "channel", common: { name: `${member.displayName} (${(0, import_utils.userNameOrTag)(member.user)})` }, native: {} }); await Promise.all([ this.extendObjectCached(`servers.${guild.id}.members.${member.id}.tag`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("User tag"), role: "text", type: "string", read: true, write: false, def: "" }, native: {} }), this.extendObjectCached(`servers.${guild.id}.members.${member.id}.name`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("User name"), role: "text", type: "string", read: true, write: false, def: "" }, native: {} }), this.extendObjectCached(`servers.${guild.id}.members.${member.id}.displayName`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Display name"), role: "text", type: "string", read: true, write: false, def: "" }, native: {} }), this.extendObjectCached(`servers.${guild.id}.members.${member.id}.roles`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Roles"), role: "text", type: "string", read: true, write: false, def: "" }, native: {} }), this.extendObjectCached(`servers.${guild.id}.members.${member.id}.joinedAt`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Joined at"), role: "date", type: "number", read: true, write: false, def: 0 }, native: {} }), this.extendObjectCached(`servers.${guild.id}.members.${member.id}.voiceChannel`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Voice channel"), role: "text", type: "string", read: true, write: false, def: "" }, native: {} }), this.extendObjectCached(`servers.${guild.id}.members.${member.id}.voiceDisconnect`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Voice disconnect"), role: "button", type: "boolean", read: false, write: true, def: false }, native: {} }), this.extendObjectCached(`servers.${guild.id}.members.${member.id}.voiceSelfDeaf`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Voice self deafen"), role: "indicator", type: "boolean", read: true, write: false, def: false }, native: {} }), this.extendObjectCached(`servers.${guild.id}.members.${member.id}.voiceServerDeaf`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Voice server deafen"), role: "switch", type: "boolean", read: true, write: true, def: false }, native: {} }), this.extendObjectCached(`servers.${guild.id}.members.${member.id}.voiceSelfMute`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Voice self mute"), role: "indicator", type: "boolean", read: true, write: false, def: false }, native: {} }), this.extendObjectCached(`servers.${guild.id}.members.${member.id}.voiceServerMute`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Voice server mute"), role: "switch", type: "boolean", read: true, write: true, def: false }, native: {} }), this.extendObjectCached(`servers.${guild.id}.members.${member.id}.json`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("JSON data"), role: "json", type: "string", read: true, write: false, def: "" }, native: {} }) ]); const memberRoles = member.roles.cache.map((role) => role.name); await Promise.all([ this.setState(`servers.${guild.id}.members.${member.id}.tag`, member.user.tag, true), this.setState(`servers.${guild.id}.members.${member.id}.name`, member.user.username, true), this.setState(`servers.${guild.id}.members.${member.id}.displayName`, member.displayName, true), this.setState(`servers.${guild.id}.members.${member.id}.roles`, memberRoles.join(", "), true), this.setState(`servers.${guild.id}.members.${member.id}.joinedAt`, member.joinedTimestamp, true), this.setState(`servers.${guild.id}.members.${member.id}.voiceChannel`, member.voice.channel?.name ?? "", true), this.setState(`servers.${guild.id}.members.${member.id}.voiceSelfDeaf`, !!member.voice.selfDeaf, true), this.setState(`servers.${guild.id}.members.${member.id}.voiceServerDeaf`, !!member.voice.serverDeaf, true), this.setState(`servers.${guild.id}.members.${member.id}.voiceSelfMute`, !!member.voice.selfMute, true), this.setState(`servers.${guild.id}.members.${member.id}.voiceServerMute`, !!member.voice.serverMute, true) ]); const json = { tag: member.user.tag, name: member.user.username, id: member.id, displayName: member.displayName, roles: memberRoles, joined: member.joinedTimestamp, voiceChannel: member.voice.channel?.name ?? "", voiceChannelId: member.voice.channel?.id ?? "", voiceSelfDeaf: !!member.voice.selfDeaf, voiceServerDeaf: !!member.voice.serverDeaf, voiceSelfMute: !!member.voice.selfMute, voiceServerMute: !!member.voice.serverMute }; if (!(0, import_node_util.isDeepStrictEqual)(json, this.jsonStateCache.get(`${this.namespace}.servers.${guild.id}.members.${member.id}.json`))) { await this.setState(`servers.${guild.id}.members.${member.id}.json`, JSON.stringify(json), true); this.jsonStateCache.set(`${this.namespace}.servers.${guild.id}.members.${member.id}.json`, json); } } if (this.unloaded) return; const channels = await guild.channels.fetch(); if (this.unloaded) return; for (const parents of [true, false]) { for (const [, channel] of channels) { if (!channel || parents && channel.parentId || !parents && !channel.parentId) { continue; } const channelIdPrefix = parents ? `servers.${guild.id}.channels.${channel.id}` : `servers.${guild.id}.channels.${channel.parentId}.channels.${channel.id}`; knownServersAndChannelsIds.add(`${this.namespace}.${channelIdPrefix}`); let icon; if (channel.type === import_discord.ChannelType.GuildText) { icon = "channel-text.svg"; } if (channel.type === import_discord.ChannelType.GuildVoice) { icon = "channel-voice.svg"; } await this.extendObjectCached(channelIdPrefix, { type: "channel", common: { name: channel.parent ? `${channel.parent.name} / ${channel.name}` : channel.name, icon }, native: { channelId: channel.id } }); if (channel.type === import_discord.ChannelType.GuildCategory) { await this.extendObjectCached(`${channelIdPrefix}.channels`, { type: "channel", common: { name: import_i18n.i18n.getStringOrTranslated("Channels") }, native: {} }); } await Promise.all([ this.extendObjectCached(`${channelIdPrefix}.json`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("JSON data"), role: "json", type: "string", read: true, write: false, def: "" }, native: {} }), this.extendObjectCached(`${channelIdPrefix}.memberCount`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Member count"), role: "value", type: "number", read: true, write: false, def: 0 }, native: {} }), this.extendObjectCached(`${channelIdPrefix}.members`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Members"), role: "text", type: "string", read: true, write: false, def: "" }, native: {} }) ]); if (channel.type === import_discord.ChannelType.GuildText || channel.type === import_discord.ChannelType.GuildVoice) { await Promise.all([ this.extendObjectCached(`${channelIdPrefix}.message`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Last message"), role: "text", type: "string", read: true, write: false, def: "" }, native: {} }), this.extendObjectCached(`${channelIdPrefix}.messageId`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Last message ID"), role: "text", type: "string", read: true, write: false, def: "" }, native: {} }), this.extendObjectCached(`${channelIdPrefix}.messageAuthor`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Last message author"), role: "text", type: "string", read: true, write: false, def: "" }, native: {} }), this.extendObjectCached(`${channelIdPrefix}.messageTimestamp`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Last message timestamp"), role: "date", type: "number", read: true, write: false, def: 0 }, native: {} }), this.extendObjectCached(`${channelIdPrefix}.messageJson`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Last message JSON data"), role: "json", type: "string", read: true, write: false, def: "" }, native: {} }), this.extendObjectCached(`${channelIdPrefix}.send`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Send message"), role: "text", type: "string", read: false, write: true, def: "" }, native: {} }), this.extendObjectCached(`${channelIdPrefix}.sendFile`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Send file"), role: "text", type: "string", read: false, write: true, def: "" }, native: {} }), this.extendObjectCached(`${channelIdPrefix}.sendReply`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Send reply"), role: "text", type: "string", read: false, write: true, def: "" }, native: {} }), this.extendObjectCached(`${channelIdPrefix}.sendReaction`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Send reaction"), role: "text", type: "string", read: false, write: true, def: "" }, native: {} }) ]); this.messageReceiveStates.add(`${this.namespace}.${channelIdPrefix}.message`); } await this.updateChannelInfoStates(channel); } } const objListMembers = await this.getObjectListAsync({ startkey: `${this.namespace}.servers.${guild.id}.members.`, endkey: `${this.namespace}.servers.${guild.id}.members.\u9999` }); const reServersMembers = new RegExp(`^${this.name}\\.${this.instance}\\.servers\\.${guild.id}.members\\.(\\d+)$`); for (const item of objListMembers.rows) { const m = item.id.match(reServersMembers); if (m) { const memberId = m[1]; if (!guild.members.cache.has(memberId)) { this.log.debug(`Server member ${memberId} of server ${guild.id} is no longer available - deleting objects`); this.jsonStateCache.delete(`${this.namespace}.servers.${guild.id}.members.${memberId}.json`); await this.delObjectAsyncCached(`servers.${guild.id}.members.${memberId}`, { recursive: true }); } } } } for (const [, { user, presence }] of allServersUsers) { this.log.debug(`Known user: ${user.tag} id:${user.id}`); await this.extendObjectCached(`users.${user.id}`, { type: "channel", common: { name: (0, import_utils.userNameOrTag)(user) }, native: { userId: user.id } }); await Promise.all([ this.extendObjectCached(`users.${user.id}.json`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("JSON data"), role: "json", type: "string", read: true, write: false, def: "" }, native: {} }), this.extendObjectCached(`users.${user.id}.tag`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("User tag"), role: "text", type: "string", read: true, write: false, def: "" }, native: {} }), this.extendObjectCached(`users.${user.id}.name`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("User name"), role: "text", type: "string", read: true, write: false, def: "" }, native: {} }), this.extendObjectCached(`users.${user.id}.message`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Last message"), role: "text", type: "string", read: true, write: false, def: "" }, native: {} }), this.extendObjectCached(`users.${user.id}.messageId`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Last message ID"), role: "text", type: "string", read: true, write: false, def: "" }, native: {} }), this.extendObjectCached(`users.${user.id}.messageTimestamp`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Last message timestamp"), role: "date", type: "number", read: true, write: false, def: 0 }, native: {} }), this.extendObjectCached(`users.${user.id}.messageJson`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Last message JSON data"), role: "json", type: "string", read: true, write: false, def: "" }, native: {} }), this.extendObjectCached(`users.${user.id}.send`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Send message"), role: "text", type: "string", read: false, write: true, def: "" }, native: {} }), this.extendObjectCached(`users.${user.id}.sendFile`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Send file"), role: "text", type: "string", read: false, write: true, def: "" }, native: {} }), this.extendObjectCached(`users.${user.id}.sendReply`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Send reply"), role: "text", type: "string", read: false, write: true, def: "" }, native: {} }), this.extendObjectCached(`users.${user.id}.sendReaction`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Send reaction"), role: "text", type: "string", read: false, write: true, def: "" }, native: {} }), this.extendObjectCached(`users.${user.id}.avatarUrl`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Avatar"), role: "media.link", type: "string", read: true, write: false, def: "" }, native: {} }), this.extendObjectCached(`users.${user.id}.bot`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Bot"), role: "indicator", type: "boolean", read: true, write: false, def: false }, native: {} }), this.extendObjectCached(`users.${user.id}.status`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Status"), role: "text", type: "string", read: true, write: false, def: "" }, native: {} }), this.extendObjectCached(`users.${user.id}.activityType`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Activity type"), role: "text", type: "string", read: true, write: false, def: "" }, native: {} }), this.extendObjectCached(`users.${user.id}.activityName`, { type: "state", common: { name: import_i18n.i18n.getStringOrTranslated("Activity name"), role: "text", type: "string", read: true, write: false, def: "" }, native: {} }) ]); this.messageReceiveStates.add(`${this.namespace}.users.${user.id}.message`); this.knownUsers.add(user.id); const ps = await this.updateUserPresence(user.id, presence, true); const proms = []; const json = { id: user.id, tag: user.tag, name: user.username, activityName: ps.activityName, activityType: ps.activityType, avatarUrl: user.displayAvatarURL(), bot: user.bot, status: ps.status }; if (!(0, import_node_util.isDeepStrictEqual)(json, this.jsonStateCache.get(`${this.namespace}.users.${user.id}.json`))) { proms.push(this.setState(`users.${user.id}.json`, JSON.stringify(json), true)); this.jsonStateCache.set(`${this.namespace}.users.${user.id}.json`, json); } await Promise.all([ this.setState(`users.${user.id}.tag`, user.tag, true), this.setState(`users.${user.id}.name`, user.username, true), this.setState(`users.${user.id}.avatarUrl`, json.avatarUrl, true), this.setState(`users.${user.id}.bot`, user.bot, true), ...proms, this.updateUserPresence(user.id, presence) ]); } const objListServers = await this.getObjectListAsync({ startkey: `${this.namespace}.servers.`, endkey: `${this.namespace}.servers.\u9999` }); const reServersChannels = new RegExp(`^${this.name}\\.${this.instance}\\.servers\\.((\\d+)(\\.channels\\.(\\d+)){0,2})$`); for (const item of objListServers.rows) { const m = item.id.match(reServersChannels); if (m) { const idPath = m[1]; if (!knownServersAndChannelsIds.has(item.id)) { this.log.debug(`Server/Channel ${idPath} "${(0, import_utils.getObjName)(item.value.common)}" is no longer available - deleting objects`); this.messageReceiveStates.delete(`${this.namespace}.servers.${idPath}.message`); this.jsonStateCache.delete(`${this.namespace}.servers.${idPath}.json`); await this.delObjectAsyncCached(`servers.${idPath}`, { recursive: true }); } } } const objListUsers = await this.getObjectListAsync({ startkey: `${this.namespace}.users.`, endkey: `${this.namespace}.users.\u9999` }); const reUsers = new RegExp(`^${this.name}\\.${this.instance}\\.users\\.(\\d+)$`); for (const item of objListUsers.rows) { const m = item.id.match(reUsers); if (m) { const userId = m[1]; if (!allServersUsers.has(userId)) { this.log.debug(`User ${userId} "${(0, import_utils.getObjName)(item.value.common)}" is no longer available - deleting objects`); this.knownUsers.delete(userId); this.messageReceiveStates.delete(`${this.namespace}.users.${userId}.message`); this.jsonStateCache.delete(`${this.namespace}.users.${userId}.json`); await this.delObjectAsyncCached(`users.${userId}`, { recursive: true }); } } } } /** * Update the states containing basic information about a channel. * * This includes the following channel states: `.json`, `.memberCount`, `.members` * @param channel The channel to update the states for. */ async updateChannelInfoStates(channel) { const channelIdPrefix = channel.parentId ? `servers.${channel.guildId}.channels.${channel.parentId}.channels.${channel.id}` : `servers.${channel.guild.id}.channels.${channel.id}`; const members = [...channel.members.values()]; const json = { id: channel.id, name: channel.name, type: import_discord.ChannelType[channel.type], memberCount: members.length, members: members.map((m) => ({ id: m.user.id, tag: m.user.tag, name: m.user.username, displayName: m.displayName })) }; const proms = []; if (!(0, import_node_util.isDeepStrictEqual)(json, this.jsonStateCache.get(`${this.namespace}.${channelIdPrefix}.json`))) { proms.push(this.setState(`${channelIdPrefix}.json`, JSON.stringify(json), true)); this.jsonStateCache.set(`${this.namespace}.${channelIdPrefix}.json`, json); } await Promise.all([ this.setState(`${channelIdPrefix}.memberCount`, members.length, true), this.setState(`${channelIdPrefix}.members`, members.map((m) => m.displayName).join(", "), true), ...proms ]); } /** * Update the presence states of a user. * @param userId ID of the user. * @param presence The user presence. * @param skipJsonStateUpdate If the json state of the user should not be updated. */ async updateUserPresence(userId, presence, skipJsonStateUpdate = false) { if (!this.config.observeUserPresence) { return { activityName: "", activityType: "", status: "" }; } if (!this.knownUsers.has(userId)) { this.log.debug(`Can't update user presence for unknown user ${userId}`); return { activityName: "", activityType: "", status: "" }; } try { const p = { status: presence?.status ?? "", activityName: (presence?.activities[0]?.type === import_discord.ActivityType.Custom ? presence?.activities[0]?.state : presence?.activities[0]?.name) ?? "", activityType: (presence?.activities[0]?.type !== void 0 ? import_discord.ActivityType[presence.activities[0].type] : "") ?? "" }; const proms = []; if (!skipJsonStateUpdate) { const json = this.jsonStateCache.get(`${this.namespace}.users.${userId}.json`); if (json) { json.status = p.status; json.activityName = p.activityName; json.activityType = p.activityType; this.jsonStateCache.set(`${this.namespace}.users.${userId}.json`, json); proms.push(this.setState(`users.${userId}.json`, JSON.stringify(json), true)); } } await Promise.all([ this.setState(`users.${userId}.status`, p.status, true), this.setState(`users.${userId}.activityName`, p.activityName, true), this.setState(`users.${userId}.activityType`, p.activityType, true), ...proms ]); return p; } catch (err) { this.log.warn(`Error while updating user presence of user ${userId}: ${err}`); return { activityName: "", activityType: "", status: "" }; } } /** * Set the presence status of the discord bot. */ async set