diff --git a/src/client/ClientDataResolver.js b/src/client/ClientDataResolver.js index c25abc30a..ba7df252a 100644 --- a/src/client/ClientDataResolver.js +++ b/src/client/ClientDataResolver.js @@ -8,6 +8,7 @@ const Message = require('../structures/Message'); const Guild = require('../structures/Guild'); const Channel = require('../structures/Channel'); const GuildMember = require('../structures/GuildMember'); +const Role = require('../structures/Role'); const Emoji = require('../structures/Emoji'); const ReactionEmoji = require('../structures/ReactionEmoji'); const { Error, TypeError } = require('../errors'); @@ -101,6 +102,27 @@ class ClientDataResolver { return guild.members.get(user.id) || null; } + /** + * Data that can be resolved to a Role object. This can be: + * * A Role + * * A Snowflake + * @typedef {Role|Snowflake} RoleResolvable + */ + + /** + * Resolves a RoleResolvable to a Role object. + * @param {GuildResolvable} guild The guild that this role is part of + * @param {RoleResolvable} role The role resolvable to resolve + * @returns {?Role} + */ + resolveRole(guild, role) { + if (role instanceof Role) return role; + guild = this.resolveGuild(guild); + if (!guild) return null; + if (typeof role === 'string') return guild.roles.get(role); + return null; + } + /** * Data that can be resolved to give a Channel object. This can be: * * A Channel object diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js index 8391ad37b..1920d6fdc 100644 --- a/src/structures/Emoji.js +++ b/src/structures/Emoji.js @@ -106,7 +106,7 @@ class Emoji { * Data for editing an emoji. * @typedef {Object} EmojiEditData * @property {string} [name] The name of the emoji - * @property {Collection|Array} [roles] Roles to restrict emoji to + * @property {Collection|RoleResolvable[]} [roles] Roles to restrict emoji to */ /** @@ -124,7 +124,7 @@ class Emoji { return this.client.api.guilds(this.guild.id).emojis(this.id) .patch({ data: { name: data.name, - roles: data.roles ? data.roles.map(r => r.id ? r.id : r) : [], + roles: data.roles ? data.roles.map(r => r.id ? r.id : r) : undefined, }, reason }) .then(() => this); } @@ -150,13 +150,18 @@ class Emoji { /** * Add multiple roles to the list of roles that can use this emoji. - * @param {Role[]} roles Roles to add + * @param {Collection|RoleResolvable[]} roles Roles to add * @returns {Promise} */ addRestrictedRoles(roles) { const newRoles = new Collection(this.roles); - for (const role of roles) { - if (this.guild.roles.has(role.id)) newRoles.set(role.id, role); + for (let role of roles instanceof Collection ? roles.values() : roles) { + role = this.client.resolver.resolveRole(this.guild, role); + if (!role) { + return Promise.reject(new TypeError('INVALID_TYPE', 'roles', + 'Array or Collection of Roles or Snowflakes', true)); + } + newRoles.set(role.id, role); } return this.edit({ roles: newRoles }); } @@ -172,12 +177,17 @@ class Emoji { /** * Remove multiple roles from the list of roles that can use this emoji. - * @param {Role[]} roles Roles to remove + * @param {Collection|RoleResolvable[]} roles Roles to remove * @returns {Promise} */ removeRestrictedRoles(roles) { const newRoles = new Collection(this.roles); - for (const role of roles) { + for (let role of roles instanceof Collection ? roles.values() : roles) { + role = this.client.resolver.resolveRole(this.guild, role); + if (!role) { + return Promise.reject(new TypeError('INVALID_TYPE', 'roles', + 'Array or Collection of Roles or Snowflakes', true)); + } if (newRoles.has(role.id)) newRoles.delete(role.id); } return this.edit({ roles: newRoles }); @@ -206,12 +216,14 @@ class Emoji { other.id === this.id && other.name === this.name && other.managed === this.managed && - other.requiresColons === this.requiresColons + other.requiresColons === this.requiresColons && + other._roles === this._roles ); } else { return ( other.id === this.id && - other.name === this.name + other.name === this.name && + other._roles === this._roles ); } } diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 7f1d38258..ff7f5d5bd 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -508,7 +508,7 @@ class Guild { * @param {string} options.accessToken An OAuth2 access token for the user with the `guilds.join` scope granted to the * bot's application * @param {string} [options.nick] Nickname to give the member (requires `MANAGE_NICKNAMES`) - * @param {Collection|Role[]|Snowflake[]} [options.roles] Roles to add to the member + * @param {Collection|RoleResolvable[]} [options.roles] Roles to add to the member * (requires `MANAGE_ROLES`) * @param {boolean} [options.mute] Whether the member should be muted (requires `MUTE_MEMBERS`) * @param {boolean} [options.deaf] Whether the member should be deafened (requires `DEAFEN_MEMBERS`) @@ -518,9 +518,14 @@ class Guild { if (this.members.has(user.id)) return Promise.resolve(this.members.get(user.id)); options.access_token = options.accessToken; if (options.roles) { - const roles = options.roles; - if (roles instanceof Collection || (roles instanceof Array && roles[0] instanceof Role)) { - options.roles = roles.map(role => role.id); + const roles = []; + for (let role of options.roles instanceof Collection ? options.roles.values() : options.roles) { + role = this.client.resolver.resolveRole(this, role); + if (!role) { + return Promise.reject(new TypeError('INVALID_TYPE', 'options.roles', + 'Array or Collection of Roles or Snowflakes', true)); + } + roles.push(role.id); } } return this.client.api.guilds(this.id).members(user.id).put({ data: options }) @@ -898,12 +903,21 @@ class Guild { if (!this.client.user.bot) this.client.syncGuilds([this]); } + /** + * Can be used to overwrite permissions when creating a channel. + * @typedef {Object} ChannelCreationOverwrites + * @property {PermissionResolveable[]|number} [allow] The permissions to allow + * @property {PermissionResolveable[]|number} [deny] The permissions to deny + * @property {RoleResolveable|UserResolvable} id ID of the group or member this overwrite is for + */ + /** * Creates a new channel in the guild. * @param {string} name The name of the new channel * @param {string} type The type of the new channel, either `text` or `voice` - * @param {Object} options Options - * @param {Array} [options.overwrites] Permission overwrites to apply to the new channel + * @param {Object} [options={}] Options + * @param {Array} [options.overwrites] Permission overwrites + * to apply to the new channel * @param {string} [options.reason] Reason for creating this channel * @returns {Promise} * @example @@ -914,13 +928,30 @@ class Guild { */ createChannel(name, type, { overwrites, reason } = {}) { if (overwrites instanceof Collection || overwrites instanceof Array) { - overwrites = overwrites.map(overwrite => ({ - allow: overwrite.allow || overwrite._allowed, - deny: overwrite.deny || overwrite._denied, - type: overwrite.type, - id: overwrite.id, - })); + overwrites = overwrites.map(overwrite => { + let allow = overwrite.allow || overwrite._allowed; + let deny = overwrite.deny || overwrite._denied; + if (allow instanceof Array) allow = Permissions.resolve(allow); + if (deny instanceof Array) deny = Permissions.resolve(deny); + + const role = this.client.resolver.resolveRole(this, overwrite.id); + if (role) { + overwrite.id = role.id; + overwrite.type = 'role'; + } else { + overwrite.id = this.client.resolver.resolveUserID(overwrite.id); + overwrite.type = 'member'; + } + + return { + allow, + deny, + type: overwrite.type, + id: overwrite.id, + }; + }); } + return this.client.api.guilds(this.id).channels.post({ data: { name, type, permission_overwrites: overwrites, @@ -1005,7 +1036,7 @@ class Guild { * @param {BufferResolvable|Base64Resolvable} attachment The image for the emoji * @param {string} name The name for the emoji * @param {Object} [options] Options - * @param {Collection|Role[]} [options.roles] Roles to limit the emoji to + * @param {Collection|RoleResolvable[]} [options.roles] Roles to limit the emoji to * @param {string} [options.reason] Reason for creating the emoji * @returns {Promise} The created emoji * @example @@ -1022,16 +1053,27 @@ class Guild { createEmoji(attachment, name, { roles, reason } = {}) { if (typeof attachment === 'string' && attachment.startsWith('data:')) { const data = { image: attachment, name }; - if (roles) data.roles = roles.map(r => r.id ? r.id : r); + if (roles) { + data.roles = []; + for (let role of roles instanceof Collection ? roles.values() : roles) { + role = this.client.resolver.resolveRole(this, role); + if (!role) { + return Promise.reject(new TypeError('INVALID_TYPE', 'options.roles', + 'Array or Collection of Roles or Snowflakes', true)); + } + data.roles.push(role.id); + } + } + return this.client.api.guilds(this.id).emojis.post({ data, reason }) .then(emoji => this.client.actions.GuildEmojiCreate.handle(this, emoji).emoji); - } else { - return this.client.resolver.resolveBuffer(attachment) - .then(data => { - const dataURI = this.client.resolver.resolveBase64(data); - return this.createEmoji(dataURI, name, roles); - }); } + + return this.client.resolver.resolveBuffer(attachment) + .then(data => { + const dataURI = this.client.resolver.resolveBase64(data); + return this.createEmoji(dataURI, name, { roles, reason }); + }); } /** diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index f6a10cbe9..b91930e51 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -324,7 +324,7 @@ class GuildMember { * The data for editing a guild member. * @typedef {Object} GuildMemberEditData * @property {string} [nick] The nickname to set for the member - * @property {Collection|Role[]|Snowflake[]} [roles] The roles or role IDs to apply + * @property {Collection|RoleResolvable[]} [roles] The roles or role IDs to apply * @property {boolean} [mute] Whether or not the member should be muted * @property {boolean} [deaf] Whether or not the member should be deafened * @property {ChannelResolvable} [channel] Channel to move member to (if they are connected to voice) @@ -384,7 +384,7 @@ class GuildMember { /** * Sets the roles applied to the member. - * @param {Collection|Role[]|Snowflake[]} roles The roles or role IDs to apply + * @param {Collection|RoleResolvable[]} roles The roles or role IDs to apply * @param {string} [reason] Reason for applying the roles * @returns {Promise} */ @@ -394,12 +394,12 @@ class GuildMember { /** * Adds a single role to the member. - * @param {Role|Snowflake} role The role or ID of the role to add + * @param {RoleResolvable} role The role or ID of the role to add * @param {string} [reason] Reason for adding the role * @returns {Promise} */ addRole(role, reason) { - if (!(role instanceof Role)) role = this.guild.roles.get(role); + role = this.client.resolver.resolveRole(this.guild, role); if (!role) return Promise.reject(new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake')); if (this._roles.includes(role.id)) return Promise.resolve(this); return this.client.api.guilds(this.guild.id).members(this.user.id).roles(role.id) @@ -409,30 +409,33 @@ class GuildMember { /** * Adds multiple roles to the member. - * @param {Collection|Role[]|Snowflake[]} roles The roles or role IDs to add + * @param {Collection|RoleResolvable[]} roles The roles or role IDs to add * @param {string} [reason] Reason for adding the roles * @returns {Promise} */ addRoles(roles, reason) { - let allRoles; - if (roles instanceof Collection) { - allRoles = this._roles.slice(); - for (const role of roles.values()) allRoles.push(role.id ? role.id : role); - } else { - allRoles = this._roles.concat(roles.map(r => r.id ? r.id : r)); + let allRoles = this._roles.slice(); + for (let role of roles instanceof Collection ? roles.values() : roles) { + role = this.client.resolver.resolveRole(this.guild, role); + if (!role) { + return Promise.reject(new TypeError('INVALID_TYPE', 'roles', + 'Array or Collection of Roles or Snowflakes', true)); + } + allRoles.push(role.id); } return this.edit({ roles: allRoles }, reason); } /** * Removes a single role from the member. - * @param {Role|Snowflake} role The role or ID of the role to remove + * @param {RoleResolvable} role The role or ID of the role to remove * @param {string} [reason] Reason for removing the role * @returns {Promise} */ removeRole(role, reason) { - if (!(role instanceof Role)) role = this.guild.roles.get(role); + role = this.client.resolver.resolveRole(this.guild, role); if (!role) return Promise.reject(new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake')); + if (!this._roles.includes(role.id)) return Promise.resolve(this); return this.client.api.guilds(this.guild.id).members(this.user.id).roles(role.id) .delete({ reason }) .then(() => this); @@ -440,22 +443,20 @@ class GuildMember { /** * Removes multiple roles from the member. - * @param {Collection|Role[]|Snowflake[]} roles The roles or role IDs to remove + * @param {Collection|RoleResolvable[]} roles The roles or role IDs to remove * @param {string} [reason] Reason for removing the roles * @returns {Promise} */ removeRoles(roles, reason) { const allRoles = this._roles.slice(); - if (roles instanceof Collection) { - for (const role of roles.values()) { - const index = allRoles.indexOf(role.id); - if (index >= 0) allRoles.splice(index, 1); - } - } else { - for (const role of roles) { - const index = allRoles.indexOf(role instanceof Role ? role.id : role); - if (index >= 0) allRoles.splice(index, 1); + for (let role of roles instanceof Collection ? roles.values() : roles) { + role = this.client.resolver.resolveRole(this.guild, role); + if (!role) { + return Promise.reject(new TypeError('INVALID_TYPE', 'roles', + 'Array or Collection of Roles or Snowflakes', true)); } + const index = allRoles.indexOf(role.id); + if (index >= 0) allRoles.splice(index, 1); } return this.edit({ roles: allRoles }, reason); } diff --git a/src/structures/Role.js b/src/structures/Role.js index 2c9bbbbfe..c6dfb611d 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -169,11 +169,13 @@ class Role { /** * Compares this role's position to another role's. - * @param {Role} role Role to compare to this one + * @param {RoleResolvable} role Role to compare to this one * @returns {number} Negative number if the this role's position is lower (other role's is higher), * positive number if the this one is higher (other's is lower), 0 if equal */ comparePositionTo(role) { + role = this.client.resolver.resolveRole(this.guild, role); + if (!role) return Promise.reject(new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake')); return this.constructor.comparePositions(this, role); }