From 7129965423e9fb333ca93cecba54b588073873fd Mon Sep 17 00:00:00 2001 From: Shubham Parihar Date: Sun, 3 Oct 2021 22:25:29 +0530 Subject: [PATCH] feat: add support for role icons (#6633) --- src/managers/RoleManager.js | 23 ++++++++++++++- src/structures/Guild.js | 1 + src/structures/Role.js | 57 ++++++++++++++++++++++++++++++++++++- src/util/Constants.js | 2 ++ typings/index.d.ts | 11 ++++++- 5 files changed, 91 insertions(+), 3 deletions(-) diff --git a/src/managers/RoleManager.js b/src/managers/RoleManager.js index b0c5b971c188..21e49bc66324 100644 --- a/src/managers/RoleManager.js +++ b/src/managers/RoleManager.js @@ -4,6 +4,7 @@ const { Collection } = require('@discordjs/collection'); const CachedManager = require('./CachedManager'); const { TypeError } = require('../errors'); const Role = require('../structures/Role'); +const DataResolver = require('../util/DataResolver'); const Permissions = require('../util/Permissions'); const { resolveColor, setPosition } = require('../util/Util'); @@ -104,6 +105,10 @@ class RoleManager extends CachedManager { * @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 */ @@ -128,9 +133,14 @@ class RoleManager extends CachedManager { * .catch(console.error); */ async create(options = {}) { - let { name, color, hoist, permissions, position, mentionable, reason } = options; + let { name, color, hoist, permissions, position, mentionable, reason, icon, unicodeEmoji } = options; color &&= resolveColor(color); if (typeof permissions !== 'undefined') permissions = new Permissions(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({ data: { @@ -139,6 +149,8 @@ class RoleManager extends CachedManager { hoist, permissions, mentionable, + icon, + unicode_emoji: unicodeEmoji, }, reason, }); @@ -182,12 +194,21 @@ class RoleManager extends CachedManager { }); } + 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 _data = { name: data.name, color: typeof data.color === 'undefined' ? undefined : resolveColor(data.color), hoist: data.hoist, permissions: typeof data.permissions === 'undefined' ? undefined : new Permissions(data.permissions), mentionable: data.mentionable, + icon, + unicode_emoji: data.unicodeEmoji, }; const d = await this.client.api.guilds(this.guild.id).roles(role.id).patch({ data: _data, reason }); diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 3e565c91c70a..5f4abb7fef51 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -186,6 +186,7 @@ class Guild extends AnonymousGuild { * * THREE_DAY_THREAD_ARCHIVE * * SEVEN_DAY_THREAD_ARCHIVE * * PRIVATE_THREADS + * * ROLE_ICONS * @typedef {string} Features * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-guild-features} */ diff --git a/src/structures/Role.js b/src/structures/Role.js index feea3a2a0ee7..9fb5a09e3210 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -91,6 +91,18 @@ class Role extends Base { */ this.deleted = false; + /** + * The icon hash of the role + * @type {?string} + */ + this.icon = data.icon; + + /** + * The unicode emoji for the role + * @type {?string} + */ + this.unicodeEmoji = data.unicode_emoji; + /** * The tags this role has * @type {?Object} @@ -191,6 +203,10 @@ class Role extends Base { * @property {number} [position] The position of the role * @property {PermissionResolvable} [permissions] The permissions of the role * @property {boolean} [mentionable] Whether or not the 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 */ /** @@ -300,6 +316,33 @@ class Role extends Base { return this.edit({ mentionable }, reason); } + /** + * Sets a new icon for the role. + * @param {?(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 + * @param {string} [reason] Reason for changing the role's icon + * @returns {Promise} + */ + setIcon(icon, reason) { + return this.edit({ icon }, reason); + } + + /** + * Sets a new unicode emoji for the role. + * @param {?string} unicodeEmoji The new unicode emoji for the role + * @param {string} [reason] Reason for changing the role's unicode emoji + * @returns {Promise} + * @example + * // Set a new unicode emoji for the role + * role.setUnicodeEmoji('🤖') + * .then(updated => console.log(`Set unicode emoji for the role to ${updated.unicodeEmoji}`)) + * .catch(console.error); + */ + setUnicodeEmoji(unicodeEmoji, reason) { + return this.edit({ unicodeEmoji }, reason); + } + /** * Options used to set position of a role. * @typedef {Object} SetRolePositionOptions @@ -350,6 +393,16 @@ class Role extends Base { return this; } + /** + * A link to the role's icon + * @param {StaticImageURLOptions} [options={}] Options for the image URL + * @returns {?string} + */ + iconURL({ format, size } = {}) { + if (!this.icon) return null; + return this.client.rest.cdn.RoleIcon(this.id, this.icon, format, size); + } + /** * Whether this role equals another role. It compares all properties, so for most operations * it is advisable to just compare `role.id === role2.id` as it is much faster and is often @@ -366,7 +419,9 @@ class Role extends Base { this.hoist === role.hoist && this.position === role.position && this.permissions.bitfield === role.permissions.bitfield && - this.managed === role.managed + this.managed === role.managed && + this.icon === role.icon && + this.unicodeEmoji === role.unicodeEmoji ); } diff --git a/src/util/Constants.js b/src/util/Constants.js index a71d64fa1aec..80155da577b4 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -73,6 +73,8 @@ exports.Endpoints = { TeamIcon: (teamId, hash, options) => makeImageUrl(`${root}/team-icons/${teamId}/${hash}`, options), Sticker: (stickerId, stickerFormat) => `${root}/stickers/${stickerId}.${stickerFormat === 'LOTTIE' ? 'json' : 'png'}`, + RoleIcon: (roleId, hash, format = 'webp', size) => + makeImageUrl(`${root}/role-icons/${roleId}/${hash}`, { size, format }), }; }, invite: (root, code) => `${root}/${code}`, diff --git a/typings/index.d.ts b/typings/index.d.ts index 2fc8b099d193..d81713d9e9d6 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1647,16 +1647,21 @@ export class Role extends Base { public rawPosition: number; public tags: RoleTagData | null; public comparePositionTo(role: RoleResolvable): number; + public icon: string | null; + public unicodeEmoji: string | null; public delete(reason?: string): Promise; public edit(data: RoleData, reason?: string): Promise; public equals(role: Role): boolean; + public iconURL(options?: StaticImageURLOptions): string | null; public permissionsIn(channel: GuildChannel | Snowflake): Readonly; public setColor(color: ColorResolvable, reason?: string): Promise; public setHoist(hoist?: boolean, reason?: string): Promise; public setMentionable(mentionable?: boolean, reason?: string): Promise; public setName(name: string, reason?: string): Promise; public setPermissions(permissions: PermissionResolvable, reason?: string): Promise; + public setIcon(icon: BufferResolvable | Base64Resolvable | EmojiResolvable | null, reason?: string): Promise; public setPosition(position: number, options?: SetRolePositionOptions): Promise; + public setUnicodeEmoji(unicodeEmoji: string | null, reason?: string): Promise; public toJSON(): unknown; public toString(): RoleMention; @@ -2362,6 +2367,7 @@ export const Constants: { { format, size }: { format: AllowedImageFormat; size: AllowedImageSize }, ) => string; Sticker: (stickerId: Snowflake, stickerFormat: StickerFormatType) => string; + RoleIcon: (roleId: Snowflake, hash: string, format: AllowedImageFormat, size: AllowedImageSize) => string; }; }; WSCodes: { @@ -4069,7 +4075,8 @@ export type GuildFeatures = | 'MORE_STICKERS' | 'THREE_DAY_THREAD_ARCHIVE' | 'SEVEN_DAY_THREAD_ARCHIVE' - | 'PRIVATE_THREADS'; + | 'PRIVATE_THREADS' + | 'ROLE_ICONS'; export interface GuildMemberEditData { nick?: string | null; @@ -4740,6 +4747,8 @@ export interface RoleData { position?: number; permissions?: PermissionResolvable; mentionable?: boolean; + icon?: BufferResolvable | Base64Resolvable | EmojiResolvable | null; + unicodeEmoji?: string | null; } export type RoleMention = '@everyone' | `<@&${Snowflake}>`;