'use strict'; const process = require('node:process'); const { Collection } = require('@discordjs/collection'); const { Routes } = require('discord-api-types/v9'); const CachedManager = require('./CachedManager'); const { TypeError } = require('../errors'); const { Role } = require('../structures/Role'); const DataResolver = require('../util/DataResolver'); const PermissionsBitField = require('../util/PermissionsBitField'); const { resolveColor } = require('../util/Util'); const Util = require('../util/Util'); let cacheWarningEmitted = false; /** * Manages API methods for roles and stores their cache. * @extends {CachedManager} */ class RoleManager extends CachedManager { constructor(guild, iterable) { super(guild.client, Role, iterable); if (!cacheWarningEmitted && this._cache.constructor.name !== 'Collection') { cacheWarningEmitted = true; process.emitWarning( `Overriding the cache handling for ${this.constructor.name} is unsupported and breaks functionality.`, 'UnsupportedCacheOverwriteWarning', ); } /** * The guild belonging to this manager * @type {Guild} */ this.guild = guild; } /** * The role cache of this manager * @type {Collection} * @name RoleManager#cache */ _add(data, cache) { return super._add(data, cache, { extras: [this.guild] }); } /** * Obtains a role from Discord, or the role cache if they're already available. * @param {Snowflake} [id] The role's id * @param {BaseFetchOptions} [options] Additional options for this fetch * @returns {Promise>} * @example * // Fetch all roles from the guild * message.guild.roles.fetch() * .then(roles => console.log(`There are ${roles.size} roles.`)) * .catch(console.error); * @example * // Fetch a single role * message.guild.roles.fetch('222078108977594368') * .then(role => console.log(`The role color is: ${role.color}`)) * .catch(console.error); */ async fetch(id, { cache = true, force = false } = {}) { if (id && !force) { const existing = this.cache.get(id); if (existing) return existing; } // We cannot fetch a single role, as of this commit's date, Discord API throws with 405 const data = await this.client.api.guilds(this.guild.id).roles.get(); const roles = new Collection(); for (const role of data) roles.set(role.id, this._add(role, cache)); return id ? roles.get(id) ?? null : roles; } /** * Data that can be resolved to a Role object. This can be: * * A Role * * A Snowflake * @typedef {Role|Snowflake} RoleResolvable */ /** * Resolves a {@link RoleResolvable} to a {@link Role} object. * @method resolve * @memberof RoleManager * @instance * @param {RoleResolvable} role The role resolvable to resolve * @returns {?Role} */ /** * Resolves a {@link RoleResolvable} to a {@link Role} id. * @method resolveId * @memberof RoleManager * @instance * @param {RoleResolvable} role The role resolvable to resolve * @returns {?Snowflake} */ /** * Options used to create a new role. * @typedef {Object} CreateRoleOptions * @property {string} [name] The name of the new role * @property {ColorResolvable} [color] The data to create the role with * @property {boolean} [hoist] Whether or not the new role should be hoisted * @property {PermissionResolvable} [permissions] The permissions for the new role * @property {number} [position] The position of the new role * @property {boolean} [mentionable] Whether or not the new role should be mentionable * @property {?(BufferResolvable|Base64Resolvable|EmojiResolvable)} [icon] The icon for the role * The `EmojiResolvable` should belong to the same guild as the role. * If not, pass the emoji's URL directly * @property {?string} [unicodeEmoji] The unicode emoji for the role * @property {string} [reason] The reason for creating this role */ /** * Creates a new role in the guild with given information. * The position will silently reset to 1 if an invalid one is provided, or none. * @param {CreateRoleOptions} [options] Options for creating the new role * @returns {Promise} * @example * // Create a new role * guild.roles.create() * .then(console.log) * .catch(console.error); * @example * // Create a new role with data and a reason * guild.roles.create({ * name: 'Super Cool Blue People', * color: Colors.Blue, * reason: 'we needed a role for Super Cool People', * }) * .then(console.log) * .catch(console.error); */ async create(options = {}) { let { name, color, hoist, permissions, position, mentionable, reason, icon, unicodeEmoji } = options; color &&= resolveColor(color); if (typeof permissions !== 'undefined') permissions = new PermissionsBitField(permissions); if (icon) { const guildEmojiURL = this.guild.emojis.resolve(icon)?.url; icon = guildEmojiURL ? await DataResolver.resolveImage(guildEmojiURL) : await DataResolver.resolveImage(icon); if (typeof icon !== 'string') icon = undefined; } const data = await this.client.api.guilds(this.guild.id).roles.post({ body: { name, color, hoist, permissions, mentionable, icon, unicode_emoji: unicodeEmoji, }, reason, }) const { role } = this.client.actions.GuildRoleCreate.handle({ guild_id: this.guild.id, role: data, }); if (position) return this.setPosition(role, position, { reason }); return role; } /** * Edits a role of the guild. * @param {RoleResolvable} role The role to edit * @param {RoleData} data The new data for the role * @param {string} [reason] Reason for editing this role * @returns {Promise} * @example * // Edit a role * guild.roles.edit('222079219327434752', { name: 'buddies' }) * .then(updated => console.log(`Edited role name to ${updated.name}`)) * .catch(console.error); */ async edit(role, data, reason) { role = this.resolve(role); if (!role) throw new TypeError('INVALID_TYPE', 'role', 'RoleResolvable'); if (typeof data.position === 'number') { await this.setPosition(role, data.position, { reason }); } let icon = data.icon; if (icon) { const guildEmojiURL = this.guild.emojis.resolve(icon)?.url; icon = guildEmojiURL ? await DataResolver.resolveImage(guildEmojiURL) : await DataResolver.resolveImage(icon); if (typeof icon !== 'string') icon = undefined; } const body = { name: data.name, color: typeof data.color === 'undefined' ? undefined : resolveColor(data.color), hoist: data.hoist, permissions: typeof data.permissions === 'undefined' ? undefined : new PermissionsBitField(data.permissions), mentionable: data.mentionable, icon, unicode_emoji: data.unicodeEmoji, }; const d = await this.client.api.guilds(this.guild.id).roles(role.id).patch({ body, reason }); const clone = role._clone(); clone._patch(d); return clone; } /** * Deletes a role. * @param {RoleResolvable} role The role to delete * @param {string} [reason] Reason for deleting the role * @returns {Promise} * @example * // Delete a role * guild.roles.delete('222079219327434752', 'The role needed to go') * .then(() => console.log('Deleted the role')) * .catch(console.error); */ async delete(role, reason) { const id = this.resolveId(role); await this.client.api.guilds(this.guild.id).roles(id).delete({ reason }); this.client.actions.GuildRoleDelete.handle({ guild_id: this.guild.id, role_id: id }); } /** * Sets the new position of the role. * @param {RoleResolvable} role The role to change the position of * @param {number} position The new position for the role * @param {SetRolePositionOptions} [options] Options for setting the position * @returns {Promise} * @example * // Set the position of the role * guild.roles.setPosition('222197033908436994', 1) * .then(updated => console.log(`Role position: ${updated.position}`)) * .catch(console.error); */ async setPosition(role, position, { relative, reason } = {}) { role = this.resolve(role); if (!role) throw new TypeError('INVALID_TYPE', 'role', 'RoleResolvable'); const updatedRoles = await Util.setPosition( role, position, relative, this.guild._sortedRoles(), this.client, Routes.guildRoles(this.guild.id), reason, ); this.client.actions.GuildRolesPositionUpdate.handle({ guild_id: this.guild.id, roles: updatedRoles, }); return role; } /** * The data needed for updating a guild role's position * @typedef {Object} GuildRolePosition * @property {RoleResolvable} role The role's id * @property {number} position The position to update */ /** * Batch-updates the guild's role positions * @param {GuildRolePosition[]} rolePositions Role positions to update * @returns {Promise} * @example * guild.roles.setPositions([{ role: roleId, position: updatedRoleIndex }]) * .then(guild => console.log(`Role positions updated for ${guild}`)) * .catch(console.error); */ async setPositions(rolePositions) { // Make sure rolePositions are prepared for API rolePositions = rolePositions.map(o => ({ id: this.resolveId(o.role), position: o.position, })); // Call the API to update role positions await this.client.api.guilds(this.guild.id).roles.patch({ body: rolePositions }); return this.client.actions.GuildRolesPositionUpdate.handle({ guild_id: this.guild.id, roles: rolePositions, }).guild; } /** * Compares the positions of two roles. * @param {RoleResolvable} role1 First role to compare * @param {RoleResolvable} role2 Second role to compare * @returns {number} Negative number if the first role's position is lower (second role's is higher), * positive number if the first's is higher (second's is lower), 0 if equal */ comparePositions(role1, role2) { const resolvedRole1 = this.resolve(role1); const resolvedRole2 = this.resolve(role2); if (!resolvedRole1 || !resolvedRole2) throw new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake'); if (resolvedRole1.position === resolvedRole2.position) { return Number(BigInt(resolvedRole2.id) - BigInt(resolvedRole1.id)); } return resolvedRole1.position - resolvedRole2.position; } /** * Gets the managed role a user created when joining the guild, if any * Only ever available for bots * @param {UserResolvable} user The user to access the bot role for * @returns {?Role} */ botRoleFor(user) { const userId = this.client.users.resolveId(user); if (!userId) return null; return this.cache.find(role => role.tags?.botId === userId) ?? null; } /** * The `@everyone` role of the guild * @type {Role} * @readonly */ get everyone() { return this.cache.get(this.guild.id); } /** * The premium subscriber role of the guild, if any * @type {?Role} * @readonly */ get premiumSubscriberRole() { return this.cache.find(role => role.tags?.premiumSubscriberRole) ?? null; } /** * The role with the highest position in the cache * @type {Role} * @readonly */ get highest() { return this.cache.reduce((prev, role) => (role.comparePositionTo(prev) > 0 ? role : prev), this.cache.first()); } } module.exports = RoleManager;