From 29433e2f7dbd0e4a73d3c78ffe1005b922fb5982 Mon Sep 17 00:00:00 2001 From: Alee14 Date: Sun, 26 Mar 2017 15:18:10 -0400 Subject: Don't mind me i'm adding the discord.js files --- node_modules/discord.js/src/structures/Channel.js | 67 ++ .../src/structures/ClientOAuth2Application.js | 26 + .../discord.js/src/structures/ClientUser.js | 274 +++++++ .../discord.js/src/structures/DMChannel.js | 60 ++ node_modules/discord.js/src/structures/Emoji.js | 140 ++++ .../src/structures/EvaluatedPermissions.js | 67 ++ .../discord.js/src/structures/GroupDMChannel.js | 144 ++++ node_modules/discord.js/src/structures/Guild.js | 851 +++++++++++++++++++++ .../discord.js/src/structures/GuildChannel.js | 299 ++++++++ .../discord.js/src/structures/GuildMember.js | 442 +++++++++++ node_modules/discord.js/src/structures/Invite.js | 159 ++++ node_modules/discord.js/src/structures/Message.js | 568 ++++++++++++++ .../discord.js/src/structures/MessageAttachment.js | 68 ++ .../discord.js/src/structures/MessageCollector.js | 151 ++++ .../discord.js/src/structures/MessageEmbed.js | 293 +++++++ .../discord.js/src/structures/MessageReaction.js | 92 +++ .../discord.js/src/structures/OAuth2Application.js | 82 ++ .../discord.js/src/structures/PartialGuild.js | 51 ++ .../src/structures/PartialGuildChannel.js | 44 ++ .../src/structures/PermissionOverwrites.js | 43 ++ node_modules/discord.js/src/structures/Presence.js | 92 +++ .../discord.js/src/structures/ReactionEmoji.js | 49 ++ .../discord.js/src/structures/RichEmbed.js | 204 +++++ node_modules/discord.js/src/structures/Role.js | 341 +++++++++ .../discord.js/src/structures/TextChannel.js | 96 +++ node_modules/discord.js/src/structures/User.js | 277 +++++++ .../discord.js/src/structures/UserConnection.js | 48 ++ .../discord.js/src/structures/UserProfile.js | 56 ++ .../discord.js/src/structures/VoiceChannel.js | 120 +++ node_modules/discord.js/src/structures/Webhook.js | 200 +++++ .../src/structures/interface/TextBasedChannel.js | 377 +++++++++ 31 files changed, 5781 insertions(+) create mode 100644 node_modules/discord.js/src/structures/Channel.js create mode 100644 node_modules/discord.js/src/structures/ClientOAuth2Application.js create mode 100644 node_modules/discord.js/src/structures/ClientUser.js create mode 100644 node_modules/discord.js/src/structures/DMChannel.js create mode 100644 node_modules/discord.js/src/structures/Emoji.js create mode 100644 node_modules/discord.js/src/structures/EvaluatedPermissions.js create mode 100644 node_modules/discord.js/src/structures/GroupDMChannel.js create mode 100644 node_modules/discord.js/src/structures/Guild.js create mode 100644 node_modules/discord.js/src/structures/GuildChannel.js create mode 100644 node_modules/discord.js/src/structures/GuildMember.js create mode 100644 node_modules/discord.js/src/structures/Invite.js create mode 100644 node_modules/discord.js/src/structures/Message.js create mode 100644 node_modules/discord.js/src/structures/MessageAttachment.js create mode 100644 node_modules/discord.js/src/structures/MessageCollector.js create mode 100644 node_modules/discord.js/src/structures/MessageEmbed.js create mode 100644 node_modules/discord.js/src/structures/MessageReaction.js create mode 100644 node_modules/discord.js/src/structures/OAuth2Application.js create mode 100644 node_modules/discord.js/src/structures/PartialGuild.js create mode 100644 node_modules/discord.js/src/structures/PartialGuildChannel.js create mode 100644 node_modules/discord.js/src/structures/PermissionOverwrites.js create mode 100644 node_modules/discord.js/src/structures/Presence.js create mode 100644 node_modules/discord.js/src/structures/ReactionEmoji.js create mode 100644 node_modules/discord.js/src/structures/RichEmbed.js create mode 100644 node_modules/discord.js/src/structures/Role.js create mode 100644 node_modules/discord.js/src/structures/TextChannel.js create mode 100644 node_modules/discord.js/src/structures/User.js create mode 100644 node_modules/discord.js/src/structures/UserConnection.js create mode 100644 node_modules/discord.js/src/structures/UserProfile.js create mode 100644 node_modules/discord.js/src/structures/VoiceChannel.js create mode 100644 node_modules/discord.js/src/structures/Webhook.js create mode 100644 node_modules/discord.js/src/structures/interface/TextBasedChannel.js (limited to 'node_modules/discord.js/src/structures') diff --git a/node_modules/discord.js/src/structures/Channel.js b/node_modules/discord.js/src/structures/Channel.js new file mode 100644 index 0000000..b37b14b --- /dev/null +++ b/node_modules/discord.js/src/structures/Channel.js @@ -0,0 +1,67 @@ +/** + * Represents any channel on Discord + */ +class Channel { + constructor(client, data) { + /** + * The client that instantiated the Channel + * @name Channel#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + /** + * The type of the channel, either: + * * `dm` - a DM channel + * * `group` - a Group DM channel + * * `text` - a guild text channel + * * `voice` - a guild voice channel + * @type {string} + */ + this.type = null; + + if (data) this.setup(data); + } + + setup(data) { + /** + * The unique ID of the channel + * @type {string} + */ + this.id = data.id; + } + + /** + * The timestamp the channel was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return (this.id / 4194304) + 1420070400000; + } + + /** + * The time the channel was created + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * Deletes the channel + * @returns {Promise} + * @example + * // delete the channel + * channel.delete() + * .then() // success + * .catch(console.error); // log error + */ + delete() { + return this.client.rest.methods.deleteChannel(this); + } +} + +module.exports = Channel; diff --git a/node_modules/discord.js/src/structures/ClientOAuth2Application.js b/node_modules/discord.js/src/structures/ClientOAuth2Application.js new file mode 100644 index 0000000..46e1250 --- /dev/null +++ b/node_modules/discord.js/src/structures/ClientOAuth2Application.js @@ -0,0 +1,26 @@ +const User = require('./User'); +const OAuth2Application = require('./OAuth2Application'); + +/** + * Represents the client's OAuth2 Application + * @extends {OAuth2Application} + */ +class ClientOAuth2Application extends OAuth2Application { + setup(data) { + super.setup(data); + + /** + * The app's flags + * @type {number} + */ + this.flags = data.flags; + + /** + * The app's owner + * @type {User} + */ + this.owner = new User(this.client, data.owner); + } +} + +module.exports = ClientOAuth2Application; diff --git a/node_modules/discord.js/src/structures/ClientUser.js b/node_modules/discord.js/src/structures/ClientUser.js new file mode 100644 index 0000000..d526af6 --- /dev/null +++ b/node_modules/discord.js/src/structures/ClientUser.js @@ -0,0 +1,274 @@ +const User = require('./User'); +const Collection = require('../util/Collection'); + +/** + * Represents the logged in client's Discord user + * @extends {User} + */ +class ClientUser extends User { + setup(data) { + super.setup(data); + + /** + * Whether or not this account has been verified + * @type {boolean} + */ + this.verified = data.verified; + + /** + * The email of this account + * @type {string} + */ + this.email = data.email; + this.localPresence = {}; + this._typing = new Map(); + + /** + * A Collection of friends for the logged in user. + * This is only filled when using a user account. + * @type {Collection} + */ + this.friends = new Collection(); + + /** + * A Collection of blocked users for the logged in user. + * This is only filled when using a user account. + * @type {Collection} + */ + this.blocked = new Collection(); + + /** + * A Collection of notes for the logged in user. + * This is only filled when using a user account. + * @type {Collection} + */ + this.notes = new Collection(); + } + + edit(data) { + return this.client.rest.methods.updateCurrentUser(data); + } + + /** + * Set the username of the logged in Client. + * Changing usernames in Discord is heavily rate limited, with only 2 requests + * every hour. Use this sparingly! + * @param {string} username The new username + * @param {string} [password] Current password (only for user accounts) + * @returns {Promise} + * @example + * // set username + * client.user.setUsername('discordjs') + * .then(user => console.log(`My new username is ${user.username}`)) + * .catch(console.error); + */ + setUsername(username, password) { + return this.client.rest.methods.updateCurrentUser({ username }, password); + } + + /** + * Changes the email for the client user's account. + * This is only available when using a user account. + * @param {string} email New email to change to + * @param {string} password Current password + * @returns {Promise} + * @example + * // set email + * client.user.setEmail('bob@gmail.com', 'some amazing password 123') + * .then(user => console.log(`My new email is ${user.email}`)) + * .catch(console.error); + */ + setEmail(email, password) { + return this.client.rest.methods.updateCurrentUser({ email }, password); + } + + /** + * Changes the password for the client user's account. + * This is only available when using a user account. + * @param {string} newPassword New password to change to + * @param {string} oldPassword Current password + * @returns {Promise} + * @example + * // set password + * client.user.setPassword('some new amazing password 456', 'some amazing password 123') + * .then(user => console.log('New password set!')) + * .catch(console.error); + */ + setPassword(newPassword, oldPassword) { + return this.client.rest.methods.updateCurrentUser({ password: newPassword }, oldPassword); + } + + /** + * Set the avatar of the logged in Client. + * @param {BufferResolvable|Base64Resolvable} avatar The new avatar + * @returns {Promise} + * @example + * // set avatar + * client.user.setAvatar('./avatar.png') + * .then(user => console.log(`New avatar set!`)) + * .catch(console.error); + */ + setAvatar(avatar) { + if (avatar.startsWith('data:')) { + return this.client.rest.methods.updateCurrentUser({ avatar }); + } else { + return this.client.resolver.resolveBuffer(avatar).then(data => + this.client.rest.methods.updateCurrentUser({ avatar: data }) + ); + } + } + + /** + * Data resembling a raw Discord presence + * @typedef {Object} PresenceData + * @property {PresenceStatus} [status] Status of the user + * @property {boolean} [afk] Whether the user is AFK + * @property {Object} [game] Game the user is playing + * @property {string} [game.name] Name of the game + * @property {string} [game.url] Twitch stream URL + */ + + /** + * Sets the full presence of the client user. + * @param {PresenceData} data Data for the presence + * @returns {Promise} + */ + setPresence(data) { + // {"op":3,"d":{"status":"dnd","since":0,"game":null,"afk":false}} + return new Promise(resolve => { + let status = this.localPresence.status || this.presence.status; + let game = this.localPresence.game; + let afk = this.localPresence.afk || this.presence.afk; + + if (!game && this.presence.game) { + game = { + name: this.presence.game.name, + type: this.presence.game.type, + url: this.presence.game.url, + }; + } + + if (data.status) { + if (typeof data.status !== 'string') throw new TypeError('Status must be a string'); + status = data.status; + } + + if (data.game) { + game = data.game; + if (game.url) game.type = 1; + } + + if (typeof data.afk !== 'undefined') afk = data.afk; + afk = Boolean(afk); + + this.localPresence = { status, game, afk }; + this.localPresence.since = 0; + this.localPresence.game = this.localPresence.game || null; + + this.client.ws.send({ + op: 3, + d: this.localPresence, + }); + + this.client._setPresence(this.id, this.localPresence); + + resolve(this); + }); + } + + /** + * A user's status. Must be one of: + * - `online` + * - `idle` + * - `invisible` + * - `dnd` (do not disturb) + * @typedef {string} PresenceStatus + */ + + /** + * Sets the status of the client user. + * @param {PresenceStatus} status Status to change to + * @returns {Promise} + */ + setStatus(status) { + return this.setPresence({ status }); + } + + /** + * Sets the game the client user is playing. + * @param {string} game Game being played + * @param {string} [streamingURL] Twitch stream URL + * @returns {Promise} + */ + setGame(game, streamingURL) { + return this.setPresence({ game: { + name: game, + url: streamingURL, + } }); + } + + /** + * Sets/removes the AFK flag for the client user. + * @param {boolean} afk Whether or not the user is AFK + * @returns {Promise} + */ + setAFK(afk) { + return this.setPresence({ afk }); + } + + /** + * Fetches messages that mentioned the client's user + * @param {Object} [options] Options for the fetch + * @param {number} [options.limit=25] Maximum number of mentions to retrieve + * @param {boolean} [options.roles=true] Whether to include role mentions + * @param {boolean} [options.everyone=true] Whether to include everyone/here mentions + * @param {Guild|string} [options.guild] Limit the search to a specific guild + * @returns {Promise} + */ + fetchMentions(options = { limit: 25, roles: true, everyone: true, guild: null }) { + return this.client.rest.methods.fetchMentions(options); + } + + /** + * Send a friend request + * This is only available when using a user account. + * @param {UserResolvable} user The user to send the friend request to. + * @returns {Promise} The user the friend request was sent to. + */ + addFriend(user) { + user = this.client.resolver.resolveUser(user); + return this.client.rest.methods.addFriend(user); + } + + /** + * Remove a friend + * This is only available when using a user account. + * @param {UserResolvable} user The user to remove from your friends + * @returns {Promise} The user that was removed + */ + removeFriend(user) { + user = this.client.resolver.resolveUser(user); + return this.client.rest.methods.removeFriend(user); + } + + /** + * Creates a guild + * This is only available when using a user account. + * @param {string} name The name of the guild + * @param {string} region The region for the server + * @param {BufferResolvable|Base64Resolvable} [icon=null] The icon for the guild + * @returns {Promise} The guild that was created + */ + createGuild(name, region, icon = null) { + if (!icon) return this.client.rest.methods.createGuild({ name, icon, region }); + if (icon.startsWith('data:')) { + return this.client.rest.methods.createGuild({ name, icon, region }); + } else { + return this.client.resolver.resolveBuffer(icon).then(data => + this.client.rest.methods.createGuild({ name, icon: data, region }) + ); + } + } +} + +module.exports = ClientUser; diff --git a/node_modules/discord.js/src/structures/DMChannel.js b/node_modules/discord.js/src/structures/DMChannel.js new file mode 100644 index 0000000..de49c8b --- /dev/null +++ b/node_modules/discord.js/src/structures/DMChannel.js @@ -0,0 +1,60 @@ +const Channel = require('./Channel'); +const TextBasedChannel = require('./interface/TextBasedChannel'); +const Collection = require('../util/Collection'); + +/** + * Represents a direct message channel between two users. + * @extends {Channel} + * @implements {TextBasedChannel} + */ +class DMChannel extends Channel { + constructor(client, data) { + super(client, data); + this.type = 'dm'; + this.messages = new Collection(); + this._typing = new Map(); + } + + setup(data) { + super.setup(data); + + /** + * The recipient on the other end of the DM + * @type {User} + */ + this.recipient = this.client.dataManager.newUser(data.recipients[0]); + + this.lastMessageID = data.last_message_id; + } + + /** + * When concatenated with a string, this automatically concatenates the recipient's mention instead of the + * DM channel object. + * @returns {string} + */ + toString() { + return this.recipient.toString(); + } + + // These are here only for documentation purposes - they are implemented by TextBasedChannel + send() { return; } + sendMessage() { return; } + sendEmbed() { return; } + sendFile() { return; } + sendCode() { return; } + fetchMessage() { return; } + fetchMessages() { return; } + fetchPinnedMessages() { return; } + startTyping() { return; } + stopTyping() { return; } + get typing() { return; } + get typingCount() { return; } + createCollector() { return; } + awaitMessages() { return; } + bulkDelete() { return; } + _cacheMessage() { return; } +} + +TextBasedChannel.applyToClass(DMChannel, true); + +module.exports = DMChannel; diff --git a/node_modules/discord.js/src/structures/Emoji.js b/node_modules/discord.js/src/structures/Emoji.js new file mode 100644 index 0000000..d8a62e1 --- /dev/null +++ b/node_modules/discord.js/src/structures/Emoji.js @@ -0,0 +1,140 @@ +const Constants = require('../util/Constants'); +const Collection = require('../util/Collection'); + +/** + * Represents a custom emoji + */ +class Emoji { + constructor(guild, data) { + /** + * The Client that instantiated this object + * @name Emoji#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: guild.client }); + + /** + * The guild this emoji is part of + * @type {Guild} + */ + this.guild = guild; + + this.setup(data); + } + + setup(data) { + /** + * The ID of the emoji + * @type {string} + */ + 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; + + this._roles = data.roles; + } + + /** + * The timestamp the emoji was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return (this.id / 4194304) + 1420070400000; + } + + /** + * The time the emoji was created + * @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 Constants.Endpoints.emoji(this.id); + } + + /** + * When concatenated with a string, this automatically returns the emoji mention rather than the object. + * @returns {string} + * @example + * // send an emoji: + * const emoji = guild.emojis.first(); + * msg.reply(`Hello! ${emoji}`); + */ + toString() { + return this.requiresColons ? `<:${this.name}:${this.id}>` : this.name; + } + + /** + * 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 + ); + } else { + return ( + other.id === this.id && + other.name === this.name + ); + } + } + + /** + * The identifier of this emoji, used for message reactions + * @readonly + * @type {string} + */ + get identifier() { + if (this.id) { + return `${this.name}:${this.id}`; + } + return encodeURIComponent(this.name); + } +} + +module.exports = Emoji; diff --git a/node_modules/discord.js/src/structures/EvaluatedPermissions.js b/node_modules/discord.js/src/structures/EvaluatedPermissions.js new file mode 100644 index 0000000..ae8a643 --- /dev/null +++ b/node_modules/discord.js/src/structures/EvaluatedPermissions.js @@ -0,0 +1,67 @@ +const Constants = require('../util/Constants'); + +/** + * The final evaluated permissions for a member in a channel + */ +class EvaluatedPermissions { + constructor(member, raw) { + /** + * The member this permissions refer to + * @type {GuildMember} + */ + this.member = member; + + /** + * A number representing the packed permissions + * @type {number} + */ + this.raw = raw; + } + + /** + * Get an object mapping permission name, e.g. `READ_MESSAGES` to a boolean - whether the user + * can perform this or not. + * @returns {Object} + */ + serialize() { + const serializedPermissions = {}; + for (const permissionName in Constants.PermissionFlags) { + serializedPermissions[permissionName] = this.hasPermission(permissionName); + } + return serializedPermissions; + } + + /** + * Checks whether the user has a certain permission, e.g. `READ_MESSAGES`. + * @param {PermissionResolvable} permission The permission to check for + * @param {boolean} [explicit=false] Whether to require the user to explicitly have the exact permission + * @returns {boolean} + */ + hasPermission(permission, explicit = false) { + permission = this.member.client.resolver.resolvePermission(permission); + if (!explicit && (this.raw & Constants.PermissionFlags.ADMINISTRATOR) > 0) return true; + return (this.raw & permission) > 0; + } + + /** + * Checks whether the user has all specified permissions. + * @param {PermissionResolvable[]} permissions The permissions to check for + * @param {boolean} [explicit=false] Whether to require the user to explicitly have the exact permissions + * @returns {boolean} + */ + hasPermissions(permissions, explicit = false) { + return permissions.every(p => this.hasPermission(p, explicit)); + } + + /** + * Checks whether the user has all specified permissions, and lists any missing permissions. + * @param {PermissionResolvable[]} permissions The permissions to check for + * @param {boolean} [explicit=false] Whether to require the user to explicitly have the exact permissions + * @returns {PermissionResolvable[]} + */ + missingPermissions(permissions, explicit = false) { + return permissions.filter(p => !this.hasPermission(p, explicit)); + } +} + +module.exports = EvaluatedPermissions; diff --git a/node_modules/discord.js/src/structures/GroupDMChannel.js b/node_modules/discord.js/src/structures/GroupDMChannel.js new file mode 100644 index 0000000..84fe4f9 --- /dev/null +++ b/node_modules/discord.js/src/structures/GroupDMChannel.js @@ -0,0 +1,144 @@ +const Channel = require('./Channel'); +const TextBasedChannel = require('./interface/TextBasedChannel'); +const Collection = require('../util/Collection'); + +/* +{ type: 3, + recipients: + [ { username: 'Charlie', + id: '123', + discriminator: '6631', + avatar: '123' }, + { username: 'Ben', + id: '123', + discriminator: '2055', + avatar: '123' }, + { username: 'Adam', + id: '123', + discriminator: '2406', + avatar: '123' } ], + owner_id: '123', + name: null, + last_message_id: '123', + id: '123', + icon: null } +*/ + +/** + * Represents a Group DM on Discord + * @extends {Channel} + * @implements {TextBasedChannel} + */ +class GroupDMChannel extends Channel { + constructor(client, data) { + super(client, data); + this.type = 'group'; + this.messages = new Collection(); + this._typing = new Map(); + } + + setup(data) { + super.setup(data); + + /** + * The name of this Group DM, can be null if one isn't set. + * @type {string} + */ + this.name = data.name; + + /** + * A hash of the Group DM icon. + * @type {string} + */ + this.icon = data.icon; + + /** + * The user ID of this Group DM's owner. + * @type {string} + */ + this.ownerID = data.owner_id; + + if (!this.recipients) { + /** + * A collection of the recipients of this DM, mapped by their ID. + * @type {Collection} + */ + this.recipients = new Collection(); + } + + if (data.recipients) { + for (const recipient of data.recipients) { + const user = this.client.dataManager.newUser(recipient); + this.recipients.set(user.id, user); + } + } + + this.lastMessageID = data.last_message_id; + } + + /** + * The owner of this Group DM. + * @type {User} + * @readonly + */ + get owner() { + return this.client.users.get(this.ownerID); + } + + /** + * Whether this channel equals another channel. It compares all properties, so for most operations + * it is advisable to just compare `channel.id === channel2.id` as it is much faster and is often + * what most users need. + * @param {GroupDMChannel} channel Channel to compare with + * @returns {boolean} + */ + equals(channel) { + const equal = channel && + this.id === channel.id && + this.name === channel.name && + this.icon === channel.icon && + this.ownerID === channel.ownerID; + + if (equal) { + return this.recipients.equals(channel.recipients); + } + + return equal; + } + + /** + * When concatenated with a string, this automatically concatenates the channel's name instead of the Channel object. + * @returns {string} + * @example + * // logs: Hello from My Group DM! + * console.log(`Hello from ${channel}!`); + * @example + * // logs: Hello from My Group DM! + * console.log(`Hello from ' + channel + '!'); + */ + toString() { + return this.name; + } + + // These are here only for documentation purposes - they are implemented by TextBasedChannel + send() { return; } + sendMessage() { return; } + sendEmbed() { return; } + sendFile() { return; } + sendCode() { return; } + fetchMessage() { return; } + fetchMessages() { return; } + fetchPinnedMessages() { return; } + startTyping() { return; } + stopTyping() { return; } + get typing() { return; } + get typingCount() { return; } + createCollector() { return; } + awaitMessages() { return; } + bulkDelete() { return; } + _cacheMessage() { return; } +} + +TextBasedChannel.applyToClass(GroupDMChannel, true); + +module.exports = GroupDMChannel; diff --git a/node_modules/discord.js/src/structures/Guild.js b/node_modules/discord.js/src/structures/Guild.js new file mode 100644 index 0000000..acd5b6f --- /dev/null +++ b/node_modules/discord.js/src/structures/Guild.js @@ -0,0 +1,851 @@ +const User = require('./User'); +const Role = require('./Role'); +const Emoji = require('./Emoji'); +const Presence = require('./Presence').Presence; +const GuildMember = require('./GuildMember'); +const Constants = require('../util/Constants'); +const Collection = require('../util/Collection'); +const cloneObject = require('../util/CloneObject'); +const arraysEqual = require('../util/ArraysEqual'); + +/** + * Represents a guild (or a server) on Discord. + * It's recommended to see if a guild is available before performing operations or reading data from it. You can + * check this with `guild.available`. + */ +class Guild { + constructor(client, data) { + /** + * The Client that created the instance of the the Guild. + * @name Guild#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + /** + * A collection of members that are in this guild. The key is the member's ID, the value is the member. + * @type {Collection} + */ + this.members = new Collection(); + + /** + * A collection of channels that are in this guild. The key is the channel's ID, the value is the channel. + * @type {Collection} + */ + this.channels = new Collection(); + + /** + * A collection of roles that are in this guild. The key is the role's ID, the value is the role. + * @type {Collection} + */ + this.roles = new Collection(); + + /** + * A collection of presences in this guild + * @type {Collection} + */ + this.presences = new Collection(); + + if (!data) return; + if (data.unavailable) { + /** + * Whether the guild is available to access. If it is not available, it indicates a server outage. + * @type {boolean} + */ + this.available = false; + + /** + * The Unique ID of the Guild, useful for comparisons. + * @type {string} + */ + this.id = data.id; + } else { + this.available = true; + this.setup(data); + } + } + + /** + * Sets up the Guild + * @param {*} data The raw data of the guild + * @private + */ + setup(data) { + /** + * The name of the guild + * @type {string} + */ + this.name = data.name; + + /** + * The hash of the guild icon, or null if there is no icon. + * @type {?string} + */ + this.icon = data.icon; + + /** + * The hash of the guild splash image, or null if no splash (VIP only) + * @type {?string} + */ + this.splash = data.splash; + + /** + * The region the guild is located in + * @type {string} + */ + this.region = data.region; + + /** + * The full amount of members in this guild as of `READY` + * @type {number} + */ + this.memberCount = data.member_count || this.memberCount; + + /** + * Whether the guild is "large" (has more than 250 members) + * @type {boolean} + */ + this.large = data.large || this.large; + + /** + * An array of guild features. + * @type {Object[]} + */ + this.features = data.features; + + /** + * The ID of the application that created this guild (if applicable) + * @type {?string} + */ + this.applicationID = data.application_id; + + /** + * A collection of emojis that are in this guild. The key is the emoji's ID, the value is the emoji. + * @type {Collection} + */ + this.emojis = new Collection(); + for (const emoji of data.emojis) this.emojis.set(emoji.id, new Emoji(this, emoji)); + + /** + * The time in seconds before a user is counted as "away from keyboard". + * @type {?number} + */ + this.afkTimeout = data.afk_timeout; + + /** + * The ID of the voice channel where AFK members are moved. + * @type {?string} + */ + this.afkChannelID = data.afk_channel_id; + + /** + * Whether embedded images are enabled on this guild. + * @type {boolean} + */ + this.embedEnabled = data.embed_enabled; + + /** + * The verification level of the guild. + * @type {number} + */ + this.verificationLevel = data.verification_level; + + /** + * The timestamp the client user joined the guild at + * @type {number} + */ + this.joinedTimestamp = data.joined_at ? new Date(data.joined_at).getTime() : this.joinedTimestamp; + + this.id = data.id; + this.available = !data.unavailable; + this.features = data.features || this.features || []; + + if (data.members) { + this.members.clear(); + for (const guildUser of data.members) this._addMember(guildUser, false); + } + + if (data.owner_id) { + /** + * The user ID of this guild's owner. + * @type {string} + */ + this.ownerID = data.owner_id; + } + + if (data.channels) { + this.channels.clear(); + for (const channel of data.channels) this.client.dataManager.newChannel(channel, this); + } + + if (data.roles) { + this.roles.clear(); + for (const role of data.roles) { + const newRole = new Role(this, role); + this.roles.set(newRole.id, newRole); + } + } + + if (data.presences) { + for (const presence of data.presences) { + this._setPresence(presence.user.id, presence); + } + } + + this._rawVoiceStates = new Collection(); + if (data.voice_states) { + for (const voiceState of data.voice_states) { + this._rawVoiceStates.set(voiceState.user_id, voiceState); + const member = this.members.get(voiceState.user_id); + if (member) { + member.serverMute = voiceState.mute; + member.serverDeaf = voiceState.deaf; + member.selfMute = voiceState.self_mute; + member.selfDeaf = voiceState.self_deaf; + member.voiceSessionID = voiceState.session_id; + member.voiceChannelID = voiceState.channel_id; + this.channels.get(voiceState.channel_id).members.set(member.user.id, member); + } + } + } + } + + /** + * The timestamp the guild was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return (this.id / 4194304) + 1420070400000; + } + + /** + * The time the guild was created + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The time the client user joined the guild + * @type {Date} + * @readonly + */ + get joinedAt() { + return new Date(this.joinedTimestamp); + } + + /** + * Gets the URL to this guild's icon (if it has one, otherwise it returns null) + * @type {?string} + * @readonly + */ + get iconURL() { + if (!this.icon) return null; + return Constants.Endpoints.guildIcon(this.id, this.icon); + } + + /** + * Gets the URL to this guild's splash (if it has one, otherwise it returns null) + * @type {?string} + * @readonly + */ + get splashURL() { + if (!this.splash) return null; + return Constants.Endpoints.guildSplash(this.id, this.splash); + } + + /** + * The owner of the guild + * @type {GuildMember} + * @readonly + */ + get owner() { + return this.members.get(this.ownerID); + } + + /** + * If the client is connected to any voice channel in this guild, this will be the relevant VoiceConnection. + * @type {?VoiceConnection} + * @readonly + */ + get voiceConnection() { + if (this.client.browser) return null; + return this.client.voice.connections.get(this.id) || null; + } + + /** + * The `#general` GuildChannel of the server. + * @type {GuildChannel} + * @readonly + */ + get defaultChannel() { + return this.channels.get(this.id); + } + + /** + * Returns the GuildMember form of a User object, if the user is present in the guild. + * @param {UserResolvable} user The user that you want to obtain the GuildMember of + * @returns {?GuildMember} + * @example + * // get the guild member of a user + * const member = guild.member(message.author); + */ + member(user) { + return this.client.resolver.resolveGuildMember(this, user); + } + + /** + * Fetch a collection of banned users in this guild. + * @returns {Promise>} + */ + fetchBans() { + return this.client.rest.methods.getGuildBans(this); + } + + /** + * Fetch a collection of invites to this guild. Resolves with a collection mapping invites by their codes. + * @returns {Promise>} + */ + fetchInvites() { + return this.client.rest.methods.getGuildInvites(this); + } + + /** + * Fetch all webhooks for the guild. + * @returns {Collection} + */ + fetchWebhooks() { + return this.client.rest.methods.getGuildWebhooks(this); + } + + /** + * Fetch a single guild member from a user. + * @param {UserResolvable} user The user to fetch the member for + * @returns {Promise} + */ + fetchMember(user) { + if (this._fetchWaiter) return Promise.reject(new Error('Already fetching guild members.')); + user = this.client.resolver.resolveUser(user); + if (!user) return Promise.reject(new Error('User is not cached. Use Client.fetchUser first.')); + if (this.members.has(user.id)) return Promise.resolve(this.members.get(user.id)); + return this.client.rest.methods.getGuildMember(this, user); + } + + /** + * Fetches all the members in the guild, even if they are offline. If the guild has less than 250 members, + * this should not be necessary. + * @param {string} [query=''] An optional query to provide when fetching members + * @returns {Promise} + */ + fetchMembers(query = '') { + return new Promise((resolve, reject) => { + if (this._fetchWaiter) throw new Error('Already fetching guild members in ${this.id}.'); + if (this.memberCount === this.members.size) { + resolve(this); + return; + } + this._fetchWaiter = resolve; + this.client.ws.send({ + op: Constants.OPCodes.REQUEST_GUILD_MEMBERS, + d: { + guild_id: this.id, + query, + limit: 0, + }, + }); + this._checkChunks(); + this.client.setTimeout(() => reject(new Error('Members didn\'t arrive in time.')), 120 * 1000); + }); + } + + /** + * The data for editing a guild + * @typedef {Object} GuildEditData + * @property {string} [name] The name of the guild + * @property {string} [region] The region of the guild + * @property {number} [verificationLevel] The verification level of the guild + * @property {ChannelResolvable} [afkChannel] The AFK channel of the guild + * @property {number} [afkTimeout] The AFK timeout of the guild + * @property {Base64Resolvable} [icon] The icon of the guild + * @property {GuildMemberResolvable} [owner] The owner of the guild + * @property {Base64Resolvable} [splash] The splash screen of the guild + */ + + /** + * Updates the Guild with new information - e.g. a new name. + * @param {GuildEditData} data The data to update the guild with + * @returns {Promise} + * @example + * // set the guild name and region + * guild.edit({ + * name: 'Discord Guild', + * region: 'london', + * }) + * .then(updated => console.log(`New guild name ${updated.name} in region ${updated.region}`)) + * .catch(console.error); + */ + edit(data) { + return this.client.rest.methods.updateGuild(this, data); + } + + /** + * Edit the name of the guild. + * @param {string} name The new name of the guild + * @returns {Promise} + * @example + * // edit the guild name + * guild.setName('Discord Guild') + * .then(updated => console.log(`Updated guild name to ${guild.name}`)) + * .catch(console.error); + */ + setName(name) { + return this.edit({ name }); + } + + /** + * Edit the region of the guild. + * @param {string} region The new region of the guild. + * @returns {Promise} + * @example + * // edit the guild region + * guild.setRegion('london') + * .then(updated => console.log(`Updated guild region to ${guild.region}`)) + * .catch(console.error); + */ + setRegion(region) { + return this.edit({ region }); + } + + /** + * Edit the verification level of the guild. + * @param {number} verificationLevel The new verification level of the guild + * @returns {Promise} + * @example + * // edit the guild verification level + * guild.setVerificationLevel(1) + * .then(updated => console.log(`Updated guild verification level to ${guild.verificationLevel}`)) + * .catch(console.error); + */ + setVerificationLevel(verificationLevel) { + return this.edit({ verificationLevel }); + } + + /** + * Edit the AFK channel of the guild. + * @param {ChannelResolvable} afkChannel The new AFK channel + * @returns {Promise} + * @example + * // edit the guild AFK channel + * guild.setAFKChannel(channel) + * .then(updated => console.log(`Updated guild AFK channel to ${guild.afkChannel}`)) + * .catch(console.error); + */ + setAFKChannel(afkChannel) { + return this.edit({ afkChannel }); + } + + /** + * Edit the AFK timeout of the guild. + * @param {number} afkTimeout The time in seconds that a user must be idle to be considered AFK + * @returns {Promise} + * @example + * // edit the guild AFK channel + * guild.setAFKTimeout(60) + * .then(updated => console.log(`Updated guild AFK timeout to ${guild.afkTimeout}`)) + * .catch(console.error); + */ + setAFKTimeout(afkTimeout) { + return this.edit({ afkTimeout }); + } + + /** + * Set a new guild icon. + * @param {Base64Resolvable} icon The new icon of the guild + * @returns {Promise} + * @example + * // edit the guild icon + * guild.setIcon(fs.readFileSync('./icon.png')) + * .then(updated => console.log('Updated the guild icon')) + * .catch(console.error); + */ + setIcon(icon) { + return this.edit({ icon }); + } + + /** + * Sets a new owner of the guild. + * @param {GuildMemberResolvable} owner The new owner of the guild + * @returns {Promise} + * @example + * // edit the guild owner + * guild.setOwner(guilds.members[0]) + * .then(updated => console.log(`Updated the guild owner to ${updated.owner.username}`)) + * .catch(console.error); + */ + setOwner(owner) { + return this.edit({ owner }); + } + + /** + * Set a new guild splash screen. + * @param {Base64Resolvable} splash The new splash screen of the guild + * @returns {Promise} + * @example + * // edit the guild splash + * guild.setIcon(fs.readFileSync('./splash.png')) + * .then(updated => console.log('Updated the guild splash')) + * .catch(console.error); + */ + setSplash(splash) { + return this.edit({ splash }); + } + + /** + * Bans a user from the guild. + * @param {UserResolvable} user The user to ban + * @param {number} [deleteDays=0] The amount of days worth of messages from this user that should + * also be deleted. Between `0` and `7`. + * @returns {Promise} Result object will be resolved as specifically as possible. + * If the GuildMember cannot be resolved, the User will instead be attempted to be resolved. If that also cannot + * be resolved, the user ID will be the result. + * @example + * // ban a user + * guild.ban('123123123123'); + */ + ban(user, deleteDays = 0) { + return this.client.rest.methods.banGuildMember(this, user, deleteDays); + } + + /** + * Unbans a user from the guild. + * @param {UserResolvable} user The user to unban + * @returns {Promise} + * @example + * // unban a user + * guild.unban('123123123123') + * .then(user => console.log(`Unbanned ${user.username} from ${guild.name}`)) + * .catch(reject); + */ + unban(user) { + return this.client.rest.methods.unbanGuildMember(this, user); + } + + /** + * Prunes members from the guild based on how long they have been inactive. + * @param {number} days Number of days of inactivity required to kick + * @param {boolean} [dry=false] If true, will return number of users that will be kicked, without actually doing it + * @returns {Promise} The number of members that were/will be kicked + * @example + * // see how many members will be pruned + * guild.pruneMembers(12, true) + * .then(pruned => console.log(`This will prune ${pruned} people!`); + * .catch(console.error); + * @example + * // actually prune the members + * guild.pruneMembers(12) + * .then(pruned => console.log(`I just pruned ${pruned} people!`); + * .catch(console.error); + */ + pruneMembers(days, dry = false) { + if (typeof days !== 'number') throw new TypeError('Days must be a number.'); + return this.client.rest.methods.pruneGuildMembers(this, days, dry); + } + + /** + * Syncs this guild (already done automatically every 30 seconds). + * This is only available when using a user account. + */ + sync() { + if (!this.client.user.bot) this.client.syncGuilds([this]); + } + + /** + * 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 {Array} overwrites Permission overwrites to apply to the new channel + * @returns {Promise} + * @example + * // create a new text channel + * guild.createChannel('new-general', 'text') + * .then(channel => console.log(`Created new channel ${channel}`)) + * .catch(console.error); + */ + createChannel(name, type, overwrites) { + return this.client.rest.methods.createChannel(this, name, type, overwrites); + } + + /** + * Creates a new role in the guild, and optionally updates it with the given information. + * @param {RoleData} [data] The data to update the role with + * @returns {Promise} + * @example + * // create a new role + * guild.createRole() + * .then(role => console.log(`Created role ${role}`)) + * .catch(console.error); + * @example + * // create a new role with data + * guild.createRole({ name: 'Super Cool People' }) + * .then(role => console.log(`Created role ${role}`)) + * .catch(console.error) + */ + createRole(data) { + const create = this.client.rest.methods.createGuildRole(this); + if (!data) return create; + return create.then(role => role.edit(data)); + } + + /** + * Creates a new custom emoji in the guild. + * @param {BufferResolvable} attachment The image for the emoji. + * @param {string} name The name for the emoji. + * @returns {Promise} The created emoji. + * @example + * // create a new emoji from a url + * guild.createEmoji('https://i.imgur.com/w3duR07.png', 'rip') + * .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`)) + * .catch(console.error); + * @example + * // create a new emoji from a file on your computer + * guild.createEmoji('./memes/banana.png', 'banana') + * .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`)) + * .catch(console.error); + */ + createEmoji(attachment, name) { + return new Promise(resolve => { + if (attachment.startsWith('data:')) { + resolve(this.client.rest.methods.createEmoji(this, attachment, name)); + } else { + this.client.resolver.resolveBuffer(attachment).then(data => + resolve(this.client.rest.methods.createEmoji(this, data, name)) + ); + } + }); + } + + /** + * Delete an emoji. + * @param {Emoji|string} emoji The emoji to delete. + * @returns {Promise} + */ + deleteEmoji(emoji) { + if (!(emoji instanceof Emoji)) emoji = this.emojis.get(emoji); + return this.client.rest.methods.deleteEmoji(emoji); + } + + /** + * Causes the Client to leave the guild. + * @returns {Promise} + * @example + * // leave a guild + * guild.leave() + * .then(g => console.log(`Left the guild ${g}`)) + * .catch(console.error); + */ + leave() { + return this.client.rest.methods.leaveGuild(this); + } + + /** + * Causes the Client to delete the guild. + * @returns {Promise} + * @example + * // delete a guild + * guild.delete() + * .then(g => console.log(`Deleted the guild ${g}`)) + * .catch(console.error); + */ + delete() { + return this.client.rest.methods.deleteGuild(this); + } + + /** + * Set the position of a role in this guild + * @param {string|Role} role the role to edit, can be a role object or a role ID. + * @param {number} position the new position of the role + * @returns {Promise} + */ + setRolePosition(role, position) { + if (typeof role === 'string') { + role = this.roles.get(role); + if (!role) return Promise.reject(new Error('Supplied role is not a role or string.')); + } + + position = Number(position); + if (isNaN(position)) return Promise.reject(new Error('Supplied position is not a number.')); + + const lowestAffected = Math.min(role.position, position); + const highestAffected = Math.max(role.position, position); + + const rolesToUpdate = this.roles.filter(r => r.position >= lowestAffected && r.position <= highestAffected); + + // stop role positions getting stupidly inflated + if (position > role.position) { + position = rolesToUpdate.first().position; + } else { + position = rolesToUpdate.last().position; + } + + const updatedRoles = []; + + for (const uRole of rolesToUpdate.values()) { + updatedRoles.push({ + id: uRole.id, + position: uRole.id === role.id ? position : uRole.position + (position < role.position ? 1 : -1), + }); + } + + return this.client.rest.methods.setRolePositions(this.id, updatedRoles); + } + + /** + * Whether this Guild equals another Guild. It compares all properties, so for most operations + * it is advisable to just compare `guild.id === guild2.id` as it is much faster and is often + * what most users need. + * @param {Guild} guild Guild to compare with + * @returns {boolean} + */ + equals(guild) { + let equal = + guild && + this.id === guild.id && + this.available === !guild.unavailable && + this.splash === guild.splash && + this.region === guild.region && + this.name === guild.name && + this.memberCount === guild.member_count && + this.large === guild.large && + this.icon === guild.icon && + arraysEqual(this.features, guild.features) && + this.ownerID === guild.owner_id && + this.verificationLevel === guild.verification_level && + this.embedEnabled === guild.embed_enabled; + + if (equal) { + if (this.embedChannel) { + if (this.embedChannel.id !== guild.embed_channel_id) equal = false; + } else if (guild.embed_channel_id) { + equal = false; + } + } + + return equal; + } + + /** + * When concatenated with a string, this automatically concatenates the guild's name instead of the Guild object. + * @returns {string} + * @example + * // logs: Hello from My Guild! + * console.log(`Hello from ${guild}!`); + * @example + * // logs: Hello from My Guild! + * console.log(`Hello from ' + guild + '!'); + */ + toString() { + return this.name; + } + + _addMember(guildUser, emitEvent = true) { + const existing = this.members.has(guildUser.user.id); + if (!(guildUser.user instanceof User)) guildUser.user = this.client.dataManager.newUser(guildUser.user); + + guildUser.joined_at = guildUser.joined_at || 0; + const member = new GuildMember(this, guildUser); + this.members.set(member.id, member); + + if (this._rawVoiceStates && this._rawVoiceStates.has(member.user.id)) { + const voiceState = this._rawVoiceStates.get(member.user.id); + member.serverMute = voiceState.mute; + member.serverDeaf = voiceState.deaf; + member.selfMute = voiceState.self_mute; + member.selfDeaf = voiceState.self_deaf; + member.voiceSessionID = voiceState.session_id; + member.voiceChannelID = voiceState.channel_id; + if (this.client.channels.has(voiceState.channel_id)) { + this.client.channels.get(voiceState.channel_id).members.set(member.user.id, member); + } else { + this.client.emit('warn', `Member ${member.id} added in guild ${this.id} with an uncached voice channel`); + } + } + + /** + * Emitted whenever a user joins a guild. + * @event Client#guildMemberAdd + * @param {GuildMember} member The member that has joined a guild + */ + if (this.client.ws.status === Constants.Status.READY && emitEvent && !existing) { + this.client.emit(Constants.Events.GUILD_MEMBER_ADD, member); + } + + this._checkChunks(); + return member; + } + + _updateMember(member, data) { + const oldMember = cloneObject(member); + + if (data.roles) member._roles = data.roles; + if (typeof data.nick !== 'undefined') member.nickname = data.nick; + + const notSame = member.nickname !== oldMember.nickname || !arraysEqual(member._roles, oldMember._roles); + + if (this.client.ws.status === Constants.Status.READY && notSame) { + /** + * Emitted whenever a guild member changes - i.e. new role, removed role, nickname + * @event Client#guildMemberUpdate + * @param {GuildMember} oldMember The member before the update + * @param {GuildMember} newMember The member after the update + */ + this.client.emit(Constants.Events.GUILD_MEMBER_UPDATE, oldMember, member); + } + + return { + old: oldMember, + mem: member, + }; + } + + _removeMember(guildMember) { + this.members.delete(guildMember.id); + this._checkChunks(); + } + + _memberSpeakUpdate(user, speaking) { + const member = this.members.get(user); + if (member && member.speaking !== speaking) { + member.speaking = speaking; + /** + * Emitted once a guild member starts/stops speaking + * @event Client#guildMemberSpeaking + * @param {GuildMember} member The member that started/stopped speaking + * @param {boolean} speaking Whether or not the member is speaking + */ + this.client.emit(Constants.Events.GUILD_MEMBER_SPEAKING, member, speaking); + } + } + + _setPresence(id, presence) { + if (this.presences.get(id)) { + this.presences.get(id).update(presence); + return; + } + this.presences.set(id, new Presence(presence)); + } + + _checkChunks() { + if (this._fetchWaiter) { + if (this.members.size === this.memberCount) { + this._fetchWaiter(this); + this._fetchWaiter = null; + } + } + } +} + +module.exports = Guild; diff --git a/node_modules/discord.js/src/structures/GuildChannel.js b/node_modules/discord.js/src/structures/GuildChannel.js new file mode 100644 index 0000000..a930789 --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildChannel.js @@ -0,0 +1,299 @@ +const Channel = require('./Channel'); +const Role = require('./Role'); +const PermissionOverwrites = require('./PermissionOverwrites'); +const EvaluatedPermissions = require('./EvaluatedPermissions'); +const Constants = require('../util/Constants'); +const Collection = require('../util/Collection'); + +/** + * Represents a guild channel (i.e. text channels and voice channels) + * @extends {Channel} + */ +class GuildChannel extends Channel { + constructor(guild, data) { + super(guild.client, data); + + /** + * The guild the channel is in + * @type {Guild} + */ + this.guild = guild; + } + + setup(data) { + super.setup(data); + + /** + * The name of the guild channel + * @type {string} + */ + this.name = data.name; + + /** + * The position of the channel in the list. + * @type {number} + */ + this.position = data.position; + + /** + * A map of permission overwrites in this channel for roles and users. + * @type {Collection} + */ + this.permissionOverwrites = new Collection(); + if (data.permission_overwrites) { + for (const overwrite of data.permission_overwrites) { + this.permissionOverwrites.set(overwrite.id, new PermissionOverwrites(this, overwrite)); + } + } + } + + /** + * 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 + * @returns {?EvaluatedPermissions} + */ + permissionsFor(member) { + member = this.client.resolver.resolveGuildMember(this.guild, member); + if (!member) return null; + if (member.id === this.guild.ownerID) return new EvaluatedPermissions(member, Constants.ALL_PERMISSIONS); + + let permissions = 0; + + const roles = member.roles; + for (const role of roles.values()) permissions |= role.permissions; + + const overwrites = this.overwritesFor(member, true, roles); + for (const overwrite of overwrites.role.concat(overwrites.member)) { + permissions &= ~overwrite.deny; + permissions |= overwrite.allow; + } + + const admin = Boolean(permissions & Constants.PermissionFlags.ADMINISTRATOR); + if (admin) permissions = Constants.ALL_PERMISSIONS; + + return new EvaluatedPermissions(member, permissions); + } + + overwritesFor(member, verified = false, roles = null) { + if (!verified) member = this.client.resolver.resolveGuildMember(this.guild, member); + if (!member) return []; + + roles = roles || member.roles; + const roleOverwrites = []; + const memberOverwrites = []; + + for (const overwrite of this.permissionOverwrites.values()) { + if (overwrite.id === member.id) { + memberOverwrites.push(overwrite); + } else if (roles.has(overwrite.id)) { + roleOverwrites.push(overwrite); + } + } + + return { + role: roleOverwrites, + member: memberOverwrites, + }; + } + + /** + * An object mapping permission flags to `true` (enabled) or `false` (disabled) + * ```js + * { + * 'SEND_MESSAGES': true, + * 'ATTACH_FILES': false, + * } + * ``` + * @typedef {Object} PermissionOverwriteOptions + */ + + /** + * Overwrites the permissions for a user or role in this channel. + * @param {RoleResolvable|UserResolvable} userOrRole The user or role to update + * @param {PermissionOverwriteOptions} options The configuration for the update + * @returns {Promise} + * @example + * // overwrite permissions for a message author + * message.channel.overwritePermissions(message.author, { + * SEND_MESSAGES: false + * }) + * .then(() => console.log('Done!')) + * .catch(console.error); + */ + overwritePermissions(userOrRole, options) { + const payload = { + allow: 0, + deny: 0, + }; + + if (userOrRole instanceof Role) { + payload.type = 'role'; + } else if (this.guild.roles.has(userOrRole)) { + userOrRole = this.guild.roles.get(userOrRole); + payload.type = 'role'; + } else { + userOrRole = this.client.resolver.resolveUser(userOrRole); + payload.type = 'member'; + if (!userOrRole) return Promise.reject(new TypeError('Supplied parameter was neither a User nor a Role.')); + } + + payload.id = userOrRole.id; + + const prevOverwrite = this.permissionOverwrites.get(userOrRole.id); + + if (prevOverwrite) { + payload.allow = prevOverwrite.allow; + payload.deny = prevOverwrite.deny; + } + + for (const perm in options) { + if (options[perm] === true) { + payload.allow |= Constants.PermissionFlags[perm] || 0; + payload.deny &= ~(Constants.PermissionFlags[perm] || 0); + } else if (options[perm] === false) { + payload.allow &= ~(Constants.PermissionFlags[perm] || 0); + payload.deny |= Constants.PermissionFlags[perm] || 0; + } else if (options[perm] === null) { + payload.allow &= ~(Constants.PermissionFlags[perm] || 0); + payload.deny &= ~(Constants.PermissionFlags[perm] || 0); + } + } + + return this.client.rest.methods.setChannelOverwrite(this, payload); + } + + /** + * The data for a guild channel + * @typedef {Object} ChannelData + * @property {string} [name] The name of the channel + * @property {number} [position] The position of the channel + * @property {string} [topic] The topic of the text channel + * @property {number} [bitrate] The bitrate of the voice channel + * @property {number} [userLimit] The user limit of the channel + */ + + /** + * Edits the channel + * @param {ChannelData} data The new data for the channel + * @returns {Promise} + * @example + * // edit a channel + * channel.edit({name: 'new-channel'}) + * .then(c => console.log(`Edited channel ${c}`)) + * .catch(console.error); + */ + edit(data) { + return this.client.rest.methods.updateChannel(this, data); + } + + /** + * Set a new name for the guild channel + * @param {string} name The new name for the guild channel + * @returns {Promise} + * @example + * // set a new channel name + * channel.setName('not_general') + * .then(newChannel => console.log(`Channel's new name is ${newChannel.name}`)) + * .catch(console.error); + */ + setName(name) { + return this.edit({ name }); + } + + /** + * Set a new position for the guild channel + * @param {number} position The new position for the guild channel + * @returns {Promise} + * @example + * // set a new channel position + * channel.setPosition(2) + * .then(newChannel => console.log(`Channel's new position is ${newChannel.position}`)) + * .catch(console.error); + */ + setPosition(position) { + return this.client.rest.methods.updateChannel(this, { position }); + } + + /** + * Set a new topic for the guild channel + * @param {string} topic The new topic for the guild channel + * @returns {Promise} + * @example + * // set a new channel topic + * channel.setTopic('needs more rate limiting') + * .then(newChannel => console.log(`Channel's new topic is ${newChannel.topic}`)) + * .catch(console.error); + */ + setTopic(topic) { + return this.client.rest.methods.updateChannel(this, { topic }); + } + + /** + * Options given when creating a guild channel invite + * @typedef {Object} InviteOptions + * @property {boolean} [temporary=false] Whether the invite should kick users after 24hrs if they are not given a role + * @property {number} [maxAge=0] Time in seconds the invite expires in + * @property {number} [maxUses=0] Maximum amount of uses for this invite + */ + + /** + * Create an invite to this guild channel + * @param {InviteOptions} [options={}] The options for the invite + * @returns {Promise} + */ + createInvite(options = {}) { + return this.client.rest.methods.createChannelInvite(this, options); + } + + /** + * Clone this channel + * @param {string} [name=this.name] Optional name for the new channel, otherwise it has the name of this channel + * @param {boolean} [withPermissions=true] Whether to clone the channel with this channel's permission overwrites + * @returns {Promise} + */ + clone(name = this.name, withPermissions = true) { + return this.guild.createChannel(name, this.type, withPermissions ? this.permissionOverwrites : []); + } + + /** + * Checks if this channel has the same type, topic, position, name, overwrites and ID as another channel. + * In most cases, a simple `channel.id === channel2.id` will do, and is much faster too. + * @param {GuildChannel} channel Channel to compare with + * @returns {boolean} + */ + equals(channel) { + let equal = channel && + this.id === channel.id && + this.type === channel.type && + this.topic === channel.topic && + this.position === channel.position && + this.name === channel.name; + + if (equal) { + if (this.permissionOverwrites && channel.permissionOverwrites) { + equal = this.permissionOverwrites.equals(channel.permissionOverwrites); + } else { + equal = !this.permissionOverwrites && !channel.permissionOverwrites; + } + } + + return equal; + } + + /** + * When concatenated with a string, this automatically returns the channel's mention instead of the Channel object. + * @returns {string} + * @example + * // Outputs: Hello from #general + * console.log(`Hello from ${channel}`); + * @example + * // Outputs: Hello from #general + * console.log('Hello from ' + channel); + */ + toString() { + return `<#${this.id}>`; + } +} + +module.exports = GuildChannel; diff --git a/node_modules/discord.js/src/structures/GuildMember.js b/node_modules/discord.js/src/structures/GuildMember.js new file mode 100644 index 0000000..60a498a --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildMember.js @@ -0,0 +1,442 @@ +const TextBasedChannel = require('./interface/TextBasedChannel'); +const Role = require('./Role'); +const EvaluatedPermissions = require('./EvaluatedPermissions'); +const Constants = require('../util/Constants'); +const Collection = require('../util/Collection'); +const Presence = require('./Presence').Presence; + +/** + * Represents a member of a guild on Discord + * @implements {TextBasedChannel} + */ +class GuildMember { + constructor(guild, data) { + /** + * The Client that instantiated this GuildMember + * @name GuildMember#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: guild.client }); + + /** + * The guild that this member is part of + * @type {Guild} + */ + this.guild = guild; + + /** + * The user that this guild member instance Represents + * @type {User} + */ + this.user = {}; + + this._roles = []; + if (data) this.setup(data); + + /** + * The ID of the last message sent by the member in their guild, if one was sent. + * @type {?string} + */ + this.lastMessageID = null; + } + + setup(data) { + /** + * Whether this member is deafened server-wide + * @type {boolean} + */ + this.serverDeaf = data.deaf; + + /** + * Whether this member is muted server-wide + * @type {boolean} + */ + this.serverMute = data.mute; + + /** + * Whether this member is self-muted + * @type {boolean} + */ + this.selfMute = data.self_mute; + + /** + * Whether this member is self-deafened + * @type {boolean} + */ + this.selfDeaf = data.self_deaf; + + /** + * The voice session ID of this member, if any + * @type {?string} + */ + this.voiceSessionID = data.session_id; + + /** + * The voice channel ID of this member, if any + * @type {?string} + */ + this.voiceChannelID = data.channel_id; + + /** + * Whether this member is speaking + * @type {boolean} + */ + this.speaking = false; + + /** + * The nickname of this guild member, if they have one + * @type {?string} + */ + this.nickname = data.nick || null; + + /** + * The timestamp the member joined the guild at + * @type {number} + */ + this.joinedTimestamp = new Date(data.joined_at).getTime(); + + this.user = data.user; + this._roles = data.roles; + } + + /** + * The time the member joined the guild + * @type {Date} + * @readonly + */ + get joinedAt() { + return new Date(this.joinedTimestamp); + } + + /** + * The presence of this guild member + * @type {Presence} + * @readonly + */ + get presence() { + return this.frozenPresence || this.guild.presences.get(this.id) || new Presence(); + } + + /** + * A list of roles that are applied to this GuildMember, mapped by the role ID. + * @type {Collection} + * @readonly + */ + get roles() { + const list = new Collection(); + const everyoneRole = this.guild.roles.get(this.guild.id); + + if (everyoneRole) list.set(everyoneRole.id, everyoneRole); + + for (const roleID of this._roles) { + const role = this.guild.roles.get(roleID); + if (role) list.set(role.id, role); + } + + return list; + } + + /** + * The role of the member with the highest position. + * @type {Role} + * @readonly + */ + get highestRole() { + return this.roles.reduce((prev, role) => !prev || role.comparePositionTo(prev) > 0 ? role : prev); + } + + /** + * Whether this member is muted in any way + * @type {boolean} + * @readonly + */ + get mute() { + return this.selfMute || this.serverMute; + } + + /** + * Whether this member is deafened in any way + * @type {boolean} + * @readonly + */ + get deaf() { + return this.selfDeaf || this.serverDeaf; + } + + /** + * The voice channel this member is in, if any + * @type {?VoiceChannel} + * @readonly + */ + get voiceChannel() { + return this.guild.channels.get(this.voiceChannelID); + } + + /** + * The ID of this user + * @type {string} + * @readonly + */ + get id() { + return this.user.id; + } + + /** + * The nickname of the member, or their username if they don't have one + * @type {string} + * @readonly + */ + get displayName() { + return this.nickname || this.user.username; + } + + /** + * The overall set of permissions for the guild member, taking only roles into account + * @type {EvaluatedPermissions} + * @readonly + */ + get permissions() { + if (this.user.id === this.guild.ownerID) return new EvaluatedPermissions(this, Constants.ALL_PERMISSIONS); + + let permissions = 0; + const roles = this.roles; + for (const role of roles.values()) permissions |= role.permissions; + + const admin = Boolean(permissions & Constants.PermissionFlags.ADMINISTRATOR); + if (admin) permissions = Constants.ALL_PERMISSIONS; + + return new EvaluatedPermissions(this, permissions); + } + + /** + * Whether the member is kickable by the client user. + * @type {boolean} + * @readonly + */ + get kickable() { + if (this.user.id === this.guild.ownerID) return false; + if (this.user.id === this.client.user.id) return false; + const clientMember = this.guild.member(this.client.user); + if (!clientMember.hasPermission(Constants.PermissionFlags.KICK_MEMBERS)) return false; + return clientMember.highestRole.comparePositionTo(this.highestRole) > 0; + } + + /** + * Whether the member is bannable by the client user. + * @type {boolean} + * @readonly + */ + get bannable() { + if (this.user.id === this.guild.ownerID) return false; + if (this.user.id === this.client.user.id) return false; + const clientMember = this.guild.member(this.client.user); + if (!clientMember.hasPermission(Constants.PermissionFlags.BAN_MEMBERS)) return false; + return clientMember.highestRole.comparePositionTo(this.highestRole) > 0; + } + + /** + * Returns `channel.permissionsFor(guildMember)`. Returns evaluated permissions for a member in a guild channel. + * @param {ChannelResolvable} channel Guild channel to use as context + * @returns {?EvaluatedPermissions} + */ + permissionsIn(channel) { + channel = this.client.resolver.resolveChannel(channel); + if (!channel || !channel.guild) throw new Error('Could not resolve channel to a guild channel.'); + return channel.permissionsFor(this); + } + + /** + * Checks if any of the member's roles have a permission. + * @param {PermissionResolvable} permission The permission to check for + * @param {boolean} [explicit=false] Whether to require the roles to explicitly have the exact permission + * @returns {boolean} + */ + hasPermission(permission, explicit = false) { + if (!explicit && this.user.id === this.guild.ownerID) return true; + return this.roles.some(r => r.hasPermission(permission, explicit)); + } + + /** + * Checks whether the roles of the member allows them to perform specific actions. + * @param {PermissionResolvable[]} permissions The permissions to check for + * @param {boolean} [explicit=false] Whether to require the member to explicitly have the exact permissions + * @returns {boolean} + */ + hasPermissions(permissions, explicit = false) { + if (!explicit && this.user.id === this.guild.ownerID) return true; + return permissions.every(p => this.hasPermission(p, explicit)); + } + + /** + * Checks whether the roles of the member allows them to perform specific actions, and lists any missing permissions. + * @param {PermissionResolvable[]} permissions The permissions to check for + * @param {boolean} [explicit=false] Whether to require the member to explicitly have the exact permissions + * @returns {PermissionResolvable[]} + */ + missingPermissions(permissions, explicit = false) { + return permissions.filter(p => !this.hasPermission(p, explicit)); + } + + /** + * Edit a guild member + * @param {GuildmemberEditData} data The data to edit the member with + * @returns {Promise} + */ + edit(data) { + return this.client.rest.methods.updateGuildMember(this, data); + } + + /** + * Mute/unmute a user + * @param {boolean} mute Whether or not the member should be muted + * @returns {Promise} + */ + setMute(mute) { + return this.edit({ mute }); + } + + /** + * Deafen/undeafen a user + * @param {boolean} deaf Whether or not the member should be deafened + * @returns {Promise} + */ + setDeaf(deaf) { + return this.edit({ deaf }); + } + + /** + * Moves the guild member to the given channel. + * @param {ChannelResolvable} channel The channel to move the member to + * @returns {Promise} + */ + setVoiceChannel(channel) { + return this.edit({ channel }); + } + + /** + * Sets the roles applied to the member. + * @param {Collection|Role[]|string[]} roles The roles or role IDs to apply + * @returns {Promise} + */ + setRoles(roles) { + return this.edit({ roles }); + } + + /** + * Adds a single role to the member. + * @param {Role|string} role The role or ID of the role to add + * @returns {Promise} + */ + addRole(role) { + if (!(role instanceof Role)) role = this.guild.roles.get(role); + return this.client.rest.methods.addMemberRole(this, role); + } + + /** + * Adds multiple roles to the member. + * @param {Collection|Role[]|string[]} roles The roles or role IDs to add + * @returns {Promise} + */ + addRoles(roles) { + let allRoles; + if (roles instanceof Collection) { + allRoles = this._roles.slice(); + for (const role of roles.values()) allRoles.push(role.id); + } else { + allRoles = this._roles.concat(roles); + } + return this.edit({ roles: allRoles }); + } + + /** + * Removes a single role from the member. + * @param {Role|string} role The role or ID of the role to remove + * @returns {Promise} + */ + removeRole(role) { + if (!(role instanceof Role)) role = this.guild.roles.get(role); + return this.client.rest.methods.removeMemberRole(this, role); + } + + /** + * Removes multiple roles from the member. + * @param {Collection|Role[]|string[]} roles The roles or role IDs to remove + * @returns {Promise} + */ + removeRoles(roles) { + 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); + } + } + return this.edit({ roles: allRoles }); + } + + /** + * Set the nickname for the guild member + * @param {string} nick The nickname for the guild member + * @returns {Promise} + */ + setNickname(nick) { + return this.edit({ nick }); + } + + /** + * Deletes any DMs with this guild member + * @returns {Promise} + */ + deleteDM() { + return this.client.rest.methods.deleteChannel(this); + } + + /** + * Kick this member from the guild + * @returns {Promise} + */ + kick() { + return this.client.rest.methods.kickGuildMember(this.guild, this); + } + + /** + * Ban this guild member + * @param {number} [deleteDays=0] The amount of days worth of messages from this member that should + * also be deleted. Between `0` and `7`. + * @returns {Promise} + * @example + * // ban a guild member + * guildMember.ban(7); + */ + ban(deleteDays = 0) { + return this.client.rest.methods.banGuildMember(this.guild, this, deleteDays); + } + + /** + * When concatenated with a string, this automatically concatenates the user's mention instead of the Member object. + * @returns {string} + * @example + * // logs: Hello from <@123456789>! + * console.log(`Hello from ${member}!`); + */ + toString() { + return `<@${this.nickname ? '!' : ''}${this.user.id}>`; + } + + // These are here only for documentation purposes - they are implemented by TextBasedChannel + send() { return; } + sendMessage() { return; } + sendEmbed() { return; } + sendFile() { return; } + sendCode() { return; } +} + +TextBasedChannel.applyToClass(GuildMember); + +module.exports = GuildMember; diff --git a/node_modules/discord.js/src/structures/Invite.js b/node_modules/discord.js/src/structures/Invite.js new file mode 100644 index 0000000..b4b34da --- /dev/null +++ b/node_modules/discord.js/src/structures/Invite.js @@ -0,0 +1,159 @@ +const PartialGuild = require('./PartialGuild'); +const PartialGuildChannel = require('./PartialGuildChannel'); +const Constants = require('../util/Constants'); + +/* +{ max_age: 86400, + code: 'CG9A5', + guild: + { splash: null, + id: '123123123', + icon: '123123123', + name: 'name' }, + created_at: '2016-08-28T19:07:04.763368+00:00', + temporary: false, + uses: 0, + max_uses: 0, + inviter: + { username: '123', + discriminator: '4204', + bot: true, + id: '123123123', + avatar: '123123123' }, + channel: { type: 0, id: '123123', name: 'heavy-testing' } } +*/ + +/** + * Represents an invitation to a guild channel. + * The only guaranteed properties are `code`, `guild` and `channel`. Other properties can be missing. + */ +class Invite { + constructor(client, data) { + /** + * The client that instantiated the invite + * @name Invite#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + this.setup(data); + } + + setup(data) { + /** + * The guild the invite is for. If this guild is already known, this will be a Guild object. If the guild is + * unknown, this will be a PartialGuild object. + * @type {Guild|PartialGuild} + */ + this.guild = this.client.guilds.get(data.guild.id) || new PartialGuild(this.client, data.guild); + + /** + * The code for this invite + * @type {string} + */ + this.code = data.code; + + /** + * Whether or not this invite is temporary + * @type {boolean} + */ + this.temporary = data.temporary; + + /** + * The maximum age of the invite, in seconds + * @type {?number} + */ + this.maxAge = data.max_age; + + /** + * How many times this invite has been used + * @type {number} + */ + this.uses = data.uses; + + /** + * The maximum uses of this invite + * @type {number} + */ + this.maxUses = data.max_uses; + + if (data.inviter) { + /** + * The user who created this invite + * @type {User} + */ + this.inviter = this.client.dataManager.newUser(data.inviter); + } + + /** + * The channel the invite is for. If this channel is already known, this will be a GuildChannel object. + * If the channel is unknown, this will be a PartialGuildChannel object. + * @type {GuildChannel|PartialGuildChannel} + */ + this.channel = this.client.channels.get(data.channel.id) || new PartialGuildChannel(this.client, data.channel); + + /** + * The timestamp the invite was created at + * @type {number} + */ + this.createdTimestamp = new Date(data.created_at).getTime(); + } + + /** + * The time the invite was created + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The timestamp the invite will expire at + * @type {number} + * @readonly + */ + get expiresTimestamp() { + return this.createdTimestamp + (this.maxAge * 1000); + } + + /** + * The time the invite will expire + * @type {Date} + * @readonly + */ + get expiresAt() { + return new Date(this.expiresTimestamp); + } + + /** + * The URL to the invite + * @type {string} + * @readonly + */ + get url() { + return Constants.Endpoints.inviteLink(this.code); + } + + /** + * Deletes this invite + * @returns {Promise} + */ + delete() { + return this.client.rest.methods.deleteInvite(this); + } + + /** + * When concatenated with a string, this automatically concatenates the invite's URL instead of the object. + * @returns {string} + * @example + * // logs: Invite: https://discord.gg/A1b2C3 + * console.log(`Invite: ${invite}`); + */ + toString() { + return this.url; + } +} + +module.exports = Invite; diff --git a/node_modules/discord.js/src/structures/Message.js b/node_modules/discord.js/src/structures/Message.js new file mode 100644 index 0000000..7fcc5b4 --- /dev/null +++ b/node_modules/discord.js/src/structures/Message.js @@ -0,0 +1,568 @@ +const Attachment = require('./MessageAttachment'); +const Embed = require('./MessageEmbed'); +const MessageReaction = require('./MessageReaction'); +const Collection = require('../util/Collection'); +const Constants = require('../util/Constants'); +const escapeMarkdown = require('../util/EscapeMarkdown'); + +// Done purely for GuildMember, which would cause a bad circular dependency +const Discord = require('..'); + +/** + * Represents a message on Discord + */ +class Message { + constructor(channel, data, client) { + /** + * The Client that instantiated the Message + * @name Message#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + /** + * The channel that the message was sent in + * @type {TextChannel|DMChannel|GroupDMChannel} + */ + this.channel = channel; + + if (data) this.setup(data); + } + + setup(data) { // eslint-disable-line complexity + /** + * The ID of the message (unique in the channel it was sent) + * @type {string} + */ + this.id = data.id; + + /** + * The type of the message + * @type {string} + */ + this.type = Constants.MessageTypes[data.type]; + + /** + * The content of the message + * @type {string} + */ + this.content = data.content; + + /** + * The author of the message + * @type {User} + */ + this.author = this.client.dataManager.newUser(data.author); + + /** + * Represents the author of the message as a guild member. Only available if the message comes from a guild + * where the author is still a member. + * @type {GuildMember} + */ + this.member = this.guild ? this.guild.member(this.author) || null : null; + + /** + * Whether or not this message is pinned + * @type {boolean} + */ + this.pinned = data.pinned; + + /** + * Whether or not the message was Text-To-Speech + * @type {boolean} + */ + this.tts = data.tts; + + /** + * A random number used for checking message delivery + * @type {string} + */ + this.nonce = data.nonce; + + /** + * Whether or not this message was sent by Discord, not actually a user (e.g. pin notifications) + * @type {boolean} + */ + this.system = data.type === 6; + + /** + * A list of embeds in the message - e.g. YouTube Player + * @type {MessageEmbed[]} + */ + this.embeds = data.embeds.map(e => new Embed(this, e)); + + /** + * A collection of attachments in the message - e.g. Pictures - mapped by their ID. + * @type {Collection} + */ + this.attachments = new Collection(); + for (const attachment of data.attachments) this.attachments.set(attachment.id, new Attachment(this, attachment)); + + /** + * The timestamp the message was sent at + * @type {number} + */ + this.createdTimestamp = new Date(data.timestamp).getTime(); + + /** + * The timestamp the message was last edited at (if applicable) + * @type {?number} + */ + this.editedTimestamp = data.edited_timestamp ? new Date(data.edited_timestamp).getTime() : null; + + /** + * An object containing a further users, roles or channels collections + * @type {Object} + * @property {Collection} mentions.users Mentioned users, maps their ID to the user object. + * @property {Collection} mentions.roles Mentioned roles, maps their ID to the role object. + * @property {Collection} mentions.channels Mentioned channels, + * maps their ID to the channel object. + * @property {boolean} mentions.everyone Whether or not @everyone was mentioned. + */ + this.mentions = { + users: new Collection(), + roles: new Collection(), + channels: new Collection(), + everyone: data.mention_everyone, + }; + + for (const mention of data.mentions) { + let user = this.client.users.get(mention.id); + if (user) { + this.mentions.users.set(user.id, user); + } else { + user = this.client.dataManager.newUser(mention); + this.mentions.users.set(user.id, user); + } + } + + if (data.mention_roles) { + for (const mention of data.mention_roles) { + const role = this.channel.guild.roles.get(mention); + if (role) this.mentions.roles.set(role.id, role); + } + } + + if (this.channel.guild) { + const channMentionsRaw = data.content.match(/<#([0-9]{14,20})>/g) || []; + for (const raw of channMentionsRaw) { + const chan = this.channel.guild.channels.get(raw.match(/([0-9]{14,20})/g)[0]); + if (chan) this.mentions.channels.set(chan.id, chan); + } + } + + this._edits = []; + + /** + * A collection of reactions to this message, mapped by the reaction "id". + * @type {Collection} + */ + this.reactions = new Collection(); + + if (data.reactions && data.reactions.length > 0) { + for (const reaction of data.reactions) { + const id = reaction.emoji.id ? `${reaction.emoji.name}:${reaction.emoji.id}` : reaction.emoji.name; + this.reactions.set(id, new MessageReaction(this, reaction.emoji, reaction.count, reaction.me)); + } + } + + /** + * ID of the webhook that sent the message, if applicable + * @type {?string} + */ + this.webhookID = data.webhook_id || null; + } + + patch(data) { // eslint-disable-line complexity + if (data.author) { + this.author = this.client.users.get(data.author.id); + if (this.guild) this.member = this.guild.member(this.author); + } + if (data.content) this.content = data.content; + if (data.timestamp) this.createdTimestamp = new Date(data.timestamp).getTime(); + if (data.edited_timestamp) { + this.editedTimestamp = data.edited_timestamp ? new Date(data.edited_timestamp).getTime() : null; + } + if ('tts' in data) this.tts = data.tts; + if ('mention_everyone' in data) this.mentions.everyone = data.mention_everyone; + if (data.nonce) this.nonce = data.nonce; + if (data.embeds) this.embeds = data.embeds.map(e => new Embed(this, e)); + if (data.type > -1) { + this.system = false; + if (data.type === 6) this.system = true; + } + if (data.attachments) { + this.attachments = new Collection(); + for (const attachment of data.attachments) { + this.attachments.set(attachment.id, new Attachment(this, attachment)); + } + } + if (data.mentions) { + for (const mention of data.mentions) { + let user = this.client.users.get(mention.id); + if (user) { + this.mentions.users.set(user.id, user); + } else { + user = this.client.dataManager.newUser(mention); + this.mentions.users.set(user.id, user); + } + } + } + if (data.mention_roles) { + for (const mention of data.mention_roles) { + const role = this.channel.guild.roles.get(mention); + if (role) this.mentions.roles.set(role.id, role); + } + } + if (data.id) this.id = data.id; + if (this.channel.guild && data.content) { + const channMentionsRaw = data.content.match(/<#([0-9]{14,20})>/g) || []; + for (const raw of channMentionsRaw) { + const chan = this.channel.guild.channels.get(raw.match(/([0-9]{14,20})/g)[0]); + if (chan) this.mentions.channels.set(chan.id, chan); + } + } + if (data.reactions) { + this.reactions = new Collection(); + if (data.reactions.length > 0) { + for (const reaction of data.reactions) { + const id = reaction.emoji.id ? `${reaction.emoji.name}:${reaction.emoji.id}` : reaction.emoji.name; + this.reactions.set(id, new MessageReaction(this, data.emoji, data.count, data.me)); + } + } + } + } + + /** + * The time the message was sent + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The time the message was last edited at (if applicable) + * @type {?Date} + * @readonly + */ + get editedAt() { + return this.editedTimestamp ? new Date(this.editedTimestamp) : null; + } + + /** + * The guild the message was sent in (if in a guild channel) + * @type {?Guild} + * @readonly + */ + get guild() { + return this.channel.guild || null; + } + + /** + * The message contents with all mentions replaced by the equivalent text. If mentions cannot be resolved to a name, + * the relevant mention in the message content will not be converted. + * @type {string} + * @readonly + */ + get cleanContent() { + return this.content + .replace(/@(everyone|here)/g, '@\u200b$1') + .replace(/<@!?[0-9]+>/g, (input) => { + const id = input.replace(/<|!|>|@/g, ''); + if (this.channel.type === 'dm' || this.channel.type === 'group') { + return this.client.users.has(id) ? `@${this.client.users.get(id).username}` : input; + } + + const member = this.channel.guild.members.get(id); + if (member) { + if (member.nickname) return `@${member.nickname}`; + return `@${member.user.username}`; + } else { + const user = this.client.users.get(id); + if (user) return `@${user.username}`; + return input; + } + }) + .replace(/<#[0-9]+>/g, (input) => { + const channel = this.client.channels.get(input.replace(/<|#|>/g, '')); + if (channel) return `#${channel.name}`; + return input; + }) + .replace(/<@&[0-9]+>/g, (input) => { + if (this.channel.type === 'dm' || this.channel.type === 'group') return input; + const role = this.guild.roles.get(input.replace(/<|@|>|&/g, '')); + if (role) return `@${role.name}`; + return input; + }); + } + + /** + * An array of cached versions of the message, including the current version. + * Sorted from latest (first) to oldest (last). + * @type {Message[]} + * @readonly + */ + get edits() { + const copy = this._edits.slice(); + copy.unshift(this); + return copy; + } + + /** + * Whether the message is editable by the client user. + * @type {boolean} + * @readonly + */ + get editable() { + return this.author.id === this.client.user.id; + } + + /** + * Whether the message is deletable by the client user. + * @type {boolean} + * @readonly + */ + get deletable() { + return this.author.id === this.client.user.id || (this.guild && + this.channel.permissionsFor(this.client.user).hasPermission(Constants.PermissionFlags.MANAGE_MESSAGES) + ); + } + + /** + * Whether the message is pinnable by the client user. + * @type {boolean} + * @readonly + */ + get pinnable() { + return !this.guild || + this.channel.permissionsFor(this.client.user).hasPermission(Constants.PermissionFlags.MANAGE_MESSAGES); + } + + /** + * Whether or not a user, channel or role is mentioned in this message. + * @param {GuildChannel|User|Role|string} data either a guild channel, user or a role object, or a string representing + * the ID of any of these. + * @returns {boolean} + */ + isMentioned(data) { + data = data && data.id ? data.id : data; + return this.mentions.users.has(data) || this.mentions.channels.has(data) || this.mentions.roles.has(data); + } + + /** + * Whether or not a guild member is mentioned in this message. Takes into account + * user mentions, role mentions, and @everyone/@here mentions. + * @param {GuildMember|User} member Member/user to check for a mention of + * @returns {boolean} + */ + isMemberMentioned(member) { + if (this.mentions.everyone) return true; + if (this.mentions.users.has(member.id)) return true; + if (member instanceof Discord.GuildMember && member.roles.some(r => this.mentions.roles.has(r.id))) return true; + return false; + } + + /** + * Options that can be passed into editMessage + * @typedef {Object} MessageEditOptions + * @property {Object} [embed] An embed to be added/edited + * @property {string|boolean} [code] Language for optional codeblock formatting to apply + */ + + /** + * Edit the content of the message + * @param {StringResolvable} [content] The new content for the message + * @param {MessageEditOptions} [options] The options to provide + * @returns {Promise} + * @example + * // update the content of a message + * message.edit('This is my new content!') + * .then(msg => console.log(`Updated the content of a message from ${msg.author}`)) + * .catch(console.error); + */ + edit(content, options) { + if (!options && typeof content === 'object') { + options = content; + content = ''; + } else if (!options) { + options = {}; + } + return this.client.rest.methods.updateMessage(this, content, options); + } + + /** + * Edit the content of the message, with a code block + * @param {string} lang Language for the code block + * @param {StringResolvable} content The new content for the message + * @returns {Promise} + */ + editCode(lang, content) { + content = escapeMarkdown(this.client.resolver.resolveString(content), true); + return this.edit(`\`\`\`${lang || ''}\n${content}\n\`\`\``); + } + + /** + * Pins this message to the channel's pinned messages + * @returns {Promise} + */ + pin() { + return this.client.rest.methods.pinMessage(this); + } + + /** + * Unpins this message from the channel's pinned messages + * @returns {Promise} + */ + unpin() { + return this.client.rest.methods.unpinMessage(this); + } + + /** + * Add a reaction to the message + * @param {string|Emoji|ReactionEmoji} emoji Emoji to react with + * @returns {Promise} + */ + react(emoji) { + emoji = this.client.resolver.resolveEmojiIdentifier(emoji); + if (!emoji) throw new TypeError('Emoji must be a string or Emoji/ReactionEmoji'); + + return this.client.rest.methods.addMessageReaction(this, emoji); + } + + /** + * Remove all reactions from a message + * @returns {Promise} + */ + clearReactions() { + return this.client.rest.methods.removeMessageReactions(this); + } + + /** + * Deletes the message + * @param {number} [timeout=0] How long to wait to delete the message in milliseconds + * @returns {Promise} + * @example + * // delete a message + * message.delete() + * .then(msg => console.log(`Deleted message from ${msg.author}`)) + * .catch(console.error); + */ + delete(timeout = 0) { + if (timeout <= 0) { + return this.client.rest.methods.deleteMessage(this); + } else { + return new Promise(resolve => { + this.client.setTimeout(() => { + resolve(this.delete()); + }, timeout); + }); + } + } + + /** + * Reply to the message + * @param {StringResolvable} content The content for the message + * @param {MessageOptions} [options = {}] The options to provide + * @returns {Promise} + * @example + * // reply to a message + * message.reply('Hey, I\'m a reply!') + * .then(msg => console.log(`Sent a reply to ${msg.author}`)) + * .catch(console.error); + */ + reply(content, options = {}) { + content = `${this.guild || this.channel.type === 'group' ? `${this.author}, ` : ''}${content}`; + return this.channel.send(content, options); + } + + /** + * Fetches the webhook used to create this message. + * @returns {Promise} + */ + fetchWebhook() { + if (!this.webhookID) return Promise.reject(new Error('The message was not sent by a webhook.')); + return this.client.fetchWebhook(this.webhookID); + } + + /** + * Used mainly internally. Whether two messages are identical in properties. If you want to compare messages + * without checking all the properties, use `message.id === message2.id`, which is much more efficient. This + * method allows you to see if there are differences in content, embeds, attachments, nonce and tts properties. + * @param {Message} message The message to compare it to + * @param {Object} rawData Raw data passed through the WebSocket about this message + * @returns {boolean} + */ + equals(message, rawData) { + if (!message) return false; + const embedUpdate = !message.author && !message.attachments; + if (embedUpdate) return this.id === message.id && this.embeds.length === message.embeds.length; + + let equal = this.id === message.id && + this.author.id === message.author.id && + this.content === message.content && + this.tts === message.tts && + this.nonce === message.nonce && + this.embeds.length === message.embeds.length && + this.attachments.length === message.attachments.length; + + if (equal && rawData) { + equal = this.mentions.everyone === message.mentions.everyone && + this.createdTimestamp === new Date(rawData.timestamp).getTime() && + this.editedTimestamp === new Date(rawData.edited_timestamp).getTime(); + } + + return equal; + } + + /** + * When concatenated with a string, this automatically concatenates the message's content instead of the object. + * @returns {string} + * @example + * // logs: Message: This is a message! + * console.log(`Message: ${message}`); + */ + toString() { + return this.content; + } + + _addReaction(emoji, user) { + const emojiID = emoji.id ? `${emoji.name}:${emoji.id}` : emoji.name; + let reaction; + if (this.reactions.has(emojiID)) { + reaction = this.reactions.get(emojiID); + if (!reaction.me) reaction.me = user.id === this.client.user.id; + } else { + reaction = new MessageReaction(this, emoji, 0, user.id === this.client.user.id); + this.reactions.set(emojiID, reaction); + } + if (!reaction.users.has(user.id)) { + reaction.users.set(user.id, user); + reaction.count++; + return reaction; + } + return null; + } + + _removeReaction(emoji, user) { + const emojiID = emoji.id || emoji; + if (this.reactions.has(emojiID)) { + const reaction = this.reactions.get(emojiID); + if (reaction.users.has(user.id)) { + reaction.users.delete(user.id); + reaction.count--; + if (user.id === this.client.user.id) reaction.me = false; + return reaction; + } + } + return null; + } + + _clearReactions() { + this.reactions.clear(); + } +} + +module.exports = Message; diff --git a/node_modules/discord.js/src/structures/MessageAttachment.js b/node_modules/discord.js/src/structures/MessageAttachment.js new file mode 100644 index 0000000..29dfb52 --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageAttachment.js @@ -0,0 +1,68 @@ +/** + * Represents an attachment in a message + */ +class MessageAttachment { + constructor(message, data) { + /** + * The Client that instantiated this MessageAttachment. + * @name MessageAttachment#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: message.client }); + + /** + * The message this attachment is part of. + * @type {Message} + */ + this.message = message; + + this.setup(data); + } + + setup(data) { + /** + * The ID of this attachment + * @type {string} + */ + this.id = data.id; + + /** + * The file name of this attachment + * @type {string} + */ + this.filename = data.filename; + + /** + * The size of this attachment in bytes + * @type {number} + */ + this.filesize = data.size; + + /** + * The URL to this attachment + * @type {string} + */ + this.url = data.url; + + /** + * The Proxy URL to this attachment + * @type {string} + */ + this.proxyURL = data.proxy_url; + + /** + * The height of this attachment (if an image) + * @type {?number} + */ + this.height = data.height; + + /** + * The width of this attachment (if an image) + * @type {?number} + */ + this.width = data.width; + } +} + +module.exports = MessageAttachment; diff --git a/node_modules/discord.js/src/structures/MessageCollector.js b/node_modules/discord.js/src/structures/MessageCollector.js new file mode 100644 index 0000000..f84ecbd --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageCollector.js @@ -0,0 +1,151 @@ +const EventEmitter = require('events').EventEmitter; +const Collection = require('../util/Collection'); + +/** + * Collects messages based on a specified filter, then emits them. + * @extends {EventEmitter} + */ +class MessageCollector extends EventEmitter { + /** + * A function that takes a Message object and a MessageCollector and returns a boolean. + * ```js + * function(message, collector) { + * if (message.content.includes('discord')) { + * return true; // passed the filter test + * } + * return false; // failed the filter test + * } + * ``` + * @typedef {Function} CollectorFilterFunction + */ + + /** + * An object containing options used to configure a MessageCollector. All properties are optional. + * @typedef {Object} CollectorOptions + * @property {number} [time] Duration for the collector in milliseconds + * @property {number} [max] Maximum number of messages to handle + * @property {number} [maxMatches] Maximum number of successfully filtered messages to obtain + */ + + /** + * @param {Channel} channel The channel to collect messages in + * @param {CollectorFilterFunction} filter The filter function + * @param {CollectorOptions} [options] Options for the collector + */ + constructor(channel, filter, options = {}) { + super(); + + /** + * The channel this collector is operating on + * @type {Channel} + */ + this.channel = channel; + + /** + * A function used to filter messages that the collector collects. + * @type {CollectorFilterFunction} + */ + this.filter = filter; + + /** + * Options for the collecor. + * @type {CollectorOptions} + */ + this.options = options; + + /** + * Whether this collector has stopped collecting messages. + * @type {boolean} + */ + this.ended = false; + + /** + * A collection of collected messages, mapped by message ID. + * @type {Collection} + */ + this.collected = new Collection(); + + this.listener = message => this.verify(message); + this.channel.client.on('message', this.listener); + if (options.time) this.channel.client.setTimeout(() => this.stop('time'), options.time); + } + + /** + * Verifies a message against the filter and options + * @private + * @param {Message} message The message + * @returns {boolean} + */ + verify(message) { + if (this.channel ? this.channel.id !== message.channel.id : false) return false; + if (this.filter(message, this)) { + this.collected.set(message.id, message); + /** + * Emitted whenever the collector receives a message that passes the filter test. + * @param {Message} message The received message + * @param {MessageCollector} collector The collector the message passed through + * @event MessageCollector#message + */ + this.emit('message', message, this); + if (this.collected.size >= this.options.maxMatches) this.stop('matchesLimit'); + else if (this.options.max && this.collected.size === this.options.max) this.stop('limit'); + return true; + } + return false; + } + + /** + * Returns a promise that resolves when a valid message is sent. Rejects + * with collected messages if the Collector ends before receiving a message. + * @type {Promise} + * @readonly + */ + get next() { + return new Promise((resolve, reject) => { + if (this.ended) { + reject(this.collected); + return; + } + + const cleanup = () => { + this.removeListener('message', onMessage); + this.removeListener('end', onEnd); + }; + + const onMessage = (...args) => { + cleanup(); + resolve(...args); + }; + + const onEnd = (...args) => { + cleanup(); + reject(...args); + }; + + this.once('message', onMessage); + this.once('end', onEnd); + }); + } + + /** + * Stops the collector and emits `end`. + * @param {string} [reason='user'] An optional reason for stopping the collector + */ + stop(reason = 'user') { + if (this.ended) return; + this.ended = true; + this.channel.client.removeListener('message', this.listener); + /** + * Emitted when the Collector stops collecting. + * @param {Collection} collection A collection of messages collected + * during the lifetime of the collector, mapped by the ID of the messages. + * @param {string} reason The reason for the end of the collector. If it ended because it reached the specified time + * limit, this would be `time`. If you invoke `.stop()` without specifying a reason, this would be `user`. If it + * ended because it reached its message limit, it will be `limit`. + * @event MessageCollector#end + */ + this.emit('end', this.collected, reason); + } +} + +module.exports = MessageCollector; diff --git a/node_modules/discord.js/src/structures/MessageEmbed.js b/node_modules/discord.js/src/structures/MessageEmbed.js new file mode 100644 index 0000000..1249c42 --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageEmbed.js @@ -0,0 +1,293 @@ +/** + * Represents an embed in a message (image/video preview, rich embed, etc.) + */ +class MessageEmbed { + constructor(message, data) { + /** + * The client that instantiated this embed + * @name MessageEmbed#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: message.client }); + + /** + * The message this embed is part of + * @type {Message} + */ + this.message = message; + + this.setup(data); + } + + setup(data) { + /** + * The type of this embed + * @type {string} + */ + this.type = data.type; + + /** + * The title of this embed, if there is one + * @type {?string} + */ + this.title = data.title; + + /** + * The description of this embed, if there is one + * @type {?string} + */ + this.description = data.description; + + /** + * The URL of this embed + * @type {string} + */ + this.url = data.url; + + /** + * The color of the embed + * @type {number} + */ + this.color = data.color; + + /** + * The fields of this embed + * @type {MessageEmbedField[]} + */ + this.fields = []; + if (data.fields) for (const field of data.fields) this.fields.push(new MessageEmbedField(this, field)); + + /** + * The timestamp of this embed + * @type {number} + */ + this.createdTimestamp = data.timestamp; + + /** + * The thumbnail of this embed, if there is one + * @type {MessageEmbedThumbnail} + */ + this.thumbnail = data.thumbnail ? new MessageEmbedThumbnail(this, data.thumbnail) : null; + + /** + * The author of this embed, if there is one + * @type {MessageEmbedAuthor} + */ + this.author = data.author ? new MessageEmbedAuthor(this, data.author) : null; + + /** + * The provider of this embed, if there is one + * @type {MessageEmbedProvider} + */ + this.provider = data.provider ? new MessageEmbedProvider(this, data.provider) : null; + + /** + * The footer of this embed + * @type {MessageEmbedFooter} + */ + this.footer = data.footer ? new MessageEmbedFooter(this, data.footer) : null; + } + + /** + * The date this embed was created + * @type {Date} + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The hexadecimal version of the embed color, with a leading hash. + * @type {string} + * @readonly + */ + get hexColor() { + let col = this.color.toString(16); + while (col.length < 6) col = `0${col}`; + return `#${col}`; + } +} + +/** + * Represents a thumbnail for a message embed + */ +class MessageEmbedThumbnail { + constructor(embed, data) { + /** + * The embed this thumbnail is part of + * @type {MessageEmbed} + */ + this.embed = embed; + + this.setup(data); + } + + setup(data) { + /** + * The URL for this thumbnail + * @type {string} + */ + this.url = data.url; + + /** + * The Proxy URL for this thumbnail + * @type {string} + */ + this.proxyURL = data.proxy_url; + + /** + * The height of the thumbnail + * @type {number} + */ + this.height = data.height; + + /** + * The width of the thumbnail + * @type {number} + */ + this.width = data.width; + } +} + +/** + * Represents a provider for a message embed + */ +class MessageEmbedProvider { + constructor(embed, data) { + /** + * The embed this provider is part of + * @type {MessageEmbed} + */ + this.embed = embed; + + this.setup(data); + } + + setup(data) { + /** + * The name of this provider + * @type {string} + */ + this.name = data.name; + + /** + * The URL of this provider + * @type {string} + */ + this.url = data.url; + } +} + +/** + * Represents an author for a message embed + */ +class MessageEmbedAuthor { + constructor(embed, data) { + /** + * The embed this author is part of + * @type {MessageEmbed} + */ + this.embed = embed; + + this.setup(data); + } + + setup(data) { + /** + * The name of this author + * @type {string} + */ + this.name = data.name; + + /** + * The URL of this author + * @type {string} + */ + this.url = data.url; + + /** + * The icon URL of this author + * @type {string} + */ + this.iconURL = data.icon_url; + } +} + +/** + * Represents a field for a message embed + */ +class MessageEmbedField { + constructor(embed, data) { + /** + * The embed this footer is part of + * @type {MessageEmbed} + */ + this.embed = embed; + + this.setup(data); + } + + setup(data) { + /** + * The name of this field + * @type {string} + */ + this.name = data.name; + + /** + * The value of this field + * @type {string} + */ + this.value = data.value; + + /** + * If this field is displayed inline + * @type {boolean} + */ + this.inline = data.inline; + } +} + +/** + * Represents the footer of a message embed + */ +class MessageEmbedFooter { + constructor(embed, data) { + /** + * The embed this footer is part of + * @type {MessageEmbed} + */ + this.embed = embed; + + this.setup(data); + } + + setup(data) { + /** + * The text in this footer + * @type {string} + */ + this.text = data.text; + + /** + * The icon URL of this footer + * @type {string} + */ + this.iconURL = data.icon_url; + + /** + * The proxy icon URL of this footer + * @type {string} + */ + this.proxyIconUrl = data.proxy_icon_url; + } +} + +MessageEmbed.Thumbnail = MessageEmbedThumbnail; +MessageEmbed.Provider = MessageEmbedProvider; +MessageEmbed.Author = MessageEmbedAuthor; +MessageEmbed.Field = MessageEmbedField; +MessageEmbed.Footer = MessageEmbedFooter; + +module.exports = MessageEmbed; diff --git a/node_modules/discord.js/src/structures/MessageReaction.js b/node_modules/discord.js/src/structures/MessageReaction.js new file mode 100644 index 0000000..30c555f --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageReaction.js @@ -0,0 +1,92 @@ +const Collection = require('../util/Collection'); +const Emoji = require('./Emoji'); +const ReactionEmoji = require('./ReactionEmoji'); + +/** + * Represents a reaction to a message + */ +class MessageReaction { + constructor(message, emoji, count, me) { + /** + * The message that this reaction refers to + * @type {Message} + */ + this.message = message; + + /** + * Whether the client has given this reaction + * @type {boolean} + */ + this.me = me; + + /** + * The number of people that have given the same reaction. + * @type {number} + */ + this.count = count || 0; + + /** + * The users that have given this reaction, mapped by their ID. + * @type {Collection} + */ + this.users = new Collection(); + + this._emoji = new ReactionEmoji(this, emoji.name, emoji.id); + } + + /** + * The emoji of this reaction, either an Emoji 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} + */ + get emoji() { + if (this._emoji instanceof Emoji) 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; + if (emojis.has(this._emoji.id)) { + const emoji = emojis.get(this._emoji.id); + this._emoji = emoji; + return emoji; + } + } + return this._emoji; + } + + /** + * Removes a user from this reaction. + * @param {UserResolvable} [user=this.message.client.user] User to remove the reaction of + * @returns {Promise} + */ + remove(user = this.message.client.user) { + const message = this.message; + user = this.message.client.resolver.resolveUserID(user); + if (!user) return Promise.reject('Couldn\'t resolve the user ID to remove from the reaction.'); + return message.client.rest.methods.removeMessageReaction( + message, this.emoji.identifier, user + ); + } + + /** + * Fetch all the users that gave this reaction. Resolves with a collection of users, mapped by their IDs. + * @param {number} [limit=100] the maximum amount of users to fetch, defaults to 100 + * @returns {Promise>} + */ + fetchUsers(limit = 100) { + const message = this.message; + return message.client.rest.methods.getMessageReactionUsers( + message, this.emoji.identifier, limit + ).then(users => { + this.users = new Collection(); + for (const rawUser of users) { + const user = this.message.client.dataManager.newUser(rawUser); + this.users.set(user.id, user); + } + this.count = this.users.size; + return users; + }); + } +} + +module.exports = MessageReaction; diff --git a/node_modules/discord.js/src/structures/OAuth2Application.js b/node_modules/discord.js/src/structures/OAuth2Application.js new file mode 100644 index 0000000..b7c7285 --- /dev/null +++ b/node_modules/discord.js/src/structures/OAuth2Application.js @@ -0,0 +1,82 @@ +/** + * Represents an OAuth2 Application + */ +class OAuth2Application { + constructor(client, data) { + /** + * The client that instantiated the application + * @name OAuth2Application#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + this.setup(data); + } + + setup(data) { + /** + * The ID of the app + * @type {string} + */ + this.id = data.id; + + /** + * The name of the app + * @type {string} + */ + this.name = data.name; + + /** + * The app's description + * @type {string} + */ + this.description = data.description; + + /** + * The app's icon hash + * @type {string} + */ + this.icon = data.icon; + + /** + * The app's icon URL + * @type {string} + */ + this.iconURL = `https://cdn.discordapp.com/app-icons/${this.id}/${this.icon}.jpg`; + + /** + * The app's RPC origins + * @type {Array} + */ + this.rpcOrigins = data.rpc_origins; + } + + /** + * The timestamp the app was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return (this.id / 4194304) + 1420070400000; + } + + /** + * The time the app was created + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * When concatenated with a string, this automatically concatenates the app name rather than the app object. + * @returns {string} + */ + toString() { + return this.name; + } +} + +module.exports = OAuth2Application; diff --git a/node_modules/discord.js/src/structures/PartialGuild.js b/node_modules/discord.js/src/structures/PartialGuild.js new file mode 100644 index 0000000..407212e --- /dev/null +++ b/node_modules/discord.js/src/structures/PartialGuild.js @@ -0,0 +1,51 @@ +/* +{ splash: null, + id: '123123123', + icon: '123123123', + name: 'name' } +*/ + +/** + * Represents a guild that the client only has limited information for - e.g. from invites. + */ +class PartialGuild { + constructor(client, data) { + /** + * The Client that instantiated this PartialGuild + * @name PartialGuild#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + this.setup(data); + } + + setup(data) { + /** + * The ID of this guild + * @type {string} + */ + this.id = data.id; + + /** + * The name of this guild + * @type {string} + */ + this.name = data.name; + + /** + * The hash of this guild's icon, or null if there is none. + * @type {?string} + */ + this.icon = data.icon; + + /** + * The hash of the guild splash image, or null if no splash (VIP only) + * @type {?string} + */ + this.splash = data.splash; + } +} + +module.exports = PartialGuild; diff --git a/node_modules/discord.js/src/structures/PartialGuildChannel.js b/node_modules/discord.js/src/structures/PartialGuildChannel.js new file mode 100644 index 0000000..e58a6bb --- /dev/null +++ b/node_modules/discord.js/src/structures/PartialGuildChannel.js @@ -0,0 +1,44 @@ +const Constants = require('../util/Constants'); + +/* +{ type: 0, id: '123123', name: 'heavy-testing' } } +*/ + +/** + * Represents a guild channel that the client only has limited information for - e.g. from invites. + */ +class PartialGuildChannel { + constructor(client, data) { + /** + * The Client that instantiated this PartialGuildChannel + * @name PartialGuildChannel#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + this.setup(data); + } + + setup(data) { + /** + * The ID of this guild channel + * @type {string} + */ + this.id = data.id; + + /** + * The name of this guild channel + * @type {string} + */ + this.name = data.name; + + /** + * The type of this guild channel - `text` or `voice` + * @type {string} + */ + this.type = Constants.ChannelTypes.text === data.type ? 'text' : 'voice'; + } +} + +module.exports = PartialGuildChannel; diff --git a/node_modules/discord.js/src/structures/PermissionOverwrites.js b/node_modules/discord.js/src/structures/PermissionOverwrites.js new file mode 100644 index 0000000..9b2f536 --- /dev/null +++ b/node_modules/discord.js/src/structures/PermissionOverwrites.js @@ -0,0 +1,43 @@ +/** + * Represents a permission overwrite for a role or member in a guild channel. + */ +class PermissionOverwrites { + constructor(guildChannel, data) { + /** + * The GuildChannel this overwrite is for + * @name PermissionOverwrites#channel + * @type {GuildChannel} + * @readonly + */ + Object.defineProperty(this, 'channel', { value: guildChannel }); + + if (data) this.setup(data); + } + + setup(data) { + /** + * The ID of this overwrite, either a user ID or a role ID + * @type {string} + */ + this.id = data.id; + + /** + * The type of this overwrite + * @type {string} + */ + this.type = data.type; + + this.deny = data.deny; + this.allow = data.allow; + } + + /** + * Delete this Permission Overwrite. + * @returns {Promise} + */ + delete() { + return this.channel.client.rest.methods.deletePermissionOverwrites(this); + } +} + +module.exports = PermissionOverwrites; diff --git a/node_modules/discord.js/src/structures/Presence.js b/node_modules/discord.js/src/structures/Presence.js new file mode 100644 index 0000000..ddca5fb --- /dev/null +++ b/node_modules/discord.js/src/structures/Presence.js @@ -0,0 +1,92 @@ +/** + * Represents a user's presence + */ +class Presence { + constructor(data = {}) { + /** + * The status of the presence: + * + * * **`online`** - user is online + * * **`offline`** - user is offline or invisible + * * **`idle`** - user is AFK + * * **`dnd`** - user is in Do not Disturb + * @type {string} + */ + this.status = data.status || 'offline'; + + /** + * The game that the user is playing, `null` if they aren't playing a game. + * @type {?Game} + */ + this.game = data.game ? new Game(data.game) : null; + } + + update(data) { + this.status = data.status || this.status; + this.game = data.game ? new Game(data.game) : null; + } + + /** + * Whether this presence is equal to another + * @param {Presence} presence Presence to compare with + * @returns {boolean} + */ + equals(presence) { + return this === presence || ( + presence && + this.status === presence.status && + this.game ? this.game.equals(presence.game) : !presence.game + ); + } +} + +/** + * Represents a game that is part of a user's presence. + */ +class Game { + constructor(data) { + /** + * The name of the game being played + * @type {string} + */ + this.name = data.name; + + /** + * The type of the game status + * @type {number} + */ + this.type = data.type; + + /** + * If the game is being streamed, a link to the stream + * @type {?string} + */ + this.url = data.url || null; + } + + /** + * Whether or not the game is being streamed + * @type {boolean} + * @readonly + */ + get streaming() { + return this.type === 1; + } + + /** + * Whether this game is equal to another game + * @param {Game} game Game to compare with + * @returns {boolean} + */ + equals(game) { + return this === game || ( + game && + this.name === game.name && + this.type === game.type && + this.url === game.url + ); + } +} + +exports.Presence = Presence; +exports.Game = Game; diff --git a/node_modules/discord.js/src/structures/ReactionEmoji.js b/node_modules/discord.js/src/structures/ReactionEmoji.js new file mode 100644 index 0000000..b6d2cdb --- /dev/null +++ b/node_modules/discord.js/src/structures/ReactionEmoji.js @@ -0,0 +1,49 @@ +/** + * 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. + */ +class ReactionEmoji { + constructor(reaction, name, id) { + /** + * 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 {string} + */ + this.id = id; + } + + /** + * The identifier of this emoji, used for message reactions + * @readonly + * @type {string} + */ + get identifier() { + if (this.id) return `${this.name}:${this.id}`; + return encodeURIComponent(this.name); + } + + /** + * Creates the text required to form a graphical emoji on Discord. + * @example + * // send the emoji used in a reaction to the channel the reaction is part of + * reaction.message.channel.sendMessage(`The emoji used is ${reaction.emoji}`); + * @returns {string} + */ + toString() { + return this.id ? `<:${this.name}:${this.id}>` : this.name; + } +} + +module.exports = ReactionEmoji; diff --git a/node_modules/discord.js/src/structures/RichEmbed.js b/node_modules/discord.js/src/structures/RichEmbed.js new file mode 100644 index 0000000..fbd9383 --- /dev/null +++ b/node_modules/discord.js/src/structures/RichEmbed.js @@ -0,0 +1,204 @@ +/** + * A rich embed to be sent with a message + * @param {Object} [data] Data to set in the rich embed + */ +class RichEmbed { + constructor(data = {}) { + /** + * Title for this Embed + * @type {string} + */ + this.title = data.title; + + /** + * Description for this Embed + * @type {string} + */ + this.description = data.description; + + /** + * URL for this Embed + * @type {string} + */ + this.url = data.url; + + /** + * Color for this Embed + * @type {number} + */ + this.color = data.color; + + /** + * Author for this Embed + * @type {Object} + */ + this.author = data.author; + + /** + * Timestamp for this Embed + * @type {Date} + */ + this.timestamp = data.timestamp; + + /** + * Fields for this Embed + * @type {Object[]} + */ + this.fields = data.fields || []; + + /** + * Thumbnail for this Embed + * @type {Object} + */ + this.thumbnail = data.thumbnail; + + /** + * Image for this Embed + * @type {Object} + */ + this.image = data.image; + + /** + * Footer for this Embed + * @type {Object} + */ + this.footer = data.footer; + } + + /** + * Sets the title of this embed + * @param {StringResolvable} title The title + * @returns {RichEmbed} This embed + */ + setTitle(title) { + title = resolveString(title); + if (title.length > 256) throw new RangeError('RichEmbed titles may not exceed 256 characters.'); + this.title = title; + return this; + } + + /** + * Sets the description of this embed + * @param {StringResolvable} description The description + * @returns {RichEmbed} This embed + */ + setDescription(description) { + description = resolveString(description); + if (description.length > 2048) throw new RangeError('RichEmbed descriptions may not exceed 2048 characters.'); + this.description = description; + return this; + } + + /** + * Sets the URL of this embed + * @param {string} url The URL + * @returns {RichEmbed} This embed + */ + setURL(url) { + this.url = url; + return this; + } + + /** + * Sets the color of this embed + * @param {string|number|number[]} color The color to set + * @returns {RichEmbed} This embed + */ + setColor(color) { + let radix = 10; + if (color instanceof Array) { + color = (color[0] << 16) + (color[1] << 8) + color[2]; + } else if (typeof color === 'string' && color.startsWith('#')) { + radix = 16; + color = color.replace('#', ''); + } + color = parseInt(color, radix); + if (color < 0 || color > 0xFFFFFF) { + throw new RangeError('RichEmbed color must be within the range 0 - 16777215 (0xFFFFFF).'); + } else if (color && isNaN(color)) { + throw new TypeError('Unable to convert RichEmbed color to a number.'); + } + this.color = color; + return this; + } + + /** + * Sets the author of this embed + * @param {StringResolvable} name The name of the author + * @param {string} [icon] The icon URL of the author + * @param {string} [url] The URL of the author + * @returns {RichEmbed} This embed + */ + setAuthor(name, icon, url) { + this.author = { name: resolveString(name), icon_url: icon, url }; + return this; + } + + /** + * Sets the timestamp of this embed + * @param {Date} [timestamp=current date] The timestamp + * @returns {RichEmbed} This embed + */ + setTimestamp(timestamp = new Date()) { + this.timestamp = timestamp; + return this; + } + + /** + * Adds a field to the embed (max 25) + * @param {StringResolvable} name The name of the field + * @param {StringResolvable} value The value of the field + * @param {boolean} [inline=false] Set the field to display inline + * @returns {RichEmbed} This embed + */ + addField(name, value, inline = false) { + if (this.fields.length >= 25) throw new RangeError('RichEmbeds may not exceed 25 fields.'); + name = resolveString(name); + if (name.length > 256) throw new RangeError('RichEmbed field names may not exceed 256 characters.'); + value = resolveString(value); + if (value.length > 1024) throw new RangeError('RichEmbed field values may not exceed 1024 characters.'); + this.fields.push({ name: String(name), value: value, inline }); + return this; + } + + /** + * Set the thumbnail of this embed + * @param {string} url The URL of the thumbnail + * @returns {RichEmbed} This embed + */ + setThumbnail(url) { + this.thumbnail = { url }; + return this; + } + + /** + * Set the image of this embed + * @param {string} url The URL of the thumbnail + * @returns {RichEmbed} This embed + */ + setImage(url) { + this.image = { url }; + return this; + } + + /** + * Sets the footer of this embed + * @param {StringResolvable} text The text of the footer + * @param {string} [icon] The icon URL of the footer + * @returns {RichEmbed} This embed + */ + setFooter(text, icon) { + text = resolveString(text); + if (text.length > 2048) throw new RangeError('RichEmbed footer text may not exceed 2048 characters.'); + this.footer = { text, icon_url: icon }; + return this; + } +} + +module.exports = RichEmbed; + +function resolveString(data) { + if (typeof data === 'string') return data; + if (data instanceof Array) return data.join('\n'); + return String(data); +} diff --git a/node_modules/discord.js/src/structures/Role.js b/node_modules/discord.js/src/structures/Role.js new file mode 100644 index 0000000..c15ff4b --- /dev/null +++ b/node_modules/discord.js/src/structures/Role.js @@ -0,0 +1,341 @@ +const Constants = require('../util/Constants'); + +/** + * Represents a role on Discord + */ +class Role { + constructor(guild, data) { + /** + * The client that instantiated the role + * @name Role#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: guild.client }); + + /** + * The guild that the role belongs to + * @type {Guild} + */ + this.guild = guild; + + if (data) this.setup(data); + } + + setup(data) { + /** + * The ID of the role (unique to the guild it is part of) + * @type {string} + */ + this.id = data.id; + + /** + * The name of the role + * @type {string} + */ + this.name = data.name; + + /** + * The base 10 color of the role + * @type {number} + */ + this.color = data.color; + + /** + * If true, users that are part of this role will appear in a separate category in the users list + * @type {boolean} + */ + this.hoist = data.hoist; + + /** + * The position of the role in the role manager + * @type {number} + */ + this.position = data.position; + + /** + * The evaluated permissions number + * @type {number} + */ + this.permissions = data.permissions; + + /** + * Whether or not the role is managed by an external service + * @type {boolean} + */ + this.managed = data.managed; + + /** + * Whether or not the role can be mentioned by anyone + * @type {boolean} + */ + this.mentionable = data.mentionable; + } + + /** + * The timestamp the role was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return (this.id / 4194304) + 1420070400000; + } + + /** + * The time the role was created + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The hexadecimal version of the role color, with a leading hashtag. + * @type {string} + * @readonly + */ + get hexColor() { + let col = this.color.toString(16); + while (col.length < 6) col = `0${col}`; + return `#${col}`; + } + + /** + * The cached guild members that have this role. + * @type {Collection} + * @readonly + */ + get members() { + return this.guild.members.filter(m => m.roles.has(this.id)); + } + + /** + * Whether the role is editable by the client user. + * @type {boolean} + * @readonly + */ + get editable() { + if (this.managed) return false; + const clientMember = this.guild.member(this.client.user); + if (!clientMember.hasPermission(Constants.PermissionFlags.MANAGE_ROLES_OR_PERMISSIONS)) return false; + return clientMember.highestRole.comparePositionTo(this) > 0; + } + + /** + * Get an object mapping permission names to whether or not the role enables that permission + * @returns {Object} + * @example + * // print the serialized role + * console.log(role.serialize()); + */ + serialize() { + const serializedPermissions = {}; + for (const permissionName in Constants.PermissionFlags) { + serializedPermissions[permissionName] = this.hasPermission(permissionName); + } + return serializedPermissions; + } + + /** + * Checks if the role has a permission. + * @param {PermissionResolvable} permission The permission to check for + * @param {boolean} [explicit=false] Whether to require the role to explicitly have the exact permission + * @returns {boolean} + * @example + * // see if a role can ban a member + * if (role.hasPermission('BAN_MEMBERS')) { + * console.log('This role can ban members'); + * } else { + * console.log('This role can\'t ban members'); + * } + */ + hasPermission(permission, explicit = false) { + permission = this.client.resolver.resolvePermission(permission); + if (!explicit && (this.permissions & Constants.PermissionFlags.ADMINISTRATOR) > 0) return true; + return (this.permissions & permission) > 0; + } + + /** + * Checks if the role has all specified permissions. + * @param {PermissionResolvable[]} permissions The permissions to check for + * @param {boolean} [explicit=false] Whether to require the role to explicitly have the exact permissions + * @returns {boolean} + */ + hasPermissions(permissions, explicit = false) { + return permissions.every(p => this.hasPermission(p, explicit)); + } + + /** + * Compares this role's position to another role's. + * @param {Role} 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) { + return this.constructor.comparePositions(this, role); + } + + /** + * The data for a role + * @typedef {Object} RoleData + * @property {string} [name] The name of the role + * @property {number|string} [color] The color of the role, either a hex string or a base 10 number + * @property {boolean} [hoist] Whether or not the role should be hoisted + * @property {number} [position] The position of the role + * @property {string[]} [permissions] The permissions of the role + * @property {boolean} [mentionable] Whether or not the role should be mentionable + */ + + /** + * Edits the role + * @param {RoleData} data The new data for the role + * @returns {Promise} + * @example + * // edit a role + * role.edit({name: 'new role'}) + * .then(r => console.log(`Edited role ${r}`)) + * .catch(console.error); + */ + edit(data) { + return this.client.rest.methods.updateGuildRole(this, data); + } + + /** + * Set a new name for the role + * @param {string} name The new name of the role + * @returns {Promise} + * @example + * // set the name of the role + * role.setName('new role') + * .then(r => console.log(`Edited name of role ${r}`)) + * .catch(console.error); + */ + setName(name) { + return this.edit({ name }); + } + + /** + * Set a new color for the role + * @param {number|string} color The new color for the role, either a hex string or a base 10 number + * @returns {Promise} + * @example + * // set the color of a role + * role.setColor('#FF0000') + * .then(r => console.log(`Set color of role ${r}`)) + * .catch(console.error); + */ + setColor(color) { + return this.edit({ color }); + } + + /** + * Set whether or not the role should be hoisted + * @param {boolean} hoist Whether or not to hoist the role + * @returns {Promise} + * @example + * // set the hoist of the role + * role.setHoist(true) + * .then(r => console.log(`Role hoisted: ${r.hoist}`)) + * .catch(console.error); + */ + setHoist(hoist) { + return this.edit({ hoist }); + } + + /** + * Set the position of the role + * @param {number} position The position of the role + * @returns {Promise} + * @example + * // set the position of the role + * role.setPosition(1) + * .then(r => console.log(`Role position: ${r.position}`)) + * .catch(console.error); + */ + setPosition(position) { + return this.guild.setRolePosition(this, position).then(() => this); + } + + /** + * Set the permissions of the role + * @param {string[]} permissions The permissions of the role + * @returns {Promise} + * @example + * // set the permissions of the role + * role.setPermissions(['KICK_MEMBERS', 'BAN_MEMBERS']) + * .then(r => console.log(`Role updated ${r}`)) + * .catch(console.error); + */ + setPermissions(permissions) { + return this.edit({ permissions }); + } + + /** + * Set whether this role is mentionable + * @param {boolean} mentionable Whether this role should be mentionable + * @returns {Promise} + * @example + * // make the role mentionable + * role.setMentionable(true) + * .then(r => console.log(`Role updated ${r}`)) + * .catch(console.error); + */ + setMentionable(mentionable) { + return this.edit({ mentionable }); + } + + /** + * Deletes the role + * @returns {Promise} + * @example + * // delete a role + * role.delete() + * .then(r => console.log(`Deleted role ${r}`)) + * .catch(console.error); + */ + delete() { + return this.client.rest.methods.deleteGuildRole(this); + } + + /** + * 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 + * what most users need. + * @param {Role} role Role to compare with + * @returns {boolean} + */ + equals(role) { + return role && + this.id === role.id && + this.name === role.name && + this.color === role.color && + this.hoist === role.hoist && + this.position === role.position && + this.permissions === role.permissions && + this.managed === role.managed; + } + + /** + * When concatenated with a string, this automatically concatenates the role mention rather than the Role object. + * @returns {string} + */ + toString() { + if (this.id === this.guild.id) return '@everyone'; + return `<@&${this.id}>`; + } + + /** + * Compares the positions of two roles. + * @param {Role} role1 First role to compare + * @param {Role} 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 + */ + static comparePositions(role1, role2) { + if (role1.position === role2.position) return role2.id - role1.id; + return role1.position - role2.position; + } +} + +module.exports = Role; diff --git a/node_modules/discord.js/src/structures/TextChannel.js b/node_modules/discord.js/src/structures/TextChannel.js new file mode 100644 index 0000000..9697abd --- /dev/null +++ b/node_modules/discord.js/src/structures/TextChannel.js @@ -0,0 +1,96 @@ +const GuildChannel = require('./GuildChannel'); +const TextBasedChannel = require('./interface/TextBasedChannel'); +const Collection = require('../util/Collection'); + +/** + * Represents a guild text channel on Discord. + * @extends {GuildChannel} + * @implements {TextBasedChannel} + */ +class TextChannel extends GuildChannel { + constructor(guild, data) { + super(guild, data); + this.type = 'text'; + this.messages = new Collection(); + this._typing = new Map(); + } + + setup(data) { + super.setup(data); + + /** + * The topic of the text channel, if there is one. + * @type {?string} + */ + this.topic = data.topic; + + this.lastMessageID = data.last_message_id; + } + + /** + * A collection of members that can see this channel, mapped by their ID. + * @type {Collection} + * @readonly + */ + get members() { + const members = new Collection(); + for (const member of this.guild.members.values()) { + if (this.permissionsFor(member).hasPermission('READ_MESSAGES')) { + members.set(member.id, member); + } + } + return members; + } + + /** + * Fetch all webhooks for the channel. + * @returns {Promise>} + */ + fetchWebhooks() { + return this.client.rest.methods.getChannelWebhooks(this); + } + + /** + * Create a webhook for the channel. + * @param {string} name The name of the webhook. + * @param {BufferResolvable} avatar The avatar for the webhook. + * @returns {Promise} webhook The created webhook. + * @example + * channel.createWebhook('Snek', 'http://snek.s3.amazonaws.com/topSnek.png') + * .then(webhook => console.log(`Created Webhook ${webhook}`)) + * .catch(console.error) + */ + createWebhook(name, avatar) { + return new Promise(resolve => { + if (avatar.startsWith('data:')) { + resolve(this.client.rest.methods.createWebhook(this, name, avatar)); + } else { + this.client.resolver.resolveBuffer(avatar).then(data => + resolve(this.client.rest.methods.createWebhook(this, name, data)) + ); + } + }); + } + + // These are here only for documentation purposes - they are implemented by TextBasedChannel + send() { return; } + sendMessage() { return; } + sendEmbed() { return; } + sendFile() { return; } + sendCode() { return; } + fetchMessage() { return; } + fetchMessages() { return; } + fetchPinnedMessages() { return; } + startTyping() { return; } + stopTyping() { return; } + get typing() { return; } + get typingCount() { return; } + createCollector() { return; } + awaitMessages() { return; } + bulkDelete() { return; } + _cacheMessage() { return; } +} + +TextBasedChannel.applyToClass(TextChannel, true); + +module.exports = TextChannel; diff --git a/node_modules/discord.js/src/structures/User.js b/node_modules/discord.js/src/structures/User.js new file mode 100644 index 0000000..f714828 --- /dev/null +++ b/node_modules/discord.js/src/structures/User.js @@ -0,0 +1,277 @@ +const TextBasedChannel = require('./interface/TextBasedChannel'); +const Constants = require('../util/Constants'); +const Presence = require('./Presence').Presence; + +/** + * Represents a user on Discord. + * @implements {TextBasedChannel} + */ +class User { + constructor(client, data) { + /** + * The Client that created the instance of the the User. + * @name User#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + if (data) this.setup(data); + } + + setup(data) { + /** + * The ID of the user + * @type {string} + */ + this.id = data.id; + + /** + * The username of the user + * @type {string} + */ + this.username = data.username; + + /** + * A discriminator based on username for the user + * @type {string} + */ + this.discriminator = data.discriminator; + + /** + * The ID of the user's avatar + * @type {string} + */ + this.avatar = data.avatar; + + /** + * Whether or not the user is a bot. + * @type {boolean} + */ + this.bot = Boolean(data.bot); + + /** + * The ID of the last message sent by the user, if one was sent. + * @type {?string} + */ + this.lastMessageID = null; + } + + patch(data) { + for (const prop of ['id', 'username', 'discriminator', 'avatar', 'bot']) { + if (typeof data[prop] !== 'undefined') this[prop] = data[prop]; + } + if (data.token) this.client.token = data.token; + } + + /** + * The timestamp the user was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return (this.id / 4194304) + 1420070400000; + } + + /** + * The time the user was created + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The presence of this user + * @type {Presence} + * @readonly + */ + get presence() { + if (this.client.presences.has(this.id)) return this.client.presences.get(this.id); + for (const guild of this.client.guilds.values()) { + if (guild.presences.has(this.id)) return guild.presences.get(this.id); + } + return new Presence(); + } + + /** + * A link to the user's avatar (if they have one, otherwise null) + * @type {?string} + * @readonly + */ + get avatarURL() { + if (!this.avatar) return null; + return Constants.Endpoints.avatar(this.id, this.avatar); + } + + /** + * A link to the user's default avatar + * @type {string} + * @readonly + */ + get defaultAvatarURL() { + let defaultAvatars = Object.values(Constants.DefaultAvatars); + let defaultAvatar = this.discriminator % defaultAvatars.length; + return Constants.Endpoints.assets(`${defaultAvatars[defaultAvatar]}.png`); + } + + /** + * A link to the user's avatar if they have one. Otherwise a link to their default avatar will be returned + * @type {string} + * @readonly + */ + get displayAvatarURL() { + return this.avatarURL || this.defaultAvatarURL; + } + + /** + * The note that is set for the user + * This is only available when using a user account. + * @type {?string} + * @readonly + */ + get note() { + return this.client.user.notes.get(this.id) || null; + } + + /** + * Check whether the user is typing in a channel. + * @param {ChannelResolvable} channel The channel to check in + * @returns {boolean} + */ + typingIn(channel) { + channel = this.client.resolver.resolveChannel(channel); + return channel._typing.has(this.id); + } + + /** + * Get the time that the user started typing. + * @param {ChannelResolvable} channel The channel to get the time in + * @returns {?Date} + */ + typingSinceIn(channel) { + channel = this.client.resolver.resolveChannel(channel); + return channel._typing.has(this.id) ? new Date(channel._typing.get(this.id).since) : null; + } + + /** + * Get the amount of time the user has been typing in a channel for (in milliseconds), or -1 if they're not typing. + * @param {ChannelResolvable} channel The channel to get the time in + * @returns {number} + */ + typingDurationIn(channel) { + channel = this.client.resolver.resolveChannel(channel); + return channel._typing.has(this.id) ? channel._typing.get(this.id).elapsedTime : -1; + } + + /** + * The DM between the client's user and this user + * @type {?DMChannel} + */ + get dmChannel() { + return this.client.channels.filter(c => c.type === 'dm').find(c => c.recipient.id === this.id); + } + + /** + * Deletes a DM channel (if one exists) between the client and the user. Resolves with the channel if successful. + * @returns {Promise} + */ + deleteDM() { + return this.client.rest.methods.deleteChannel(this); + } + + /** + * Sends a friend request to the user + * This is only available when using a user account. + * @returns {Promise} + */ + addFriend() { + return this.client.rest.methods.addFriend(this); + } + + /** + * Removes the user from your friends + * This is only available when using a user account. + * @returns {Promise} + */ + removeFriend() { + return this.client.rest.methods.removeFriend(this); + } + + /** + * Blocks the user + * This is only available when using a user account. + * @returns {Promise} + */ + block() { + return this.client.rest.methods.blockUser(this); + } + + /** + * Unblocks the user + * This is only available when using a user account. + * @returns {Promise} + */ + unblock() { + return this.client.rest.methods.unblockUser(this); + } + + /** + * Get the profile of the user + * This is only available when using a user account. + * @returns {Promise} + */ + fetchProfile() { + return this.client.rest.methods.fetchUserProfile(this); + } + + /** + * Sets a note for the user + * This is only available when using a user account. + * @param {string} note The note to set for the user + * @returns {Promise} + */ + setNote(note) { + return this.client.rest.methods.setNote(this, note); + } + + /** + * Checks if the user is equal to another. It compares ID, username, discriminator, avatar, and bot flags. + * It is recommended to compare equality by using `user.id === user2.id` unless you want to compare all properties. + * @param {User} user User to compare with + * @returns {boolean} + */ + equals(user) { + let equal = user && + this.id === user.id && + this.username === user.username && + this.discriminator === user.discriminator && + this.avatar === user.avatar && + this.bot === Boolean(user.bot); + + return equal; + } + + /** + * When concatenated with a string, this automatically concatenates the user's mention instead of the User object. + * @returns {string} + * @example + * // logs: Hello from <@123456789>! + * console.log(`Hello from ${user}!`); + */ + toString() { + return `<@${this.id}>`; + } + + // These are here only for documentation purposes - they are implemented by TextBasedChannel + send() { return; } + sendMessage() { return; } + sendEmbed() { return; } + sendFile() { return; } + sendCode() { return; } +} + +TextBasedChannel.applyToClass(User); + +module.exports = User; diff --git a/node_modules/discord.js/src/structures/UserConnection.js b/node_modules/discord.js/src/structures/UserConnection.js new file mode 100644 index 0000000..6ee9fc5 --- /dev/null +++ b/node_modules/discord.js/src/structures/UserConnection.js @@ -0,0 +1,48 @@ +/** + * Represents a user connection (or "platform identity") + */ +class UserConnection { + constructor(user, data) { + /** + * The user that owns the Connection + * @type {User} + */ + this.user = user; + + this.setup(data); + } + + setup(data) { + /** + * The type of the Connection + * @type {string} + */ + this.type = data.type; + + /** + * The username of the connection account + * @type {string} + */ + this.name = data.name; + + /** + * The id of the connection account + * @type {string} + */ + this.id = data.id; + + /** + * Whether the connection is revoked + * @type {boolean} + */ + this.revoked = data.revoked; + + /** + * an array of partial server integrations (not yet implemented in this lib) + * @type {Object[]} + */ + this.integrations = data.integrations; + } +} + +module.exports = UserConnection; diff --git a/node_modules/discord.js/src/structures/UserProfile.js b/node_modules/discord.js/src/structures/UserProfile.js new file mode 100644 index 0000000..77f097c --- /dev/null +++ b/node_modules/discord.js/src/structures/UserProfile.js @@ -0,0 +1,56 @@ +const Collection = require('../util/Collection'); +const UserConnection = require('./UserConnection'); + +/** + * Represents a user's profile on Discord. + */ +class UserProfile { + constructor(user, data) { + /** + * The owner of the profile + * @type {User} + */ + this.user = user; + + /** + * The Client that created the instance of the the UserProfile. + * @name UserProfile#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: user.client }); + + /** + * Guilds that the client user and the user share + * @type {Collection} + */ + this.mutualGuilds = new Collection(); + + /** + * The user's connections + * @type {Collection} + */ + this.connections = new Collection(); + + this.setup(data); + } + + setup(data) { + /** + * If the user has Discord Premium + * @type {boolean} + */ + this.premium = data.premium; + + for (const guild of data.mutual_guilds) { + if (this.client.guilds.has(guild.id)) { + this.mutualGuilds.set(guild.id, this.client.guilds.get(guild.id)); + } + } + for (const connection of data.connected_accounts) { + this.connections.set(connection.id, new UserConnection(this.user, connection)); + } + } +} + +module.exports = UserProfile; diff --git a/node_modules/discord.js/src/structures/VoiceChannel.js b/node_modules/discord.js/src/structures/VoiceChannel.js new file mode 100644 index 0000000..848a6d5 --- /dev/null +++ b/node_modules/discord.js/src/structures/VoiceChannel.js @@ -0,0 +1,120 @@ +const GuildChannel = require('./GuildChannel'); +const Collection = require('../util/Collection'); + +/** + * Represents a guild voice channel on Discord. + * @extends {GuildChannel} + */ +class VoiceChannel extends GuildChannel { + constructor(guild, data) { + super(guild, data); + + /** + * The members in this voice channel. + * @type {Collection} + */ + this.members = new Collection(); + + this.type = 'voice'; + } + + setup(data) { + super.setup(data); + + /** + * The bitrate of this voice channel + * @type {number} + */ + this.bitrate = data.bitrate; + + /** + * The maximum amount of users allowed in this channel - 0 means unlimited. + * @type {number} + */ + this.userLimit = data.user_limit; + } + + /** + * The voice connection for this voice channel, if the client is connected + * @type {?VoiceConnection} + * @readonly + */ + get connection() { + const connection = this.guild.voiceConnection; + if (connection && connection.channel.id === this.id) return connection; + return null; + } + + /** + * Checks if the client has permission join the voice channel + * @type {boolean} + */ + get joinable() { + if (this.client.browser) return false; + return this.permissionsFor(this.client.user).hasPermission('CONNECT'); + } + + /** + * Checks if the client has permission to send audio to the voice channel + * @type {boolean} + */ + get speakable() { + return this.permissionsFor(this.client.user).hasPermission('SPEAK'); + } + + /** + * Sets the bitrate of the channel + * @param {number} bitrate The new bitrate + * @returns {Promise} + * @example + * // set the bitrate of a voice channel + * voiceChannel.setBitrate(48000) + * .then(vc => console.log(`Set bitrate to ${vc.bitrate} for ${vc.name}`)) + * .catch(console.error); + */ + setBitrate(bitrate) { + return this.edit({ bitrate }); + } + + /** + * Sets the user limit of the channel + * @param {number} userLimit The new user limit + * @returns {Promise} + * @example + * // set the user limit of a voice channel + * voiceChannel.setUserLimit(42) + * .then(vc => console.log(`Set user limit to ${vc.userLimit} for ${vc.name}`)) + * .catch(console.error); + */ + setUserLimit(userLimit) { + return this.edit({ userLimit }); + } + + /** + * Attempts to join this voice channel + * @returns {Promise} + * @example + * // join a voice channel + * voiceChannel.join() + * .then(connection => console.log('Connected!')) + * .catch(console.error); + */ + join() { + if (this.client.browser) return Promise.reject(new Error('Voice connections are not available in browsers.')); + return this.client.voice.joinChannel(this); + } + + /** + * Leaves this voice channel + * @example + * // leave a voice channel + * voiceChannel.leave(); + */ + leave() { + if (this.client.browser) return; + const connection = this.client.voice.connections.get(this.guild.id); + if (connection && connection.channel.id === this.id) connection.disconnect(); + } +} + +module.exports = VoiceChannel; diff --git a/node_modules/discord.js/src/structures/Webhook.js b/node_modules/discord.js/src/structures/Webhook.js new file mode 100644 index 0000000..96984ff --- /dev/null +++ b/node_modules/discord.js/src/structures/Webhook.js @@ -0,0 +1,200 @@ +const path = require('path'); +const escapeMarkdown = require('../util/EscapeMarkdown'); + +/** + * Represents a webhook + */ +class Webhook { + constructor(client, dataOrID, token) { + if (client) { + /** + * The Client that instantiated the Webhook + * @name Webhook#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + if (dataOrID) this.setup(dataOrID); + } else { + this.id = dataOrID; + this.token = token; + Object.defineProperty(this, 'client', { value: this }); + } + } + + setup(data) { + /** + * The name of the webhook + * @type {string} + */ + this.name = data.name; + + /** + * The token for the webhook + * @type {string} + */ + this.token = data.token; + + /** + * The avatar for the webhook + * @type {string} + */ + this.avatar = data.avatar; + + /** + * The ID of the webhook + * @type {string} + */ + this.id = data.id; + + /** + * The guild the webhook belongs to + * @type {string} + */ + this.guildID = data.guild_id; + + /** + * The channel the webhook belongs to + * @type {string} + */ + this.channelID = data.channel_id; + + /** + * The owner of the webhook + * @type {User} + */ + if (data.user) this.owner = data.user; + } + + /** + * Options that can be passed into sendMessage, sendTTSMessage, sendFile, sendCode + * @typedef {Object} WebhookMessageOptions + * @property {boolean} [tts=false] Whether or not the message should be spoken aloud + * @property {boolean} [disableEveryone=this.options.disableEveryone] Whether or not @everyone and @here + * should be replaced with plain-text + */ + + /** + * Send a message with this webhook + * @param {StringResolvable} content The content to send. + * @param {WebhookMessageOptions} [options={}] The options to provide. + * @returns {Promise} + * @example + * // send a message + * webhook.sendMessage('hello!') + * .then(message => console.log(`Sent message: ${message.content}`)) + * .catch(console.error); + */ + sendMessage(content, options = {}) { + return this.client.rest.methods.sendWebhookMessage(this, content, options); + } + + /** + * Send a raw slack message with this webhook + * @param {Object} body The raw body to send. + * @returns {Promise} + * @example + * // send a slack message + * webhook.sendSlackMessage({ + * 'username': 'Wumpus', + * 'attachments': [{ + * 'pretext': 'this looks pretty cool', + * 'color': '#F0F', + * 'footer_icon': 'http://snek.s3.amazonaws.com/topSnek.png', + * 'footer': 'Powered by sneks', + * 'ts': Date.now() / 1000 + * }] + * }).catch(console.error); + */ + sendSlackMessage(body) { + return this.client.rest.methods.sendSlackWebhookMessage(this, body); + } + + /** + * Send a text-to-speech message with this webhook + * @param {StringResolvable} content The content to send + * @param {WebhookMessageOptions} [options={}] The options to provide + * @returns {Promise} + * @example + * // send a TTS message + * webhook.sendTTSMessage('hello!') + * .then(message => console.log(`Sent tts message: ${message.content}`)) + * .catch(console.error); + */ + sendTTSMessage(content, options = {}) { + Object.assign(options, { tts: true }); + return this.client.rest.methods.sendWebhookMessage(this, content, options); + } + + /** + * Send a file with this webhook + * @param {BufferResolvable} attachment The file to send + * @param {string} [fileName="file.jpg"] The name and extension of the file + * @param {StringResolvable} [content] Text message to send with the attachment + * @param {WebhookMessageOptions} [options] The options to provide + * @returns {Promise} + */ + sendFile(attachment, fileName, content, options = {}) { + if (!fileName) { + if (typeof attachment === 'string') { + fileName = path.basename(attachment); + } else if (attachment && attachment.path) { + fileName = path.basename(attachment.path); + } else { + fileName = 'file.jpg'; + } + } + return this.client.resolver.resolveBuffer(attachment).then(file => + this.client.rest.methods.sendWebhookMessage(this, content, options, { + file, + name: fileName, + }) + ); + } + + /** + * Send a code block with this webhook + * @param {string} lang Language for the code block + * @param {StringResolvable} content Content of the code block + * @param {WebhookMessageOptions} options The options to provide + * @returns {Promise} + */ + sendCode(lang, content, options = {}) { + if (options.split) { + if (typeof options.split !== 'object') options.split = {}; + if (!options.split.prepend) options.split.prepend = `\`\`\`${lang || ''}\n`; + if (!options.split.append) options.split.append = '\n```'; + } + content = escapeMarkdown(this.client.resolver.resolveString(content), true); + return this.sendMessage(`\`\`\`${lang || ''}\n${content}\n\`\`\``, options); + } + + /** + * Edit the webhook. + * @param {string} name The new name for the Webhook + * @param {BufferResolvable} avatar The new avatar for the Webhook. + * @returns {Promise} + */ + edit(name = this.name, avatar) { + if (avatar) { + return this.client.resolver.resolveBuffer(avatar).then(file => { + const dataURI = this.client.resolver.resolveBase64(file); + return this.client.rest.methods.editWebhook(this, name, dataURI); + }); + } + return this.client.rest.methods.editWebhook(this, name).then(data => { + this.setup(data); + return this; + }); + } + + /** + * Delete the webhook + * @returns {Promise} + */ + delete() { + return this.client.rest.methods.deleteWebhook(this); + } +} + +module.exports = Webhook; diff --git a/node_modules/discord.js/src/structures/interface/TextBasedChannel.js b/node_modules/discord.js/src/structures/interface/TextBasedChannel.js new file mode 100644 index 0000000..353c0a9 --- /dev/null +++ b/node_modules/discord.js/src/structures/interface/TextBasedChannel.js @@ -0,0 +1,377 @@ +const path = require('path'); +const Message = require('../Message'); +const MessageCollector = require('../MessageCollector'); +const Collection = require('../../util/Collection'); + + +/** + * Interface for classes that have text-channel-like features + * @interface + */ +class TextBasedChannel { + constructor() { + /** + * A collection containing the messages sent to this channel. + * @type {Collection} + */ + this.messages = new Collection(); + + /** + * The ID of the last message in the channel, if one was sent. + * @type {?string} + */ + this.lastMessageID = null; + } + + /** + * Options that can be passed into send, sendMessage, sendFile, sendEmbed, sendCode, and Message#reply + * @typedef {Object} MessageOptions + * @property {boolean} [tts=false] Whether or not the message should be spoken aloud + * @property {string} [nonce=''] The nonce for the message + * @property {Object} [embed] An embed for the message + * (see [here](https://discordapp.com/developers/docs/resources/channel#embed-object) for more details) + * @property {boolean} [disableEveryone=this.client.options.disableEveryone] Whether or not @everyone and @here + * should be replaced with plain-text + * @property {FileOptions|string} [file] A file to send with the message + * @property {string|boolean} [code] Language for optional codeblock formatting to apply + * @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if + * it exceeds the character limit. If an object is provided, these are the options for splitting the message. + */ + + /** + * @typedef {Object} FileOptions + * @property {BufferResolvable} attachment + * @property {string} [name='file.jpg'] + */ + + /** + * Options for splitting a message + * @typedef {Object} SplitOptions + * @property {number} [maxLength=1950] Maximum character length per message piece + * @property {string} [char='\n'] Character to split the message with + * @property {string} [prepend=''] Text to prepend to every piece except the first + * @property {string} [append=''] Text to append to every piece except the last + */ + + /** + * Send a message to this channel + * @param {StringResolvable} [content] Text for the message + * @param {MessageOptions} [options={}] Options for the message + * @returns {Promise} + * @example + * // send a message + * channel.send('hello!') + * .then(message => console.log(`Sent message: ${message.content}`)) + * .catch(console.error); + */ + send(content, options) { + if (!options && typeof content === 'object' && !(content instanceof Array)) { + options = content; + content = ''; + } else if (!options) { + options = {}; + } + if (options.file) { + if (typeof options.file === 'string') options.file = { attachment: options.file }; + if (!options.file.name) { + if (typeof options.file.attachment === 'string') { + options.file.name = path.basename(options.file.attachment); + } else if (options.file.attachment && options.file.attachment.path) { + options.file.name = path.basename(options.file.attachment.path); + } else { + options.file.name = 'file.jpg'; + } + } + return this.client.resolver.resolveBuffer(options.file.attachment).then(file => + this.client.rest.methods.sendMessage(this, content, options, { + file, + name: options.file.name, + }) + ); + } + return this.client.rest.methods.sendMessage(this, content, options); + } + + /** + * Send a message to this channel + * @param {StringResolvable} content Text for the message + * @param {MessageOptions} [options={}] Options for the message + * @returns {Promise} + * @example + * // send a message + * channel.sendMessage('hello!') + * .then(message => console.log(`Sent message: ${message.content}`)) + * .catch(console.error); + */ + sendMessage(content, options) { + return this.send(content, options); + } + + /** + * Send an embed to this channel + * @param {RichEmbed|Object} embed Embed for the message + * @param {string} [content] Text for the message + * @param {MessageOptions} [options] Options for the message + * @returns {Promise} + */ + sendEmbed(embed, content, options) { + if (!options && typeof content === 'object') { + options = content; + content = ''; + } else if (!options) { + options = {}; + } + return this.send(content, Object.assign(options, { embed })); + } + + /** + * Send a file to this channel + * @param {BufferResolvable} attachment File to send + * @param {string} [name='file.jpg'] Name and extension of the file + * @param {StringResolvable} [content] Text for the message + * @param {MessageOptions} [options] Options for the message + * @returns {Promise} + */ + sendFile(attachment, name, content, options = {}) { + return this.send(content, Object.assign(options, { file: { attachment, name } })); + } + + /** + * Send a code block to this channel + * @param {string} lang Language for the code block + * @param {StringResolvable} content Content of the code block + * @param {MessageOptions} [options] Options for the message + * @returns {Promise} + */ + sendCode(lang, content, options = {}) { + return this.send(content, Object.assign(options, { code: lang })); + } + + /** + * Gets a single message from this channel, regardless of it being cached or not. + * This is only available when using a bot account. + * @param {string} messageID ID of the message to get + * @returns {Promise} + * @example + * // get message + * channel.fetchMessage('99539446449315840') + * .then(message => console.log(message.content)) + * .catch(console.error); + */ + fetchMessage(messageID) { + return this.client.rest.methods.getChannelMessage(this, messageID).then(data => { + const msg = data instanceof Message ? data : new Message(this, data, this.client); + this._cacheMessage(msg); + return msg; + }); + } + + /** + * The parameters to pass in when requesting previous messages from a channel. `around`, `before` and + * `after` are mutually exclusive. All the parameters are optional. + * @typedef {Object} ChannelLogsQueryOptions + * @property {number} [limit=50] Number of messages to acquire + * @property {string} [before] ID of a message to get the messages that were posted before it + * @property {string} [after] ID of a message to get the messages that were posted after it + * @property {string} [around] ID of a message to get the messages that were posted around it + */ + + /** + * Gets the past messages sent in this channel. Resolves with a collection mapping message ID's to Message objects. + * @param {ChannelLogsQueryOptions} [options={}] Query parameters to pass in + * @returns {Promise>} + * @example + * // get messages + * channel.fetchMessages({limit: 10}) + * .then(messages => console.log(`Received ${messages.size} messages`)) + * .catch(console.error); + */ + fetchMessages(options = {}) { + return this.client.rest.methods.getChannelMessages(this, options).then(data => { + const messages = new Collection(); + for (const message of data) { + const msg = new Message(this, message, this.client); + messages.set(message.id, msg); + this._cacheMessage(msg); + } + return messages; + }); + } + + /** + * Fetches the pinned messages of this channel and returns a collection of them. + * @returns {Promise>} + */ + fetchPinnedMessages() { + return this.client.rest.methods.getChannelPinnedMessages(this).then(data => { + const messages = new Collection(); + for (const message of data) { + const msg = new Message(this, message, this.client); + messages.set(message.id, msg); + this._cacheMessage(msg); + } + return messages; + }); + } + + /** + * Starts a typing indicator in the channel. + * @param {number} [count] The number of times startTyping should be considered to have been called + * @example + * // start typing in a channel + * channel.startTyping(); + */ + startTyping(count) { + if (typeof count !== 'undefined' && count < 1) throw new RangeError('Count must be at least 1.'); + if (!this.client.user._typing.has(this.id)) { + this.client.user._typing.set(this.id, { + count: count || 1, + interval: this.client.setInterval(() => { + this.client.rest.methods.sendTyping(this.id); + }, 4000), + }); + this.client.rest.methods.sendTyping(this.id); + } else { + const entry = this.client.user._typing.get(this.id); + entry.count = count || entry.count + 1; + } + } + + /** + * Stops the typing indicator in the channel. + * The indicator will only stop if this is called as many times as startTyping(). + * It can take a few seconds for the client user to stop typing. + * @param {boolean} [force=false] Whether or not to reset the call count and force the indicator to stop + * @example + * // stop typing in a channel + * channel.stopTyping(); + * @example + * // force typing to fully stop in a channel + * channel.stopTyping(true); + */ + stopTyping(force = false) { + if (this.client.user._typing.has(this.id)) { + const entry = this.client.user._typing.get(this.id); + entry.count--; + if (entry.count <= 0 || force) { + this.client.clearInterval(entry.interval); + this.client.user._typing.delete(this.id); + } + } + } + + /** + * Whether or not the typing indicator is being shown in the channel. + * @type {boolean} + * @readonly + */ + get typing() { + return this.client.user._typing.has(this.id); + } + + /** + * Number of times `startTyping` has been called. + * @type {number} + * @readonly + */ + get typingCount() { + if (this.client.user._typing.has(this.id)) return this.client.user._typing.get(this.id).count; + return 0; + } + + /** + * Creates a Message Collector + * @param {CollectorFilterFunction} filter The filter to create the collector with + * @param {CollectorOptions} [options={}] The options to pass to the collector + * @returns {MessageCollector} + * @example + * // create a message collector + * const collector = channel.createCollector( + * m => m.content.includes('discord'), + * { time: 15000 } + * ); + * collector.on('message', m => console.log(`Collected ${m.content}`)); + * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); + */ + createCollector(filter, options = {}) { + return new MessageCollector(this, filter, options); + } + + /** + * An object containing the same properties as CollectorOptions, but a few more: + * @typedef {CollectorOptions} AwaitMessagesOptions + * @property {string[]} [errors] Stop/end reasons that cause the promise to reject + */ + + /** + * Similar to createCollector but in promise form. Resolves with a collection of messages that pass the specified + * filter. + * @param {CollectorFilterFunction} filter The filter function to use + * @param {AwaitMessagesOptions} [options={}] Optional options to pass to the internal collector + * @returns {Promise>} + * @example + * // await !vote messages + * const filter = m => m.content.startsWith('!vote'); + * // errors: ['time'] treats ending because of the time limit as an error + * channel.awaitMessages(filter, { max: 4, time: 60000, errors: ['time'] }) + * .then(collected => console.log(collected.size)) + * .catch(collected => console.log(`After a minute, only ${collected.size} out of 4 voted.`)); + */ + awaitMessages(filter, options = {}) { + return new Promise((resolve, reject) => { + const collector = this.createCollector(filter, options); + collector.on('end', (collection, reason) => { + if (options.errors && options.errors.includes(reason)) { + reject(collection); + } else { + resolve(collection); + } + }); + }); + } + + /** + * Bulk delete given messages. + * This is only available when using a bot account. + * @param {Collection|Message[]|number} messages Messages to delete, or number of messages to delete + * @returns {Promise>} Deleted messages + */ + bulkDelete(messages) { + if (!isNaN(messages)) return this.fetchMessages({ limit: messages }).then(msgs => this.bulkDelete(msgs)); + if (messages instanceof Array || messages instanceof Collection) { + const messageIDs = messages instanceof Collection ? messages.keyArray() : messages.map(m => m.id); + return this.client.rest.methods.bulkDeleteMessages(this, messageIDs); + } + throw new TypeError('The messages must be an Array, Collection, or number.'); + } + + _cacheMessage(message) { + const maxSize = this.client.options.messageCacheMaxSize; + if (maxSize === 0) return null; + if (this.messages.size >= maxSize && maxSize > 0) this.messages.delete(this.messages.firstKey()); + this.messages.set(message.id, message); + return message; + } +} + +exports.applyToClass = (structure, full = false) => { + const props = ['send', 'sendMessage', 'sendEmbed', 'sendFile', 'sendCode']; + if (full) { + props.push( + '_cacheMessage', + 'fetchMessages', + 'fetchMessage', + 'bulkDelete', + 'startTyping', + 'stopTyping', + 'typing', + 'typingCount', + 'fetchPinnedMessages', + 'createCollector', + 'awaitMessages' + ); + } + for (const prop of props) { + Object.defineProperty(structure.prototype, prop, Object.getOwnPropertyDescriptor(TextBasedChannel.prototype, prop)); + } +}; -- cgit v1.2.3