diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 69e3725fe..9f84b1073 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -14,4 +14,4 @@ To get ready to work on the codebase, please do the following: 3. If you're working on voice, also run `npm install node-opus` or `npm install opusscript` 4. Code your heart out! 5. Run `npm test` to run ESLint and ensure any JSDoc changes are valid -6. [Submit a pull request](https://github.com/hydrabolt/discord.js/compare) +6. [Submit a pull request](https://github.com/discordjs/discord.js/compare) diff --git a/.gitmodules b/.gitmodules index d5aa0ecce..44fff6d5f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "typings"] path = typings - url = https://github.com/zajrik/discord.js-typings + url = https://github.com/discordjs/discord.js-typings diff --git a/README.md b/README.md index 989d2d652..3d0f665d2 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ Discord server NPM version NPM downloads - Build status - Dependencies + Build status + Dependencies Patreon

@@ -40,7 +40,7 @@ Using opusscript is only recommended for development environments where node-opu For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers. ### Optional packages -- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for significantly faster WebSocket data inflation (`npm i zlib-sync`) +- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for significantly faster WebSocket data inflation (`npm i zlib-sync`) - [erlpack](https://github.com/discordapp/erlpack) for significantly faster WebSocket data (de)serialisation (`npm i discordapp/erlpack`) - One of the following packages can be installed for faster voice packet encryption and decryption: - [sodium](https://www.npmjs.com/package/sodium) (`npm i sodium`) @@ -67,21 +67,21 @@ client.login('your token'); ``` ## Links -* [Website](https://discord.js.org/) ([source](https://github.com/hydrabolt/discord.js-site)) +* [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website)) * [Documentation](https://discord.js.org/#/docs) * [Discord.js Discord server](https://discord.gg/bRCvFy9) * [Discord API Discord server](https://discord.gg/discord-api) -* [GitHub](https://github.com/hydrabolt/discord.js) +* [GitHub](https://github.com/discordjs/discord.js) * [NPM](https://www.npmjs.com/package/discord.js) * [Related libraries](https://discordapi.com/unofficial/libs.html) ### Extensions -* [discord-rpc](https://www.npmjs.com/package/discord-rpc) ([github](https://github.com/devsnek/discord-rpc)) +* [discord-rpc](https://www.npmjs.com/package/discord-rpc) ([github](https://github.com/discordjs/RPC)) ## Contributing Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the [documentation](https://discord.js.org/#/docs). -See [the contribution guide](https://github.com/hydrabolt/discord.js/blob/master/.github/CONTRIBUTING.md) if you'd like to submit a PR. +See [the contribution guide](https://github.com/discordjs/discord.js/blob/master/.github/CONTRIBUTING.md) if you'd like to submit a PR. ## Help If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle diff --git a/docs/general/updating.md b/docs/general/updating.md index cc2c7399f..0eab3f4b9 100644 --- a/docs/general/updating.md +++ b/docs/general/updating.md @@ -1,11 +1,11 @@ # Version 11.1.0 v11.1.0 features improved voice and gateway stability, as well as support for new features such as audit logs and searching for messages. -See [the changelog](https://github.com/hydrabolt/discord.js/releases/tag/11.1.0) for a full list of changes, including +See [the changelog](https://github.com/discordjs/discord.js/releases/tag/11.1.0) for a full list of changes, including information about deprecations. # Version 11 Version 11 contains loads of new and improved features, optimisations, and bug fixes. -See [the changelog](https://github.com/hydrabolt/discord.js/releases/tag/11.0.0) for a full list of changes. +See [the changelog](https://github.com/discordjs/discord.js/releases/tag/11.0.0) for a full list of changes. ## Significant additions * Message Reactions and Embeds (rich text) diff --git a/docs/general/welcome.md b/docs/general/welcome.md index 84b06ed33..803837b72 100644 --- a/docs/general/welcome.md +++ b/docs/general/welcome.md @@ -8,8 +8,8 @@ Discord server NPM version NPM downloads - Build status - Dependencies + Build status + Dependencies

