UNPKG

discord-reactions-roles

Version:

A simple and powerful module for adding a reaction role system to your discord.js project

551 lines (550 loc) 22 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ReactionsRoles = void 0; // Import module requirements const enmap_1 = __importDefault(require("enmap")); const discord_js_1 = require("discord.js"); // Import module utils const Utils_1 = require("./Utils"); // Import module emitter const Emitter_1 = require("./Emitter"); // Import module enums const Enums_1 = require("./Enums"); // Import package information const package_json_1 = require("../package.json"); /** * Class that controls Reaction Roles System * * @class * @classdesc ReactionRoles Class * @extends {Emitter} */ class ReactionsRoles extends Emitter_1.Emitter { client; author; website; version; ready; readyTimestamp; options; database; utils; /** * @constructor * * @param {Client} client Discord Client * @param {Options} [options] Module Options */ constructor(client, options) { super(); /** * Discord Client * * @type {Client} * @private */ this.client = client; /** * Module Author * * @type {string} */ this.author = package_json_1.author; /** * Module Website URL * * @type {string} */ this.website = package_json_1.homepage; /** * Module Version * * @type {string} */ this.version = package_json_1.version; /** * Module Ready Status * * @type {boolean} */ this.ready = null; /** * Module Ready Timestamp * * @type {number} */ this.readyTimestamp = null; /** * Module Options * * @type {Options} */ this.options = null; /** * Module Database * * @type {Enmap<string, GuildReactionsRoles[]>} */ this.database = null; /** * Module Utils * * @type {Utils} */ this.utils = new Utils_1.Utils(this); this.init(options); } /** * Method for creating new menus to distribute roles by reaction * * @param {TextChannel} channel Discord Guild text Channel * @param {CreateReactionsRolesData} options Reactions Roles Options * * @returns {Promise<GuildReactionsRoles>} Object with the data of the created menu */ create(channel, options) { return new Promise(async (res, rej) => { const parsedEmoji = this.utils.parseEmoji(channel.guild.id, options.emoji); if (!parsedEmoji.isValid) return rej(new Error(`Unknown emoji with content '${options.emoji}'!`)); if (options.role?.managed) return rej(new Error(`Role '${options.role?.name}' is controlled by an external application!`)); const defaultMessages = { started: "A new role giveaway for reactions has been launched!", ended: "This role menu has been removed and is no longer active!" }; const embed = new discord_js_1.EmbedBuilder(); embed.setColor(options.embed?.color ?? "Random"); embed.setAuthor(options.embed?.author ?? { name: channel.guild.name, iconURL: channel.guild.iconURL() }); embed.setThumbnail(options.embed?.thumbnail?.url ?? channel.guild.iconURL()); embed.setImage(options.embed?.image?.url); embed.setDescription(options.messages?.started ?? defaultMessages.started); embed.setFooter(options.embed?.footer ?? { text: channel.guild.name, iconURL: channel.guild.iconURL() }); embed.setTimestamp(); embed.data.description += `\n\n${parsedEmoji.raw} - ${options.role}`; const message = await channel.send({ content: options.mentionEveryone ? "@everyone" : "", embeds: [embed] }); await message.react(parsedEmoji.raw); const newMenu = { author: options?.author ?? this.client.user.id, created: Date.now(), updated: null, messages: { started: options.messages?.started ?? defaultMessages.started, ended: options.messages?.ended ?? defaultMessages.ended }, channelID: channel.id, messageID: message.id, data: [ { id: parsedEmoji.id, name: parsedEmoji.name, roleID: options.role?.id, raw: parsedEmoji.raw, added: Date.now(), animated: parsedEmoji.animated } ] }; const guildMenus = this.database.get(channel.guild.id); if (guildMenus?.length >= 1) { guildMenus.push(newMenu); this.database.set(channel.guild.id, guildMenus); } else this.database.set(channel.guild.id, [newMenu]); return res(newMenu); }); } /** * Method for retrieving role menu data by reaction * * @param {string} guildID Discord Guild ID * @param {string} messageID Discord Guild Message ID * * @returns {Promise<GuildReactionsRoles>} Returns an object with role data by reaction */ get(guildID, messageID) { return new Promise(async (res, rej) => { const guild = this.client.guilds.cache.get(guildID); if (!guild) return rej(new Error(`Unknown guild with ID '${guildID}'!`)); const guildMenus = this.database.get(guildID); if (guildMenus?.length <= 0) return rej(new Error(`Unknown menus with Guild ID '${guildID}'!`)); const reactionRolesMenu = guildMenus.find(menu => menu.messageID === messageID); if (!reactionRolesMenu) return rej(new Error(`Unknown menu with message ID '${messageID}'!`)); return res(reactionRolesMenu); }); } /** * Method to get a list of menu roles by reaction for the server * * @param {string} guildID Discord Guild ID * * @returns {Promise<Array<GuildReactionsRoles>>} Returns the list of role menus by server response */ list(guildID) { return new Promise(async (res, rej) => { const guild = this.client.guilds.cache.get(guildID); if (!guild) return rej(new Error(`Unknown guild with ID '${guildID}'!`)); const guildMenus = this.database.get(guildID); if (guildMenus?.length <= 0) return rej(new Error(`Unknown menus with Guild ID '${guildID}'!`)); return res(guildMenus); }); } /** * Method for changing the menu of roles by reaction (adding/removing reactions) * * @param {string} guildID Discord Guild ID * @param {string} messageID Discord Guild Message ID * @param {EditMode} mode Edit Mode * @param {EditReactionsRolesData} options Edit Options * * @returns {Promise<GuildReactionsRoles>} Modified menu data object */ edit(guildID, messageID, mode, options) { return new Promise(async (res, rej) => { const guild = this.client.guilds.cache.get(guildID); if (!guild) return rej(new Error(`Unknown guild with ID '${guildID}'!`)); const guildMenus = this.database.get(guildID); if (guildMenus?.length <= 0) return rej(new Error(`Unknown menus with Guild ID '${guildID}'!`)); const reactionRolesMenu = guildMenus.find(menu => menu.messageID === messageID); if (!reactionRolesMenu) return rej(new Error(`Unknown menu with message ID '${messageID}'!`)); const parsedEmoji = this.utils.parseEmoji(guildID, options.emoji); if (!parsedEmoji.isValid) return rej(new Error(`Unknown emoji with content '${options.emoji}'!`)); const reactionRoleData = reactionRolesMenu.data; const menuMessage = await this.utils.getMessage(guildID, reactionRolesMenu.channelID, messageID); switch (mode) { case Enums_1.EditMode.ADD: { if (options.role?.managed) return rej(new Error(`Role '${options.role?.name}' is controlled by an external application!`)); if (reactionRoleData.find(data => data.roleID === options.role?.id || data.raw === parsedEmoji.raw)) return rej(new Error(`The reaction/role '${options.emoji}/${options.role}' is already in the menu with the message ID '${messageID}'!`)); const reactionRole = { id: parsedEmoji.id, name: parsedEmoji.name, roleID: options.role?.id, raw: parsedEmoji.raw, added: Date.now(), animated: parsedEmoji.animated }; if (menuMessage) { try { const embed = discord_js_1.EmbedBuilder.from(menuMessage.embeds[0]); embed.data.description += `\n${parsedEmoji.raw} - ${options.role}`; await menuMessage.react(parsedEmoji.raw); await menuMessage.edit({ embeds: [embed] }); } catch (error) { return res(null); } reactionRoleData.push(reactionRole); reactionRolesMenu.updated = Date.now(); this.database.set(guildID, guildMenus); return res(reactionRolesMenu); } else return res(null); } case Enums_1.EditMode.REMOVE: { if (!reactionRoleData.find(data => data.roleID === options.role?.id && data.raw === parsedEmoji.raw)) return rej(new Error(`Reaction/role '${options.emoji}/${options.role}' not found in menu with message ID '${messageID}'!`)); const editedReactionRoleData = reactionRoleData.filter(data => data.raw != parsedEmoji.raw && data.roleID != options.role?.id); if (editedReactionRoleData.length <= 0) { this.remove(guildID, messageID); return res(reactionRolesMenu); } reactionRolesMenu.data = editedReactionRoleData; if (menuMessage) { try { let description = reactionRolesMenu.messages.started + "\n\n"; const embed = discord_js_1.EmbedBuilder.from(menuMessage.embeds[0]); await menuMessage.reactions.cache.get(parsedEmoji?.id ?? parsedEmoji.raw).remove(); for (let i = 0; i < reactionRolesMenu.data.length; i++) { const data = reactionRolesMenu.data[i]; await menuMessage.react(data.raw); description += `${data.raw} - <@&${data.roleID}>\n`; } embed.data.description = description; this.database.set(guildID, guildMenus); await menuMessage.edit({ embeds: [embed] }); } catch (error) { return res(null); } return res(reactionRolesMenu); } else return res(null); } } }); } /** * Method for removing server menus of roles by reaction * * @param {string} guildID Discord Guild ID * @param {string} messageID Discord Guild Message ID * * @returns {Promise<boolean>} Deletion state */ remove(guildID, messageID) { return new Promise(async (res, rej) => { const guild = this.client.guilds.cache.get(guildID); if (!guild) return rej(new Error(`Unknown guild with ID '${guildID}'!`)); const guildMenus = this.database.get(guildID); if (guildMenus?.length <= 0) return rej(new Error(`Unknown menus with Guild ID '${guildID}'!`)); const reactionRolesMenu = guildMenus.find(menu => menu.messageID === messageID); if (!reactionRolesMenu) return rej(new Error(`Unknown menu with message ID '${messageID}'!`)); const menuMessage = await this.utils.getMessage(guildID, reactionRolesMenu.channelID, reactionRolesMenu.messageID); if (menuMessage) { if (this.options.deleteOldMenus) { try { await menuMessage.delete(); } catch (error) { return res(false); } } else { const embed = discord_js_1.EmbedBuilder.from(menuMessage.embeds[0]); if (this.options.deleteOldMenusReactions) { try { await menuMessage.reactions.removeAll(); } catch (error) { return res(false); } } try { embed.data.description = reactionRolesMenu.messages.ended; await menuMessage.edit({ embeds: [embed] }); } catch (error) { return res(false); } } this.database.set(guildID, guildMenus.filter(guildMenu => guildMenu.messageID != messageID)); return res(true); } else return res(false); }); } /** * Method for module initialization * * @param {Options} [options] Module Options * * @private */ init(options) { const interval = setInterval(async () => { if (this.client.isReady()) { this.utils.fetchIntents(); this.utils.fetchPartials(); this.ready = true; this.readyTimestamp = Date.now(); this.options = this.utils.fetchOptions(options ?? {}); this.initDatabase(); this.monitorEvents(); this.emit(Enums_1.Events.READY, this); clearInterval(interval); } }, 100); } /** * Method for module database initialization * * @private */ initDatabase() { this.database = new enmap_1.default({ name: this.options.database.name, dataDir: this.options.database.path, wal: false }); } /** * Method for starting client event tracking * * @private */ monitorEvents() { this.client.on("messageReactionAdd", async (reaction, user) => { if (!user.bot) { let message; if (reaction.message.partial) message = await reaction.message.fetch(); else message = reaction.message; const guildMenus = this.database.get(message.guild.id); if (guildMenus?.length >= 1) { const reactionsRoles = guildMenus.find(guildMenu => guildMenu.messageID === message.id); if (reactionsRoles?.created) { const react = reaction.emoji; const reactionRolesData = reactionsRoles.data; const reactRaw = react?.id ? `<${react?.animated ? "a" : ""}:${react.name}:${react.id}>` : react.name; const reactionRole = reactionRolesData.find(data => data.raw === reactRaw); if (reactionRole?.raw) { try { const guildMember = await this.utils.getMember(message.guild.id, user.id); if (!guildMember?.roles?.cache?.has(reactionRole.roleID)) { await guildMember?.roles?.add(reactionRole.roleID); this.emit(Enums_1.Events.ROLE_ADDED, user, reactionRole); } } catch (error) { return; } } } } } }); this.client.on("messageReactionRemove", async (reaction, user) => { if (!user.bot) { let message; if (reaction.message.partial) message = await reaction.message.fetch(); else message = reaction.message; const guildMenus = this.database.get(message.guild.id); if (guildMenus?.length >= 1) { const reactionsRoles = guildMenus.find(guildMenu => guildMenu.messageID === message.id); if (reactionsRoles?.created) { const react = reaction.emoji; const reactionRolesData = reactionsRoles.data; const reactRaw = react?.id ? `<${react?.animated ? "a" : ""}:${react.name}:${react.id}>` : react.name; const reactionRole = reactionRolesData.find(data => data.raw === reactRaw); if (reactionRole?.raw) { try { const guildMember = await this.utils.getMember(message.guild.id, user.id); if (guildMember?.roles?.cache?.has(reactionRole.roleID)) { await guildMember?.roles?.remove(reactionRole.roleID); this.emit(Enums_1.Events.ROLE_REMOVED, user, reactionRole); } } catch (error) { return; } } } } } }); } } exports.ReactionsRoles = ReactionsRoles; /** * Emits when the module is ready * * @event ReactionsRoles#ready * * @param {ReactionsRoles} client Reactions Roles instance */ /** * Emitted when the user has received a role for clicking on a reaction * * @event ReactionsRoles#roleAdded * * @param {User} user Reaction User Data * @param {GuildReactionRole} data Reaction Role Data */ /** * Emitted when a user has lost a role for deleting a previously added reaction * * @event ReactionsRoles#roleRemoved * * @param {User} user Reaction User Data * @param {GuildReactionRole} data Reaction Role Data */ /** * Module Options * * @typedef {object} Options * * @prop {boolean} [deleteOldMenus=false] Delete inactive role menu messages by reaction? * @prop {boolean} [deleteOldMenusReactions=true] Remove reactions to inactive role menus by reaction? * @prop {object} [database] Module database configuration * @prop {string} [database.name=reactions_roles] Database name * @prop {string} [database.path=./] Database path */ /** * Guild Reactions Roles Data * * @typedef {object} GuildReactionsRoles * * @prop {string} author Menu author ID * @prop {number} created Create menu timestamp * @prop {number} updated Update menu timestamp * @prop {ReactionsRolesMessages} messages Menu Messages * @prop {string} channelID Menu channel ID * @prop {string} messageID Menu message ID * @prop {GuildReactionRole[]} data Menu reactions & roles data */ /** * Guild Reaction Role Data * * @typedef {object} GuildReactionRole * * @prop {string | null} id Reaction ID * @prop {string | null} name Reaction Name * @prop {string} raw Reaction Emoji Raw * @prop {string} roleID Reaction Role ID * @prop {number} added Reaction Role add timestamp * @prop {boolean} animated Reaction is animated? */ /** * Create Reaction Roles Menu Options * * @typedef {object} CreateReactionsRolesData * * @prop {boolean} [mentionEveryone=true] Mentione all users * @prop {Role} role Reactin Role Data * @prop {string} emoji Emoji Content * @prop {EmbedData} [embed] Discord Embed Options * @prop {string} [author] Reactions Roles Menu Author ID * @prop {ReactionsRolesMessages} [messages] Reaction Roles Menu Messages */ /** * Edit Reaction Roles Menu Options * * @typedef {object} EditReactionsRolesData * * @prop {Role} role Reaction Role Data * @prop {string} emoji Emoji Content */ /** * Reaction Roles Menu Messages * * @typedef {object} ReactionsRolesMessages * * @prop {string} [started] Reactions Roles Menu Start Message * @prop {string} [ended] Reactions Roles Menu End Message */ /** * Parsed Emoji Data * * @typedef {object} ParsedEmoji * * @prop {string} id Emoji ID * @prop {string} name Emoji Name * @prop {string} raw Emoji pointer as a string * @prop {boolean} isValid Is content emoji? * @prop {boolean} unicode Is emoji unicode? * @prop {boolean} animated Is the emoji animated? */