NPM info @@ -68,18 +68,18 @@ client.login('your token'); ``` ## Links -* [Website](https://discord.js.org/) ([source](https://github.com/hydrabolt/discord.js-site)) +* [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website)) * [Documentation](https://discord.js.org/#/docs) * [Discord.js server](https://discord.gg/bRCvFy9) * [Discord API server](https://discord.gg/rV4BwdK) -* [GitHub](https://github.com/hydrabolt/discord.js) +* [GitHub](https://github.com/discordjs/discord.js) * [NPM](https://www.npmjs.com/package/discord.js) * [Related libraries](https://discordapi.com/unofficial/libs.html) (see also [discord-rpc](https://www.npmjs.com/package/discord-rpc)) ## Contributing Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the [documentation](https://discord.js.org/#/docs). -See [the contribution guide](https://github.com/hydrabolt/discord.js/blob/master/.github/CONTRIBUTING.md) if you'd like to submit a PR. +See [the contribution guide](https://github.com/discordjs/discord.js/blob/master/.github/CONTRIBUTING.md) if you'd like to submit a PR. ## Help If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle diff --git a/docs/topics/web.md b/docs/topics/web.md index 660651bb7..863051212 100644 --- a/docs/topics/web.md +++ b/docs/topics/web.md @@ -17,7 +17,7 @@ const Discord = require('discord.js/browser'); ``` ### Webpack File -You can obtain your desired version of discord.js' web build from the [webpack branch](https://github.com/hydrabolt/discord.js/tree/webpack) of the GitHub repository. +You can obtain your desired version of discord.js' web build from the [webpack branch](https://github.com/discordjs/discord.js/tree/webpack) of the GitHub repository. There is a file for each branch and version of the library, and the ones ending in `.min.js` are minified to substantially reduce the size of the source code. Include the file on the page just as you would any other JS library, like so: diff --git a/package.json b/package.json index f99a4ebdd..cd7d010cc 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/hydrabolt/discord.js.git" + "url": "git+https://github.com/discordjs/discord.js.git" }, "keywords": [ "discord", @@ -27,9 +27,9 @@ "author": "Amish Shah ", "license": "Apache-2.0", "bugs": { - "url": "https://github.com/hydrabolt/discord.js/issues" + "url": "https://github.com/discordjs/discord.js/issues" }, - "homepage": "https://github.com/hydrabolt/discord.js#readme", + "homepage": "https://github.com/discordjs/discord.js#readme", "runkitExampleFilename": "./docs/examples/ping.js", "dependencies": { "pako": "^1.0.0", @@ -48,7 +48,7 @@ }, "devDependencies": { "@types/node": "^8.0.0", - "discord.js-docgen": "hydrabolt/discord.js-docgen", + "discord.js-docgen": "discordjs/docgen", "eslint": "^4.11.0", "jsdoc-strip-async-await": "^0.1.0", "json-filter-loader": "^1.0.0", diff --git a/src/client/Client.js b/src/client/Client.js index 77f0682c2..db0fb151a 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -15,7 +15,7 @@ const UserStore = require('../stores/UserStore'); const ChannelStore = require('../stores/ChannelStore'); const GuildStore = require('../stores/GuildStore'); const ClientPresenceStore = require('../stores/ClientPresenceStore'); -const EmojiStore = require('../stores/EmojiStore'); +const GuildEmojiStore = require('../stores/GuildEmojiStore'); const { Events, browser } = require('../util/Constants'); const DataResolver = require('../util/DataResolver'); const { Error, TypeError, RangeError } = require('../errors'); @@ -208,11 +208,11 @@ class Client extends BaseClient { /** * All custom emojis that the client has access to, mapped by their IDs - * @type {EmojiStore} + * @type {GuildEmojiStore} * @readonly */ get emojis() { - const emojis = new EmojiStore({ client: this }); + const emojis = new GuildEmojiStore({ client: this }); for (const guild of this.guilds.values()) { if (guild.available) for (const emoji of guild.emojis.values()) emojis.set(emoji.id, emoji); } @@ -287,6 +287,11 @@ class Client extends BaseClient { * Obtains an invite from Discord. * @param {InviteResolvable} invite Invite code or URL * @returns {Promise} + * @example + * client.fetchInvite('https://discord.gg/bRCvFy9') + * .then(invite => { + * console.log(`Obtained invite with code: ${invite.code}`); + * }).catch(console.error); */ fetchInvite(invite) { const code = DataResolver.resolveInviteCode(invite); @@ -299,6 +304,11 @@ class Client extends BaseClient { * @param {Snowflake} id ID of the webhook * @param {string} [token] Token for the webhook * @returns {Promise} + * @example + * client.fetchWebhook('id', 'token') + * .then(webhook => { + * console.log(`Obtained webhook with name: ${webhook.name}`); + * }).catch(console.error); */ fetchWebhook(id, token) { return this.api.webhooks(id, token).get().then(data => new Webhook(this, data)); @@ -307,6 +317,11 @@ class Client extends BaseClient { /** * Obtains the available voice regions from Discord. * @returns {Collection} + * @example + * client.fetchVoiceRegions() + * .then(regions => { + * console.log(`Available regions are: ${regions.map(region => region.name).join(', ')}`); + * }).catch(console.error); */ fetchVoiceRegions() { return this.api.voice.regions.get().then(res => { @@ -323,6 +338,10 @@ class Client extends BaseClient { * will be removed from the caches. The default is based on {@link ClientOptions#messageCacheLifetime} * @returns {number} Amount of messages that were removed from the caches, * or -1 if the message cache lifetime is unlimited + * @example + * // Remove all messages older than 1800 seconds from the messages cache + * const amount = client.sweepMessages(1800); + * console.log(`Successfully removed ${amount} messages from the cache.`); */ sweepMessages(lifetime = this.options.messageCacheLifetime) { if (typeof lifetime !== 'number' || isNaN(lifetime)) { @@ -359,6 +378,11 @@ class Client extends BaseClient { * Obtains the OAuth Application of the bot from Discord. * @param {Snowflake} [id='@me'] ID of application to fetch * @returns {Promise} + * @example + * client.fetchApplication('id') + * .then(application => { + * console.log(`Obtained application with name: ${application.name}`); + * }).catch(console.error); */ fetchApplication(id = '@me') { return this.api.oauth2.applications(id).get() @@ -374,7 +398,7 @@ class Client extends BaseClient { * client.generateInvite(['SEND_MESSAGES', 'MANAGE_GUILD', 'MENTION_EVERYONE']) * .then(link => { * console.log(`Generated bot invite link: ${link}`); - * }); + * }).catch(console.error); */ generateInvite(permissions) { if (permissions) { diff --git a/src/client/actions/GuildEmojiCreate.js b/src/client/actions/GuildEmojiCreate.js index 4b5b913c8..7fc955a0f 100644 --- a/src/client/actions/GuildEmojiCreate.js +++ b/src/client/actions/GuildEmojiCreate.js @@ -12,7 +12,7 @@ class GuildEmojiCreateAction extends Action { /** * Emitted whenever a custom emoji is created in a guild. * @event Client#emojiCreate - * @param {Emoji} emoji The emoji that was created + * @param {GuildEmoji} emoji The emoji that was created */ module.exports = GuildEmojiCreateAction; diff --git a/src/client/actions/GuildEmojiDelete.js b/src/client/actions/GuildEmojiDelete.js index 36a674b33..d8a83fc3e 100644 --- a/src/client/actions/GuildEmojiDelete.js +++ b/src/client/actions/GuildEmojiDelete.js @@ -10,9 +10,9 @@ class GuildEmojiDeleteAction extends Action { } /** - * Emitted whenever a custom guild emoji is deleted. + * Emitted whenever a custom emoji is deleted in a guild. * @event Client#emojiDelete - * @param {Emoji} emoji The emoji that was deleted + * @param {GuildEmoji} emoji The emoji that was deleted */ module.exports = GuildEmojiDeleteAction; diff --git a/src/client/actions/GuildEmojiUpdate.js b/src/client/actions/GuildEmojiUpdate.js index b3ebb4b63..e6accf2c5 100644 --- a/src/client/actions/GuildEmojiUpdate.js +++ b/src/client/actions/GuildEmojiUpdate.js @@ -10,10 +10,10 @@ class GuildEmojiUpdateAction extends Action { } /** - * Emitted whenever a custom guild emoji is updated. + * Emitted whenever a custom emoji is updated in a guild. * @event Client#emojiUpdate - * @param {Emoji} oldEmoji The old emoji - * @param {Emoji} newEmoji The new emoji + * @param {GuildEmoji} oldEmoji The old emoji + * @param {GuildEmoji} newEmoji The new emoji */ module.exports = GuildEmojiUpdateAction; diff --git a/src/client/actions/MessageReactionAdd.js b/src/client/actions/MessageReactionAdd.js index 073ba05a7..9d307ceee 100644 --- a/src/client/actions/MessageReactionAdd.js +++ b/src/client/actions/MessageReactionAdd.js @@ -33,7 +33,7 @@ class MessageReactionAdd extends Action { * Emitted whenever a reaction is added to a message. * @event Client#messageReactionAdd * @param {MessageReaction} messageReaction The reaction object - * @param {User} user The user that applied the emoji or reaction emoji + * @param {User} user The user that applied the guild or reaction emoji */ module.exports = MessageReactionAdd; diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 545452703..70ee8d47e 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -93,7 +93,7 @@ const Messages = { WEBHOOK_MESSAGE: 'The message was not sent by a webhook.', - EMOJI_TYPE: 'Emoji must be a string or Emoji/ReactionEmoji', + EMOJI_TYPE: 'Emoji must be a string or GuildEmoji/ReactionEmoji', REACTION_RESOLVE_USER: 'Couldn\'t resolve the user ID to remove from the reaction.', }; diff --git a/src/index.js b/src/index.js index 42de34564..1cd14b654 100644 --- a/src/index.js +++ b/src/index.js @@ -26,8 +26,8 @@ module.exports = { // Stores ChannelStore: require('./stores/ChannelStore'), ClientPresenceStore: require('./stores/ClientPresenceStore'), - EmojiStore: require('./stores/EmojiStore'), GuildChannelStore: require('./stores/GuildChannelStore'), + GuildEmojiStore: require('./stores/GuildEmojiStore'), GuildMemberStore: require('./stores/GuildMemberStore'), GuildStore: require('./stores/GuildStore'), ReactionUserStore: require('./stores/ReactionUserStore'), @@ -64,6 +64,7 @@ module.exports = { Guild: require('./structures/Guild'), GuildAuditLogs: require('./structures/GuildAuditLogs'), GuildChannel: require('./structures/GuildChannel'), + GuildEmoji: require('./structures/GuildEmoji'), GuildMember: require('./structures/GuildMember'), Invite: require('./structures/Invite'), Message: require('./structures/Message'), diff --git a/src/stores/EmojiStore.js b/src/stores/GuildEmojiStore.js similarity index 87% rename from src/stores/EmojiStore.js rename to src/stores/GuildEmojiStore.js index 1e50c07f4..0fc4cc60b 100644 --- a/src/stores/EmojiStore.js +++ b/src/stores/GuildEmojiStore.js @@ -1,17 +1,17 @@ const Collection = require('../util/Collection'); const DataStore = require('./DataStore'); -const Emoji = require('../structures/Emoji'); +const GuildEmoji = require('../structures/GuildEmoji'); const ReactionEmoji = require('../structures/ReactionEmoji'); const DataResolver = require('../util/DataResolver'); /** - * Stores emojis. + * Stores guild emojis. * @private * @extends {DataStore} */ -class EmojiStore extends DataStore { +class GuildEmojiStore extends DataStore { constructor(guild, iterable) { - super(guild.client, iterable, Emoji); + super(guild.client, iterable, GuildEmoji); this.guild = guild; } @@ -61,17 +61,17 @@ class EmojiStore extends DataStore { } /** - * Data that can be resolved into an Emoji object. This can be: + * Data that can be resolved into an GuildEmoji object. This can be: * * A custom emoji ID - * * An Emoji object + * * A GuildEmoji object * * A ReactionEmoji object - * @typedef {Snowflake|Emoji|ReactionEmoji} EmojiResolvable + * @typedef {Snowflake|GuildEmoji|ReactionEmoji} EmojiResolvable */ /** - * Resolves a EmojiResolvable to a Emoji object. + * Resolves an EmojiResolvable to an Emoji object. * @param {EmojiResolvable} emoji The Emoji resolvable to identify - * @returns {?Emoji} + * @returns {?GuildEmoji} */ resolve(emoji) { if (emoji instanceof ReactionEmoji) return super.resolve(emoji.id); @@ -79,7 +79,7 @@ class EmojiStore extends DataStore { } /** - * Resolves a EmojiResolvable to a Emoji ID string. + * Resolves an EmojiResolvable to an Emoji ID string. * @param {EmojiResolvable} emoji The Emoji resolvable to identify * @returns {?Snowflake} */ @@ -111,4 +111,4 @@ class EmojiStore extends DataStore { } } -module.exports = EmojiStore; +module.exports = GuildEmojiStore; diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js index 302ebfccd..f5682046f 100644 --- a/src/structures/Emoji.js +++ b/src/structures/Emoji.js @@ -1,97 +1,29 @@ -const Collection = require('../util/Collection'); -const Snowflake = require('../util/Snowflake'); const Base = require('./Base'); -const { TypeError } = require('../errors'); /** - * Represents a custom emoji. + * Represents an emoji, see {@link GuildEmoji} and {@link ReactionEmoji}. * @extends {Base} */ class Emoji extends Base { - constructor(client, data, guild) { + constructor(client, emoji) { super(client); - - /** - * The guild this emoji is part of - * @type {Guild} - */ - this.guild = guild; - - this._patch(data); - } - - _patch(data) { - /** - * The ID of the emoji - * @type {Snowflake} - */ - this.id = data.id; - - /** - * The name of the emoji - * @type {string} - */ - this.name = data.name; - - /** - * Whether or not this emoji requires colons surrounding it - * @type {boolean} - */ - this.requiresColons = data.require_colons; - - /** - * Whether this emoji is managed by an external service - * @type {boolean} - */ - this.managed = data.managed; - /** * Whether this emoji is animated * @type {boolean} */ - this.animated = data.animated; + this.animated = emoji.animated; - this._roles = data.roles; - } + /** + * The name of this emoji + * @type {string} + */ + this.name = emoji.name; - /** - * The timestamp the emoji was created at - * @type {number} - * @readonly - */ - get createdTimestamp() { - return Snowflake.deconstruct(this.id).timestamp; - } - - /** - * The time the emoji was created at - * @type {Date} - * @readonly - */ - get createdAt() { - return new Date(this.createdTimestamp); - } - - /** - * A collection of roles this emoji is active for (empty if all), mapped by role ID - * @type {Collection} - * @readonly - */ - get roles() { - const roles = new Collection(); - for (const role of this._roles) { - if (this.guild.roles.has(role)) roles.set(role, this.guild.roles.get(role)); - } - return roles; - } - - /** - * The URL to the emoji file - * @type {string} - * @readonly - */ - get url() { - return this.client.rest.cdn.Emoji(this.id, this.animated ? 'gif' : 'png'); + /** + * The ID of this emoji + * @type {?Snowflake} + */ + this.id = emoji.id; } /** @@ -100,148 +32,34 @@ class Emoji extends Base { * @readonly */ get identifier() { - if (this.id) return `${this.name}:${this.id}`; + if (this.id) return `${this.animated ? 'a:' : ''}${this.name}:${this.id}`; return encodeURIComponent(this.name); } /** - * Data for editing an emoji. - * @typedef {Object} EmojiEditData - * @property {string} [name] The name of the emoji - * @property {Collection|RoleResolvable[]} [roles] Roles to restrict emoji to + * The URL to the emoji file if its a custom emoji + * @type {?string} + * @readonly */ - - /** - * Edits the emoji. - * @param {EmojiEditData} data The new data for the emoji - * @param {string} [reason] Reason for editing this emoji - * @returns {Promise} - * @example - * // Edit an emoji - * emoji.edit({name: 'newemoji'}) - * .then(e => console.log(`Edited emoji ${e}`)) - * .catch(console.error); - */ - edit(data, reason) { - 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) : undefined, - }, reason }) - .then(() => this); + get url() { + if (!this.id) return null; + return this.client.rest.cdn.Emoji(this.id, this.animated ? 'gif' : 'png'); } /** - * Sets the name of the emoji. - * @param {string} name The new name for the emoji - * @param {string} [reason] Reason for changing the emoji's name - * @returns {Promise} - */ - setName(name, reason) { - return this.edit({ name }, reason); - } - - /** - * Adds a role to the list of roles that can use this emoji. - * @param {Role} role The role to add - * @returns {Promise} - */ - addRestrictedRole(role) { - return this.addRestrictedRoles([role]); - } - - /** - * Adds multiple roles to the list of roles that can use this emoji. - * @param {Collection|RoleResolvable[]} roles Roles to add - * @returns {Promise} - */ - addRestrictedRoles(roles) { - const newRoles = new Collection(this.roles); - for (let role of roles instanceof Collection ? roles.values() : roles) { - role = this.guild.roles.resolve(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 }); - } - - /** - * Removes a role from the list of roles that can use this emoji. - * @param {Role} role The role to remove - * @returns {Promise} - */ - removeRestrictedRole(role) { - return this.removeRestrictedRoles([role]); - } - - /** - * Removes multiple roles from the list of roles that can use this emoji. - * @param {Collection|RoleResolvable[]} roles Roles to remove - * @returns {Promise} - */ - removeRestrictedRoles(roles) { - const newRoles = new Collection(this.roles); - for (let role of roles instanceof Collection ? roles.values() : roles) { - role = this.guild.roles.resolve(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 }); - } - - /** - * When concatenated with a string, this automatically concatenates the emoji's mention instead of the Emoji object. + * When concatenated with a string, this automatically returns the text required to form a graphical emoji on Discord + * instead of the Emoji object. * @returns {string} * @example - * // Send an emoji: + * // Send a custom emoji from a guild: * const emoji = guild.emojis.first(); * msg.reply(`Hello! ${emoji}`); + * @example + * // Send the emoji used in a reaction to the channel the reaction is part of + * reaction.message.channel.send(`The emoji used was: ${reaction.emoji}`); */ toString() { - if (!this.id || !this.requiresColons) { - return this.name; - } - - return `<${this.animated ? 'a' : ''}:${this.name}:${this.id}>`; - } - - /** - * Deletes the emoji. - * @param {string} [reason] Reason for deleting the emoji - * @returns {Promise} - */ - delete(reason) { - return this.client.api.guilds(this.guild.id).emojis(this.id).delete({ reason }) - .then(() => this); - } - - /** - * Whether this emoji is the same as another one. - * @param {Emoji|Object} other The emoji to compare it to - * @returns {boolean} Whether the emoji is equal to the given emoji or not - */ - equals(other) { - if (other instanceof Emoji) { - return ( - other.id === this.id && - other.name === this.name && - other.managed === this.managed && - other.requiresColons === this.requiresColons && - other._roles === this._roles - ); - } else { - return ( - other.id === this.id && - other.name === this.name && - other._roles === this._roles - ); - } + return this.id ? `<${this.animated ? 'a' : ''}:${this.name}:${this.id}>` : this.name; } } diff --git a/src/structures/Guild.js b/src/structures/Guild.js index a75829313..51ea81d32 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -10,7 +10,7 @@ const Snowflake = require('../util/Snowflake'); const Shared = require('./shared'); const GuildMemberStore = require('../stores/GuildMemberStore'); const RoleStore = require('../stores/RoleStore'); -const EmojiStore = require('../stores/EmojiStore'); +const GuildEmojiStore = require('../stores/GuildEmojiStore'); const GuildChannelStore = require('../stores/GuildChannelStore'); const PresenceStore = require('../stores/PresenceStore'); const Base = require('./Base'); @@ -218,9 +218,9 @@ class Guild extends Base { if (!this.emojis) { /** * A collection of emojis that are in this guild. The key is the emoji's ID, the value is the emoji. - * @type {EmojiStore} + * @type {GuildEmojiStore} */ - this.emojis = new EmojiStore(this); + this.emojis = new GuildEmojiStore(this); if (data.emojis) for (const emoji of data.emojis) this.emojis.add(emoji); } else { this.client.actions.GuildEmojisUpdate.handle({ diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index 148e199a9..6df77707e 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -148,7 +148,7 @@ class GuildAuditLogs { * * An invite * * A webhook * * An object where the keys represent either the new value or the old value - * @typedef {?Object|Guild|User|Role|Emoji|Invite|Webhook} AuditLogEntryTarget + * @typedef {?Object|Guild|User|Role|GuildEmoji|Invite|Webhook} AuditLogEntryTarget */ /** diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 198adcd2c..c4d4c7bbf 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -92,31 +92,16 @@ class GuildChannel extends Channel { } /** - * Gets the overall set of permissions for a user in this channel, taking into account roles and permission - * overwrites. - * @param {GuildMemberResolvable} member The user that you want to obtain the overall permissions for + * Gets the overall set of permissions for a member or role in this channel, taking into account channel overwrites. + * @param {GuildMemberResolvable|RoleResolvable} memberOrRole The member or role to obtain the overall permissions for * @returns {?Permissions} */ - permissionsFor(member) { - member = this.guild.members.resolve(member); - if (!member) return null; - if (member.id === this.guild.ownerID) return new Permissions(Permissions.ALL).freeze(); - - const roles = member.roles; - const permissions = new Permissions(roles.map(role => role.permissions)); - - if (permissions.has(Permissions.FLAGS.ADMINISTRATOR)) return new Permissions(Permissions.ALL).freeze(); - - const overwrites = this.overwritesFor(member, true, roles); - - return permissions - .remove(overwrites.everyone ? overwrites.everyone.denied : 0) - .add(overwrites.everyone ? overwrites.everyone.allowed : 0) - .remove(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.denied) : 0) - .add(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.allowed) : 0) - .remove(overwrites.member ? overwrites.member.denied : 0) - .add(overwrites.member ? overwrites.member.allowed : 0) - .freeze(); + permissionsFor(memberOrRole) { + const member = this.guild.members.resolve(memberOrRole); + if (member) return this.memberPermissions(member); + const role = this.guild.roles.resolve(memberOrRole); + if (role) return this.rolePermissions(role); + return null; } overwritesFor(member, verified = false, roles = null) { @@ -145,6 +130,52 @@ class GuildChannel extends Channel { }; } + /** + * Gets the overall set of permissions for a member in this channel, taking into account channel overwrites. + * @param {GuildMember} member The member to obtain the overall permissions for + * @returns {Permissions} + * @private + */ + memberPermissions(member) { + if (member.id === this.guild.ownerID) return new Permissions(Permissions.ALL).freeze(); + + const roles = member.roles; + const permissions = new Permissions(roles.map(role => role.permissions)); + + if (permissions.has(Permissions.FLAGS.ADMINISTRATOR)) return new Permissions(Permissions.ALL).freeze(); + + const overwrites = this.overwritesFor(member, true, roles); + + return permissions + .remove(overwrites.everyone ? overwrites.everyone.denied : 0) + .add(overwrites.everyone ? overwrites.everyone.allowed : 0) + .remove(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.denied) : 0) + .add(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.allowed) : 0) + .remove(overwrites.member ? overwrites.member.denied : 0) + .add(overwrites.member ? overwrites.member.allowed : 0) + .freeze(); + } + + /** + * Gets the overall set of permissions for a role in this channel, taking into account channel overwrites. + * @param {Role} role The role to obtain the overall permissions for + * @returns {Permissions} + * @private + */ + rolePermissions(role) { + if (role.permissions.has(Permissions.FLAGS.ADMINISTRATOR)) return new Permissions(Permissions.ALL).freeze(); + + const everyoneOverwrites = this.permissionOverwrites.get(this.guild.id); + const roleOverwrites = this.permissionOverwrites.get(role.id); + + return role.permissions + .remove(everyoneOverwrites ? everyoneOverwrites.denied : 0) + .add(everyoneOverwrites ? everyoneOverwrites.allowed : 0) + .remove(roleOverwrites ? roleOverwrites.denied : 0) + .add(roleOverwrites ? roleOverwrites.allowed : 0) + .freeze(); + } + /** * An object mapping permission flags to `true` (enabled), `null` (default) or `false` (disabled). * ```js diff --git a/src/structures/GuildEmoji.js b/src/structures/GuildEmoji.js new file mode 100644 index 000000000..6bf63152d --- /dev/null +++ b/src/structures/GuildEmoji.js @@ -0,0 +1,197 @@ +const Collection = require('../util/Collection'); +const Snowflake = require('../util/Snowflake'); +const Emoji = require('./Emoji'); +const { TypeError } = require('../errors'); + +/** + * Represents a custom emoji. + * @extends {Emoji} + */ +class GuildEmoji extends Emoji { + constructor(client, data, guild) { + super(client, data); + + /** + * The guild this emoji is part of + * @type {Guild} + */ + this.guild = guild; + + this._patch(data); + } + + _patch(data) { + this.name = data.name; + + /** + * Whether or not this emoji requires colons surrounding it + * @type {boolean} + */ + this.requiresColons = data.require_colons; + + /** + * Whether this emoji is managed by an external service + * @type {boolean} + */ + this.managed = data.managed; + + this._roles = data.roles; + } + + /** + * The timestamp the emoji was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * The time the emoji was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * A collection of roles this emoji is active for (empty if all), mapped by role ID + * @type {Collection} + * @readonly + */ + get roles() { + const roles = new Collection(); + for (const role of this._roles) { + if (this.guild.roles.has(role)) roles.set(role, this.guild.roles.get(role)); + } + return roles; + } + + /** + * Data for editing an emoji. + * @typedef {Object} GuildEmojiEditData + * @property {string} [name] The name of the emoji + * @property {Collection|RoleResolvable[]} [roles] Roles to restrict emoji to + */ + + /** + * Edits the emoji. + * @param {Guild} data The new data for the emoji + * @param {string} [reason] Reason for editing this emoji + * @returns {Promise} + * @example + * // Edit an emoji + * emoji.edit({name: 'newemoji'}) + * .then(e => console.log(`Edited emoji ${e}`)) + * .catch(console.error); + */ + edit(data, reason) { + 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) : undefined, + }, reason }) + .then(() => this); + } + + /** + * Sets the name of the emoji. + * @param {string} name The new name for the emoji + * @param {string} [reason] Reason for changing the emoji's name + * @returns {Promise} + */ + setName(name, reason) { + return this.edit({ name }, reason); + } + + /** + * Adds a role to the list of roles that can use this emoji. + * @param {Role} role The role to add + * @returns {Promise} + */ + addRestrictedRole(role) { + return this.addRestrictedRoles([role]); + } + + /** + * Adds multiple roles to the list of roles that can use this emoji. + * @param {Collection|RoleResolvable[]} roles Roles to add + * @returns {Promise} + */ + addRestrictedRoles(roles) { + const newRoles = new Collection(this.roles); + for (let role of roles instanceof Collection ? roles.values() : roles) { + role = this.guild.roles.resolve(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 }); + } + + /** + * Removes a role from the list of roles that can use this emoji. + * @param {Role} role The role to remove + * @returns {Promise} + */ + removeRestrictedRole(role) { + return this.removeRestrictedRoles([role]); + } + + /** + * Removes multiple roles from the list of roles that can use this emoji. + * @param {Collection|RoleResolvable[]} roles Roles to remove + * @returns {Promise} + */ + removeRestrictedRoles(roles) { + const newRoles = new Collection(this.roles); + for (let role of roles instanceof Collection ? roles.values() : roles) { + role = this.guild.roles.resolve(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 }); + } + + /** + * Deletes the emoji. + * @param {string} [reason] Reason for deleting the emoji + * @returns {Promise} + */ + delete(reason) { + return this.client.api.guilds(this.guild.id).emojis(this.id).delete({ reason }) + .then(() => this); + } + + /** + * Whether this emoji is the same as another one. + * @param {GuildEmoji|Object} other The emoji to compare it to + * @returns {boolean} Whether the emoji is equal to the given emoji or not + */ + equals(other) { + if (other instanceof GuildEmoji) { + return ( + other.id === this.id && + other.name === this.name && + other.managed === this.managed && + other.requiresColons === this.requiresColons && + other._roles === this._roles + ); + } else { + return ( + other.id === this.id && + other.name === this.name && + other._roles === this._roles + ); + } + } +} + +module.exports = GuildEmoji; diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index c7384d875..08561c757 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -295,9 +295,9 @@ class GuildMember extends Base { * @returns {?Permissions} */ permissionsIn(channel) { - channel = this.client.channels.resolve(channel); - if (!channel || !channel.guild) throw new Error('GUILD_CHANNEL_RESOLVE'); - return channel.permissionsFor(this); + channel = this.guild.channels.resolve(channel); + if (!channel) throw new Error('GUILD_CHANNEL_RESOLVE'); + return channel.memberPermissions(this); } /** diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 9c8271372..de3ceddf7 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -193,7 +193,7 @@ class MessageEmbed { /** * Sets the file to upload alongside the embed. This file can be accessed via `attachment://fileName.extension` when - * setting an embed image or author/footer icons. Only one file may be attached. + * setting an embed image or author/footer icons. Multiple files can be attached. * @param {Array} files Files to attach * @returns {MessageEmbed} */ diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js index b65721134..660967f0b 100644 --- a/src/structures/MessageReaction.js +++ b/src/structures/MessageReaction.js @@ -1,4 +1,4 @@ -const Emoji = require('./Emoji'); +const GuildEmoji = require('./GuildEmoji'); const ReactionEmoji = require('./ReactionEmoji'); const ReactionUserStore = require('../stores/ReactionUserStore'); @@ -31,18 +31,18 @@ class MessageReaction { */ this.users = new ReactionUserStore(client, undefined, this); - this._emoji = new ReactionEmoji(this, data.emoji.name, data.emoji.id); + this._emoji = new ReactionEmoji(this, data.emoji); } /** - * The emoji of this reaction, either an Emoji object for known custom emojis, or a ReactionEmoji + * The emoji of this reaction, either an GuildEmoji object for known custom emojis, or a ReactionEmoji * object which has fewer properties. Whatever the prototype of the emoji, it will still have * `name`, `id`, `identifier` and `toString()` - * @type {Emoji|ReactionEmoji} + * @type {GuildEmoji|ReactionEmoji} * @readonly */ get emoji() { - if (this._emoji instanceof Emoji) return this._emoji; + if (this._emoji instanceof GuildEmoji) return this._emoji; // Check to see if the emoji has become known to the client if (this._emoji.id) { const emojis = this.message.client.emojis; diff --git a/src/structures/ReactionEmoji.js b/src/structures/ReactionEmoji.js index 94ea38930..9bb23c120 100644 --- a/src/structures/ReactionEmoji.js +++ b/src/structures/ReactionEmoji.js @@ -1,49 +1,19 @@ +const Emoji = require('./Emoji'); + /** * Represents a limited emoji set used for both custom and unicode emojis. Custom emojis * will use this class opposed to the Emoji class when the client doesn't know enough * information about them. + * @extends {Emoji} */ -class ReactionEmoji { - constructor(reaction, name, id) { +class ReactionEmoji extends Emoji { + constructor(reaction, emoji) { + super(reaction.message.client, emoji); /** * The message reaction this emoji refers to * @type {MessageReaction} */ this.reaction = reaction; - - /** - * The name of this reaction emoji - * @type {string} - */ - this.name = name; - - /** - * The ID of this reaction emoji - * @type {?Snowflake} - */ - this.id = id; - } - - /** - * The identifier of this emoji, used for message reactions - * @type {string} - * @readonly - */ - get identifier() { - if (this.id) return `${this.name}:${this.id}`; - return encodeURIComponent(this.name); - } - - /** - * When concatenated with a string, this automatically returns the text required to form a graphical emoji on Discord - * instead of the ReactionEmoji object. - * @returns {string} - * @example - * // Send the emoji used in a reaction to the channel the reaction is part of - * reaction.message.channel.send(`The emoji used was: ${reaction.emoji}`); - */ - toString() { - return this.id ? `<:${this.name}:${this.id}>` : this.name; } } diff --git a/src/structures/Role.js b/src/structures/Role.js index 39c28e11b..1d82f63ba 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -195,6 +195,18 @@ class Role extends Base { }); } + /** + * Returns `channel.permissionsFor(role)`. Returns permissions for a role in a guild channel, + * taking into account permission overwrites. + * @param {ChannelResolvable} channel The guild channel to use as context + * @returns {?Permissions} + */ + permissionsIn(channel) { + channel = this.guild.channels.resolve(channel); + if (!channel) throw new Error('GUILD_CHANNEL_RESOLVE'); + return channel.rolePermissions(this); + } + /** * Sets a new name for the role. * @param {string} name The new name of the role diff --git a/src/util/Permissions.js b/src/util/Permissions.js index 7ccd9009c..e9ef9d1c5 100644 --- a/src/util/Permissions.js +++ b/src/util/Permissions.js @@ -7,19 +7,19 @@ const { RangeError } = require('../errors'); */ class Permissions { /** - * @param {number|PermissionResolvable[]} permissions Permissions or bitfield to read from + * @param {PermissionResolvable} permissions Permission(s) to read from */ constructor(permissions) { /** * Bitfield of the packed permissions * @type {number} */ - this.bitfield = typeof permissions === 'number' ? permissions : this.constructor.resolve(permissions); + this.bitfield = this.constructor.resolve(permissions); } /** * Checks whether the bitfield has a permission, or multiple permissions. - * @param {PermissionResolvable|PermissionResolvable[]} permission Permission(s) to check for + * @param {PermissionResolvable} permission Permission(s) to check for * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override * @returns {boolean} */ @@ -32,11 +32,12 @@ class Permissions { /** * Gets all given permissions that are missing from the bitfield. - * @param {PermissionResolvable[]} permissions Permissions to check for + * @param {PermissionResolvable} permissions Permission(s) to check for * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override - * @returns {PermissionResolvable[]} + * @returns {string[]} */ missing(permissions, checkAdmin = true) { + if (!(permissions instanceof Array)) permissions = new this.constructor(permissions).toArray(false); return permissions.filter(p => !this.has(p, checkAdmin)); } @@ -92,17 +93,32 @@ class Permissions { return serialized; } + /** + * Gets an {@link Array} of permission names (such as `VIEW_CHANNEL`) based on the permissions available. + * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override + * @returns {string[]} + */ + toArray(checkAdmin = true) { + return Object.keys(this.constructor.FLAGS).filter(perm => this.has(perm, checkAdmin)); + } + + *[Symbol.iterator]() { + const keys = this.toArray(); + while (keys.length) yield keys.shift(); + } + /** * Data that can be resolved to give a permission number. This can be: * * A string (see {@link Permissions.FLAGS}) * * A permission number * * An instance of Permissions - * @typedef {string|number|Permissions} PermissionResolvable + * * An Array of PermissionResolvable + * @typedef {string|number|Permissions|PermissionResolvable[]} PermissionResolvable */ /** * Resolves permissions to their numeric form. - * @param {PermissionResolvable|PermissionResolvable[]} permission - Permission(s) to resolve + * @param {PermissionResolvable} permission - Permission(s) to resolve * @returns {number} */ static resolve(permission) { diff --git a/src/util/Structures.js b/src/util/Structures.js index a1cb7e156..e7f615c79 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -61,7 +61,7 @@ class Structures { } const structures = { - Emoji: require('../structures/Emoji'), + GuildEmoji: require('../structures/GuildEmoji'), DMChannel: require('../structures/DMChannel'), GroupDMChannel: require('../structures/GroupDMChannel'), TextChannel: require('../structures/TextChannel'), diff --git a/typings b/typings index 0b5b13f4a..895af7f3d 160000 --- a/typings +++ b/typings @@ -1 +1 @@ -Subproject commit 0b5b13f4a521cba0fc42aa0f9b2c4a1abca2de3d +Subproject commit 895af7f3dad233139b8246fe0e44079867e6cc95