diff options
| author | 8cy <[email protected]> | 2020-04-03 02:37:42 -0700 |
|---|---|---|
| committer | 8cy <[email protected]> | 2020-04-03 02:37:42 -0700 |
| commit | 60867fb030bae582082340ead7dbc7efdc2f5398 (patch) | |
| tree | 4c6a7356351be2e4914e15c4703172597c45656e /node_modules/discord.js/src/structures | |
| parent | commenting (diff) | |
| download | s5nical-60867fb030bae582082340ead7dbc7efdc2f5398.tar.xz s5nical-60867fb030bae582082340ead7dbc7efdc2f5398.zip | |
2020/04/03, 02:34, v1.2.0
Diffstat (limited to 'node_modules/discord.js/src/structures')
45 files changed, 10410 insertions, 0 deletions
diff --git a/node_modules/discord.js/src/structures/Attachment.js b/node_modules/discord.js/src/structures/Attachment.js new file mode 100644 index 0000000..216b61c --- /dev/null +++ b/node_modules/discord.js/src/structures/Attachment.js @@ -0,0 +1,75 @@ +/** + * Represents an attachment in a message. + * @param {BufferResolvable|Stream} file The file + * @param {string} [name] The name of the file, if any + */ +class Attachment { + constructor(file, name) { + this.file = null; + if (name) this.setAttachment(file, name); + else this._attach(file); + } + + /** + * The name of the file + * @type {?string} + * @readonly + */ + get name() { + return this.file.name; + } + + /** + * The file + * @type {?BufferResolvable|Stream} + * @readonly + */ + get attachment() { + return this.file.attachment; + } + + /** + * Set the file of this attachment. + * @param {BufferResolvable|Stream} file The file + * @param {string} name The name of the file + * @returns {Attachment} This attachment + */ + setAttachment(file, name) { + this.file = { attachment: file, name }; + return this; + } + + /** + * Set the file of this attachment. + * @param {BufferResolvable|Stream} attachment The file + * @returns {Attachment} This attachment + */ + setFile(attachment) { + this.file = { attachment }; + return this; + } + + /** + * Set the name of this attachment. + * @param {string} name The name of the image + * @returns {Attachment} This attachment + */ + setName(name) { + this.file.name = name; + return this; + } + + /** + * Set the file of this attachment. + * @param {BufferResolvable|Stream} file The file + * @param {string} name The name of the file + * @returns {void} + * @private + */ + _attach(file, name) { + if (typeof file === 'string') this.file = file; + else this.setAttachment(file, name); + } +} + +module.exports = Attachment; diff --git a/node_modules/discord.js/src/structures/CategoryChannel.js b/node_modules/discord.js/src/structures/CategoryChannel.js new file mode 100644 index 0000000..9ba263a --- /dev/null +++ b/node_modules/discord.js/src/structures/CategoryChannel.js @@ -0,0 +1,22 @@ +const GuildChannel = require('./GuildChannel'); + +/** + * Represents a guild category channel on Discord. + * @extends {GuildChannel} + */ +class CategoryChannel extends GuildChannel { + constructor(guild, data) { + super(guild, data); + this.type = 'category'; + } + /** + * The channels that are part of this category + * @type {?Collection<Snowflake, GuildChannel>} + * @readonly + */ + get children() { + return this.guild.channels.filter(c => c.parentID === this.id); + } +} + +module.exports = CategoryChannel; 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..732945d --- /dev/null +++ b/node_modules/discord.js/src/structures/Channel.js @@ -0,0 +1,78 @@ +const Snowflake = require('../util/Snowflake'); + +/** + * 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 + * * `category` - a guild category channel + * * `news` - a guild news channel + * * `store` - a guild store channel + * @type {string} + */ + this.type = null; + + /** + * Whether the channel has been deleted + * @type {boolean} + */ + this.deleted = false; + + if (data) this.setup(data); + } + + setup(data) { + /** + * The unique ID of the channel + * @type {Snowflake} + */ + this.id = data.id; + } + + /** + * The timestamp the channel was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * The time the channel was created + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * Deletes the channel. + * @returns {Promise<Channel>} + * @example + * // Delete the channel + * channel.delete() + * .then(console.log) + * .catch(console.error); + */ + delete() { + return this.client.rest.methods.deleteChannel(this); + } +} + +module.exports = Channel; 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..4ae8c6e --- /dev/null +++ b/node_modules/discord.js/src/structures/ClientUser.js @@ -0,0 +1,447 @@ +const User = require('./User'); +const Collection = require('../util/Collection'); +const ClientUserSettings = require('./ClientUserSettings'); +const ClientUserGuildSettings = require('./ClientUserGuildSettings'); +const Constants = require('../util/Constants'); +const util = require('util'); + +/** + * 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 + * <warn>This is only filled when using a user account.</warn> + * @type {?string} + * @deprecated + */ + this.email = data.email; + this.localPresence = {}; + this._typing = new Map(); + + /** + * A Collection of friends for the logged in user + * <warn>This is only filled when using a user account.</warn> + * @type {Collection<Snowflake, User>} + * @deprecated + */ + this.friends = new Collection(); + + /** + * A Collection of blocked users for the logged in user + * <warn>This is only filled when using a user account.</warn> + * @type {Collection<Snowflake, User>} + * @deprecated + */ + this.blocked = new Collection(); + + /** + * A Collection of notes for the logged in user + * <warn>This is only filled when using a user account.</warn> + * @type {Collection<Snowflake, string>} + * @deprecated + */ + this.notes = new Collection(); + + /** + * If the user has Discord premium (nitro) + * <warn>This is only filled when using a user account.</warn> + * @type {?boolean} + * @deprecated + */ + this.premium = typeof data.premium === 'boolean' ? data.premium : null; + + /** + * If the user has MFA enabled on their account + * @type {boolean} + */ + this.mfaEnabled = data.mfa_enabled; + + /** + * If the user has ever used a mobile device on Discord + * <warn>This is only filled when using a user account.</warn> + * @type {?boolean} + * @deprecated + */ + this.mobile = typeof data.mobile === 'boolean' ? data.mobile : null; + + /** + * Various settings for this user + * <warn>This is only filled when using a user account.</warn> + * @type {?ClientUserSettings} + * @deprecated + */ + this.settings = data.user_settings ? new ClientUserSettings(this, data.user_settings) : null; + + /** + * All of the user's guild settings + * <warn>This is only filled when using a user account</warn> + * @type {Collection<Snowflake, ClientUserGuildSettings>} + * @deprecated + */ + this.guildSettings = new Collection(); + if (data.user_guild_settings) { + for (const settings of data.user_guild_settings) { + this.guildSettings.set(settings.guild_id, new ClientUserGuildSettings(settings, this.client)); + } + } + } + + edit(data) { + return this.client.rest.methods.updateCurrentUser(data); + } + + /** + * Set the username of the logged in client. + * <info>Changing usernames in Discord is heavily rate limited, with only 2 requests + * every hour. Use this sparingly!</info> + * @param {string} username The new username + * @param {string} [password] Current password (only for user accounts) + * @returns {Promise<ClientUser>} + * @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. + * <warn>This is only available when using a user account.</warn> + * @param {string} email New email to change to + * @param {string} password Current password + * @returns {Promise<ClientUser>} + * @deprecated + * @example + * // Set email + * client.user.setEmail('[email protected]', '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. + * <warn>This is only available when using a user account.</warn> + * @param {string} newPassword New password to change to + * @param {string} oldPassword Current password + * @returns {Promise<ClientUser>} + * @deprecated + * @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<ClientUser>} + * @example + * // Set avatar + * client.user.setAvatar('./avatar.png') + * .then(user => console.log(`New avatar set!`)) + * .catch(console.error); + */ + setAvatar(avatar) { + return this.client.resolver.resolveImage(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 + * @property {?ActivityType|number} [game.type] Type of the activity + */ + + /** + * Sets the full presence of the client user. + * @param {PresenceData} data Data for the presence + * @returns {Promise<ClientUser>} + * @example + * // Set the client user's presence + * client.user.setPresence({ game: { name: 'with discord.js' }, status: 'idle' }) + * .then(console.log) + * .catch(console.error); + */ + 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'); + if (this.bot) { + status = data.status; + } else { + this.settings.update(Constants.UserSettingsMap.status, data.status); + status = 'invisible'; + } + } + + if (data.game) { + game = data.game; + game.type = game.url && typeof game.type === 'undefined' ? 1 : game.type || 0; + if (typeof game.type === 'string') { + game.type = Constants.ActivityTypes.indexOf(game.type.toUpperCase()); + } + } else if (typeof data.game !== 'undefined') { + game = null; + } + + 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<ClientUser>} + * @example + * // Set the client user's status + * client.user.setStatus('idle') + * .then(console.log) + * .catch(console.error); + */ + 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<ClientUser>} + * @deprecated + */ + setGame(game, streamingURL) { + if (!game) return this.setPresence({ game: null }); + return this.setPresence({ + game: { + name: game, + url: streamingURL, + }, + }); + } + + /** + * Sets the activity the client user is playing. + * @param {?string} name Activity being played + * @param {Object} [options] Options for setting the activity + * @param {string} [options.url] Twitch stream URL + * @param {ActivityType|number} [options.type] Type of the activity + * @returns {Promise<Presence>} + * @example + * client.user.setActivity('YouTube', { type: 'WATCHING' }) + * .then(presence => console.log(`Activity set to ${presence.game ? presence.game.name : 'none'}`)) + * .catch(console.error); + */ + setActivity(name, { url, type } = {}) { + if (!name) return this.setPresence({ game: null }); + return this.setPresence({ + game: { name, type, url }, + }).then(clientUser => clientUser.presence); + } + + /** + * Sets/removes the AFK flag for the client user. + * @param {boolean} afk Whether or not the user is AFK + * @returns {Promise<ClientUser>} + */ + setAFK(afk) { + return this.setPresence({ afk }); + } + + /** + * Fetches messages that mentioned the client's user. + * <warn>This is only available when using a user account.</warn> + * @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 {GuildResolvable} [options.guild] Limit the search to a specific guild + * @returns {Promise<Message[]>} + * @deprecated + * @example + * // Fetch mentions + * client.user.fetchMentions() + * .then(console.log) + * .catch(console.error); + * @example + * // Fetch mentions from a guild + * client.user.fetchMentions({ guild: '222078108977594368' }) + * .then(console.log) + * .catch(console.error); + */ + fetchMentions(options = {}) { + return this.client.rest.methods.fetchMentions(options); + } + + /** + * Send a friend request. + * <warn>This is only available when using a user account.</warn> + * @param {UserResolvable} user The user to send the friend request to + * @returns {Promise<User>} The user the friend request was sent to + * @deprecated + */ + addFriend(user) { + user = this.client.resolver.resolveUser(user); + return this.client.rest.methods.addFriend(user); + } + + /** + * Remove a friend. + * <warn>This is only available when using a user account.</warn> + * @param {UserResolvable} user The user to remove from your friends + * @returns {Promise<User>} The user that was removed + * @deprecated + */ + removeFriend(user) { + user = this.client.resolver.resolveUser(user); + return this.client.rest.methods.removeFriend(user); + } + + /** + * Creates a guild. + * <warn>This is only available to bots in less than 10 guilds and user accounts.</warn> + * @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<Guild>} The guild that was created + */ + createGuild(name, region, icon = null) { + if (typeof icon === 'string' && icon.startsWith('data:')) { + return this.client.rest.methods.createGuild({ name, icon, region }); + } else { + return this.client.resolver.resolveImage(icon).then(data => + this.client.rest.methods.createGuild({ name, icon: data, region }) + ); + } + } + + /** + * An object containing either a user or access token, and an optional nickname. + * @typedef {Object} GroupDMRecipientOptions + * @property {UserResolvable|Snowflake} [user] User to add to the Group DM + * (only available if a user is creating the DM) + * @property {string} [accessToken] Access token to use to add a user to the Group DM + * (only available if a bot is creating the DM) + * @property {string} [nick] Permanent nickname (only available if a bot is creating the DM) + */ + + /** + * Creates a Group DM. + * @param {GroupDMRecipientOptions[]} recipients The recipients + * @returns {Promise<GroupDMChannel>} + * @example + * // Create a Group DM with a token provided from OAuth + * client.user.createGroupDM([{ + * user: '66564597481480192', + * accessToken: token + * }]) + * .then(console.log) + * .catch(console.error); + */ + createGroupDM(recipients) { + return this.client.rest.methods.createGroupDM({ + recipients: recipients.map(u => this.client.resolver.resolveUserID(u.user)), + accessTokens: recipients.map(u => u.accessToken), + nicks: recipients.reduce((o, r) => { + if (r.nick) o[r.user ? r.user.id : r.id] = r.nick; + return o; + }, {}), + }); + } + + /** + * Accepts an invite to join a guild. + * <warn>This is only available when using a user account.</warn> + * @param {Invite|string} invite Invite or code to accept + * @returns {Promise<Guild>} Joined guild + * @deprecated + */ + acceptInvite(invite) { + return this.client.rest.methods.acceptInvite(invite); + } +} + +ClientUser.prototype.acceptInvite = + util.deprecate(ClientUser.prototype.acceptInvite, 'ClientUser#acceptInvite: userbot methods will be removed'); + +ClientUser.prototype.setGame = + util.deprecate(ClientUser.prototype.setGame, 'ClientUser#setGame: use ClientUser#setActivity instead'); + +ClientUser.prototype.addFriend = + util.deprecate(ClientUser.prototype.addFriend, 'ClientUser#addFriend: userbot methods will be removed'); + +ClientUser.prototype.removeFriend = + util.deprecate(ClientUser.prototype.removeFriend, 'ClientUser#removeFriend: userbot methods will be removed'); + +ClientUser.prototype.setPassword = + util.deprecate(ClientUser.prototype.setPassword, 'ClientUser#setPassword: userbot methods will be removed'); + +ClientUser.prototype.setEmail = + util.deprecate(ClientUser.prototype.setEmail, 'ClientUser#setEmail: userbot methods will be removed'); + +ClientUser.prototype.fetchMentions = + util.deprecate(ClientUser.prototype.fetchMentions, 'ClientUser#fetchMentions: userbot methods will be removed'); + +module.exports = ClientUser; diff --git a/node_modules/discord.js/src/structures/ClientUserChannelOverride.js b/node_modules/discord.js/src/structures/ClientUserChannelOverride.js new file mode 100644 index 0000000..93efa45 --- /dev/null +++ b/node_modules/discord.js/src/structures/ClientUserChannelOverride.js @@ -0,0 +1,30 @@ +const Constants = require('../util/Constants'); + +/** + * A wrapper around the ClientUser's channel overrides. + */ +class ClientUserChannelOverride { + constructor(data) { + this.patch(data); + } + + /** + * Patch the data contained in this class with new partial data. + * @param {Object} data Data to patch this with + * @returns {void} + * @private + */ + patch(data) { + for (const key of Object.keys(Constants.UserChannelOverrideMap)) { + const value = Constants.UserChannelOverrideMap[key]; + if (!data.hasOwnProperty(key)) continue; + if (typeof value === 'function') { + this[value.name] = value(data[key]); + } else { + this[value] = data[key]; + } + } + } +} + +module.exports = ClientUserChannelOverride; diff --git a/node_modules/discord.js/src/structures/ClientUserGuildSettings.js b/node_modules/discord.js/src/structures/ClientUserGuildSettings.js new file mode 100644 index 0000000..5a28747 --- /dev/null +++ b/node_modules/discord.js/src/structures/ClientUserGuildSettings.js @@ -0,0 +1,60 @@ +const Constants = require('../util/Constants'); +const Collection = require('../util/Collection'); +const ClientUserChannelOverride = require('./ClientUserChannelOverride'); + +/** + * A wrapper around the ClientUser's guild settings. + */ +class ClientUserGuildSettings { + constructor(data, client) { + /** + * The client that created the instance of the ClientUserGuildSettings + * @name ClientUserGuildSettings#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + /** + * The ID of the guild this settings are for + * @type {Snowflake} + */ + this.guildID = data.guild_id; + this.channelOverrides = new Collection(); + this.patch(data); + } + + /** + * Patch the data contained in this class with new partial data. + * @param {Object} data Data to patch this with + * @returns {void} + * @private + */ + patch(data) { + for (const key of Object.keys(Constants.UserGuildSettingsMap)) { + const value = Constants.UserGuildSettingsMap[key]; + if (!data.hasOwnProperty(key)) continue; + if (key === 'channel_overrides') { + for (const channel of data[key]) { + this.channelOverrides.set(channel.channel_id, + new ClientUserChannelOverride(channel)); + } + } else if (typeof value === 'function') { + this[value.name] = value(data[key]); + } else { + this[value] = data[key]; + } + } + } + + /** + * Update a specific property of the guild settings. + * @param {string} name Name of property + * @param {value} value Value to patch + * @returns {Promise<Object>} + */ + update(name, value) { + return this.client.rest.methods.patchClientUserGuildSettings(this.guildID, { [name]: value }); + } +} + +module.exports = ClientUserGuildSettings; diff --git a/node_modules/discord.js/src/structures/ClientUserSettings.js b/node_modules/discord.js/src/structures/ClientUserSettings.js new file mode 100644 index 0000000..6b18c03 --- /dev/null +++ b/node_modules/discord.js/src/structures/ClientUserSettings.js @@ -0,0 +1,80 @@ +const Constants = require('../util/Constants'); +const Util = require('../util/Util'); + +/** + * A wrapper around the ClientUser's settings. + */ +class ClientUserSettings { + constructor(user, data) { + this.user = user; + this.patch(data); + } + + /** + * Patch the data contained in this class with new partial data. + * @param {Object} data Data to patch this with + * @returns {void} + * @private + */ + patch(data) { + for (const key of Object.keys(Constants.UserSettingsMap)) { + const value = Constants.UserSettingsMap[key]; + if (!data.hasOwnProperty(key)) continue; + if (typeof value === 'function') { + this[value.name] = value(data[key]); + } else { + this[value] = data[key]; + } + } + } + + /** + * Update a specific property of of user settings. + * @param {string} name Name of property + * @param {*} value Value to patch + * @returns {Promise<Object>} + */ + update(name, value) { + return this.user.client.rest.methods.patchUserSettings({ [name]: value }); + } + + /** + * Sets the position at which this guild will appear in the Discord client. + * @param {Guild} guild The guild to move + * @param {number} position Absolute or relative position + * @param {boolean} [relative=false] Whether to position relatively or absolutely + * @returns {Promise<Guild>} + */ + setGuildPosition(guild, position, relative) { + const temp = Object.assign([], this.guildPositions); + Util.moveElementInArray(temp, guild.id, position, relative); + return this.update('guild_positions', temp).then(() => guild); + } + + /** + * Add a guild to the list of restricted guilds. + * @param {Guild} guild The guild to add + * @returns {Promise<Guild>} + */ + addRestrictedGuild(guild) { + const temp = Object.assign([], this.restrictedGuilds); + if (temp.includes(guild.id)) return Promise.reject(new Error('Guild is already restricted')); + temp.push(guild.id); + return this.update('restricted_guilds', temp).then(() => guild); + } + + /** + * Remove a guild from the list of restricted guilds. + * @param {Guild} guild The guild to remove + * @returns {Promise<Guild>} + */ + removeRestrictedGuild(guild) { + const temp = Object.assign([], this.restrictedGuilds); + const index = temp.indexOf(guild.id); + if (index < 0) return Promise.reject(new Error('Guild is not restricted')); + temp.splice(index, 1); + return this.update('restricted_guilds', temp).then(() => guild); + } +} + +module.exports = ClientUserSettings; 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..0a6a3d9 --- /dev/null +++ b/node_modules/discord.js/src/structures/DMChannel.js @@ -0,0 +1,76 @@ +const Channel = require('./Channel'); +const TextBasedChannel = require('./interfaces/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]); + + /** + * The ID of the last message in the channel, if one was sent + * @type {?Snowflake} + */ + this.lastMessageID = data.last_message_id; + + /** + * The timestamp when the last pinned message was pinned, if there was one + * @type {?number} + */ + this.lastPinTimestamp = data.last_pin_timestamp ? new Date(data.last_pin_timestamp).getTime() : null; + } + + /** + * 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 + /* eslint-disable no-empty-function */ + get lastPinAt() {} + send() {} + sendMessage() {} + sendEmbed() {} + sendFile() {} + sendFiles() {} + sendCode() {} + fetchMessage() {} + fetchMessages() {} + fetchPinnedMessages() {} + search() {} + startTyping() {} + stopTyping() {} + get typing() {} + get typingCount() {} + createCollector() {} + createMessageCollector() {} + awaitMessages() {} + // Doesn't work on DM channels; bulkDelete() {} + acknowledge() {} + _cacheMessage() {} +} + +TextBasedChannel.applyToClass(DMChannel, true, ['bulkDelete']); + +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..f514b81 --- /dev/null +++ b/node_modules/discord.js/src/structures/Emoji.js @@ -0,0 +1,273 @@ +const Constants = require('../util/Constants'); +const Collection = require('../util/Collection'); +const Permissions = require('../util/Permissions'); +const Snowflake = require('../util/Snowflake'); + +/** + * 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; + + /** + * Whether this emoji has been deleted + * @type {boolean} + */ + this.deleted = false; + + this.setup(data); + } + + setup(data) { + /** + * The ID of the emoji + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The name of the emoji + * @type {string} + */ + this.name = data.name; + + /** + * Whether or not this emoji requires colons surrounding it + * @type {boolean} + */ + this.requiresColons = data.require_colons; + + /** + * Whether this emoji is managed by an external service + * @type {boolean} + */ + this.managed = data.managed; + + /** + * Whether this emoji is animated + * @type {boolean} + */ + this.animated = data.animated; + + /** + * Whether this emoji is available + * @type {boolean} + * @name Emoji#available + */ + if (typeof data.available !== 'undefined') this.available = data.available; + + this._roles = data.roles; + } + + /** + * The timestamp the emoji was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * The time the emoji was created + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * Whether the emoji is deletable by the client user + * @type {boolean} + * @readonly + */ + get deletable() { + return !this.managed && this.guild.me.hasPermission(Permissions.FLAGS.MANAGE_EMOJIS); + } + + /** + * A collection of roles this emoji is active for (empty if all), mapped by role ID + * @type {Collection<Snowflake, Role>} + * @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.CDN(this.client.options.http.cdn).Emoji(this.id, this.animated ? 'gif' : 'png'); + } + + /** + * The identifier of this emoji, used for message reactions + * @type {string} + * @readonly + */ + get identifier() { + if (this.id) return `${this.name}:${this.id}`; + return encodeURIComponent(this.name); + } + + /** + * Data for editing an emoji. + * @typedef {Object} EmojiEditData + * @property {string} [name] The name of the emoji + * @property {Collection<Snowflake, Role>|Array<Snowflake|Role>} [roles] Roles to restrict emoji to + */ + + /** + * Edits the emoji. + * @param {EmojiEditData} data The new data for the emoji + * @param {string} [reason] Reason for editing this emoji + * @returns {Promise<Emoji>} + * @example + * // Edit an emoji + * emoji.edit({name: 'newemoji'}) + * .then(e => console.log(`Edited emoji ${e}`)) + * .catch(console.error); + */ + edit(data, reason) { + return this.client.rest.methods.updateEmoji(this, data, reason); + } + + /** + * Set the name of the emoji. + * @param {string} name The new name for the emoji + * @param {string} [reason] The reason for changing the emoji's name + * @returns {Promise<Emoji>} + */ + setName(name, reason) { + return this.edit({ name }, reason); + } + + /** + * Fetches the author for this emoji + * @returns {Promise<User>} + */ + fetchAuthor() { + if (this.managed) return Promise.reject(new Error('Emoji is managed and has no Author.')); + if (!this.guild.me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS)) { + return Promise.reject( + new Error(`Client must have Manage Emoji permission in guild ${this.guild} to see emoji authors.`) + ); + } + return this.client.rest.makeRequest('get', Constants.Endpoints.Guild(this.guild).Emoji(this.id), true) + .then(emoji => this.client.dataManager.newUser(emoji.user)); + } + + /** + * Add a role to the list of roles that can use this emoji. + * @param {Role} role The role to add + * @returns {Promise<Emoji>} + */ + addRestrictedRole(role) { + return this.addRestrictedRoles([role]); + } + + /** + * Add multiple roles to the list of roles that can use this emoji. + * @param {Role[]} roles Roles to add + * @returns {Promise<Emoji>} + */ + addRestrictedRoles(roles) { + const newRoles = new Collection(this.roles); + for (const role of roles) { + if (this.guild.roles.has(role.id)) newRoles.set(role.id, role); + } + return this.edit({ roles: newRoles }); + } + + /** + * Remove a role from the list of roles that can use this emoji. + * @param {Role} role The role to remove + * @returns {Promise<Emoji>} + */ + removeRestrictedRole(role) { + return this.removeRestrictedRoles([role]); + } + + /** + * Remove multiple roles from the list of roles that can use this emoji. + * @param {Role[]} roles Roles to remove + * @returns {Promise<Emoji>} + */ + removeRestrictedRoles(roles) { + const newRoles = new Collection(this.roles); + for (const role of roles) { + if (newRoles.has(role.id)) newRoles.delete(role.id); + } + return this.edit({ roles: newRoles }); + } + + + /** + * Deletes the emoji. + * @param {string} [reason] Reason for deleting the emoji + * @returns {Promise<Emoji>} + */ + delete(reason) { + return this.client.rest.methods.deleteEmoji(this, reason); + } + + /** + * 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() { + if (!this.id || !this.requiresColons) { + return this.name; + } + + return `<${this.animated ? 'a' : ''}:${this.name}:${this.id}>`; + } + + /** + * 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 + ); + } + } +} + +module.exports = Emoji; 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..0febf6c --- /dev/null +++ b/node_modules/discord.js/src/structures/GroupDMChannel.js @@ -0,0 +1,246 @@ +const Channel = require('./Channel'); +const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const Collection = require('../util/Collection'); +const Constants = require('../util/Constants'); + +/* +{ 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 this 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 the DM is managed by an application + * @type {boolean} + */ + this.managed = data.managed; + + /** + * Application ID of the application that made this Group DM, if applicable + * @type {?string} + */ + this.applicationID = data.application_id; + + if (data.nicks) { + /** + * Nicknames for group members + * @type {?Collection<Snowflake, string>} + */ + this.nicks = new Collection(data.nicks.map(n => [n.id, n.nick])); + } + + if (!this.recipients) { + /** + * A collection of the recipients of this DM, mapped by their ID + * @type {Collection<Snowflake, User>} + */ + 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); + } + } + + /** + * The ID of the last message in the channel, if one was sent + * @type {?Snowflake} + */ + this.lastMessageID = data.last_message_id; + + /** + * The timestamp when the last pinned message was pinned, if there was one + * @type {?number} + */ + this.lastPinTimestamp = data.last_pin_timestamp ? new Date(data.last_pin_timestamp).getTime() : null; + } + + /** + * The owner of this Group DM + * @type {User} + * @readonly + */ + get owner() { + return this.client.users.get(this.ownerID); + } + + /** + * The URL to this guild's icon + * @type {?string} + * @readonly + */ + get iconURL() { + if (!this.icon) return null; + return Constants.Endpoints.Channel(this).Icon(this.client.options.http.cdn, this.icon); + } + + edit(data) { + const _data = {}; + if (data.name) _data.name = data.name; + if (typeof data.icon !== 'undefined') _data.icon = data.icon; + return this.client.rest.methods.updateGroupDMChannel(this, _data); + } + + /** + * 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; + } + + /** + * Add a user to the DM + * @param {UserResolvable|string} accessTokenOrID Access token or user resolvable + * @param {string} [nick] Permanent nickname to give the user (only available if a bot is creating the DM) + * @returns {Promise<GroupDMChannel>} + */ + + addUser(accessTokenOrID, nick) { + return this.client.rest.methods.addUserToGroupDM(this, { + nick, + id: this.client.resolver.resolveUserID(accessTokenOrID), + accessToken: accessTokenOrID, + }); + } + + /** + * Set a new GroupDMChannel icon. + * @param {Base64Resolvable|BufferResolvable} icon The new icon of the group dm + * @returns {Promise<GroupDMChannel>} + * @example + * // Edit the group dm icon + * channel.setIcon('./icon.png') + * .then(updated => console.log('Updated the channel icon')) + * .catch(console.error); + */ + setIcon(icon) { + return this.client.resolver.resolveImage(icon).then(data => this.edit({ icon: data })); + } + + /** + * Sets a new name for this Group DM. + * @param {string} name New name for this Group DM + * @returns {Promise<GroupDMChannel>} + */ + setName(name) { + return this.edit({ name }); + } + + /** + * Removes a user from this Group DM. + * @param {UserResolvable} user User to remove + * @returns {Promise<GroupDMChannel>} + */ + removeUser(user) { + const id = this.client.resolver.resolveUserID(user); + return this.client.rest.methods.removeUserFromGroupDM(this, id); + } + + /** + * 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 + /* eslint-disable no-empty-function */ + get lastPinAt() {} + send() {} + sendMessage() {} + sendEmbed() {} + sendFile() {} + sendFiles() {} + sendCode() {} + fetchMessage() {} + fetchMessages() {} + fetchPinnedMessages() {} + search() {} + startTyping() {} + stopTyping() {} + get typing() {} + get typingCount() {} + createCollector() {} + createMessageCollector() {} + awaitMessages() {} + // Doesn't work on Group DMs; bulkDelete() {} + acknowledge() {} + _cacheMessage() {} +} + +TextBasedChannel.applyToClass(GroupDMChannel, true, ['bulkDelete']); + +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..69acfe1 --- /dev/null +++ b/node_modules/discord.js/src/structures/Guild.js @@ -0,0 +1,1711 @@ +const util = require('util'); +const Long = require('long'); +const User = require('./User'); +const Role = require('./Role'); +const Emoji = require('./Emoji'); +const Presence = require('./Presence').Presence; +const GuildMember = require('./GuildMember'); +const Integration = require('./Integration'); +const Constants = require('../util/Constants'); +const Collection = require('../util/Collection'); +const Util = require('../util/Util'); +const Snowflake = require('../util/Snowflake'); +const SystemChannelFlags = require('../util/SystemChannelFlags'); + +/** + * Represents a guild (or a server) on Discord. + * <info>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`.</info> + */ +class Guild { + constructor(client, data) { + /** + * The client that created the instance of 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<Snowflake, GuildMember>} + */ + 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<Snowflake, GuildChannel>} + */ + 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<Snowflake, Role>} + */ + this.roles = new Collection(); + + /** + * A collection of presences in this guild + * @type {Collection<Snowflake, Presence>} + */ + this.presences = new Collection(); + + /** + * Whether the bot has been removed from the guild + * @type {boolean} + */ + this.deleted = false; + + 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 {Snowflake} + */ + this.id = data.id; + } else { + this.setup(data); + if (!data.channels) this.available = false; + } + } + + /* eslint-disable complexity */ + /** + * 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 + * @type {?string} + */ + this.icon = data.icon; + + /** + * The hash of the guild splash image (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 + * @type {number} + */ + this.memberCount = data.member_count || this.memberCount; + + /** + * Whether the guild is "large" (has more than 250 members) + * @type {boolean} + */ + this.large = Boolean('large' in data ? 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 {?Snowflake} + */ + this.applicationID = data.application_id; + + /** + * 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; + + /** + * The ID of the system channel + * @type {?Snowflake} + */ + this.systemChannelID = data.system_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 explicit content filter level of the guild + * @type {number} + */ + this.explicitContentFilter = data.explicit_content_filter; + + /** + * The required MFA level for the guild + * @type {number} + */ + this.mfaLevel = data.mfa_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; + + /** + * The value set for a guild's default message notifications + * @type {DefaultMessageNotifications|number} + */ + this.defaultMessageNotifications = Constants.DefaultMessageNotifications[data.default_message_notifications] || + data.default_message_notifications; + + /** + * The value for the guild's system channel flags + * @type {Readonly<SystemChannelFlags>} + */ + this.systemChannelFlags = new SystemChannelFlags(data.system_channel_flags).freeze(); + + /** + * The type of premium tier: + * * 0: NONE + * * 1: TIER_1 + * * 2: TIER_2 + * * 3: TIER_3 + * @typedef {number} PremiumTier + */ + + /** + * The premium tier on this guild + * @type {PremiumTier} + */ + this.premiumTier = data.premium_tier; + + /** + * The total number of users currently boosting this server + * @type {?number} + * @name Guild#premiumSubscriptionCount + */ + if (typeof data.premium_subscription_count !== 'undefined') { + this.premiumSubscriptionCount = data.premium_subscription_count; + } + + /** + * The hash of the guild banner + * @type {?string} + */ + this.banner = data.banner; + + /** + * The description of the guild, if any + * @type {?string} + */ + this.description = data.description; + + /** + * The embed channel ID, if enabled + * @type {?string} + * @name Guild#embedChannelID + */ + if (typeof data.embed_channel_id !== 'undefined') this.embedChannelID = data.embed_channel_id; + + /** + * The maximum amount of members the guild can have + * <info>You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter</info> + * @type {?number} + * @name Guild#maximumMembers + */ + if (typeof data.max_members !== 'undefined') this.maximumMembers = data.max_members || 250000; + + /** + * The maximum amount of presences the guild can have + * <info>You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter</info> + * @type {?number} + * @name Guild#maximumPresences + */ + if (typeof data.max_presences !== 'undefined') this.maximumPresences = data.max_presences || 5000; + + /** + * Whether widget images are enabled on this guild + * @type {?boolean} + * @name Guild#widgetEnabled + */ + if (typeof data.widget_enabled !== 'undefined') this.widgetEnabled = data.widget_enabled; + + /** + * The widget channel ID, if enabled + * @type {?string} + * @name Guild#widgetChannelID + */ + if (typeof data.widget_channel_id !== 'undefined') this.widgetChannelID = data.widget_channel_id; + + /** + * The vanity URL code of the guild, if any + * @type {?string} + */ + this.vanityURLCode = data.vanity_url_code; + + this.id = data.id; + this.available = !data.unavailable; + this.features = data.features || this.features || []; + + /** + * The ID of the rules channel for the guild + * <info>This is only available on guilds with the `PUBLIC` feature</info> + * @type {?Snowflake} + */ + this.rulesChannelID = data.rules_channel_id; + + /** + * The ID of the public updates channel for the guild + * <info>This is only available on guilds with the `PUBLIC` feature</info> + * @type {?Snowflake} + */ + this.publicUpdatesChannelID = data.public_updates_channel_id; + + 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 {Snowflake} + */ + 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); + const voiceChannel = this.channels.get(voiceState.channel_id); + if (member && voiceChannel) { + member.serverMute = voiceState.mute; + member.serverDeaf = voiceState.deaf; + member.selfMute = voiceState.self_mute; + member.selfDeaf = voiceState.self_deaf; + member.selfStream = voiceState.self_stream || false; + member.voiceSessionID = voiceState.session_id; + member.voiceChannelID = voiceState.channel_id; + voiceChannel.members.set(member.user.id, member); + } + } + } + + if (!this.emojis) { + /** + * A collection of emojis that are in this guild + * The key is the emoji's ID, the value is the emoji + * @type {Collection<Snowflake, Emoji>} + */ + this.emojis = new Collection(); + for (const emoji of data.emojis) this.emojis.set(emoji.id, new Emoji(this, emoji)); + } else { + this.client.actions.GuildEmojisUpdate.handle({ + guild_id: this.id, + emojis: data.emojis, + }); + } + } + + /** + * The timestamp the guild was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * The time the guild was created + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * Embed channel for this guild + * @type {?TextChannel} + * @readonly + */ + get embedChannel() { + return this.channels.get(this.embedChannelID) || null; + } + + /** + * Widget channel for this guild + * @type {?TextChannel} + * @readonly + */ + get widgetChannel() { + return this.channels.get(this.widgetChannelID) || null; + } + + /** + * The time the client user joined the guild + * @type {Date} + * @readonly + */ + get joinedAt() { + return new Date(this.joinedTimestamp); + } + + /** + * If this guild is verified + * @type {boolean} + * @readonly + */ + get verified() { + return this.features.includes('VERIFIED'); + } + + /** + * The URL to this guild's icon + * @type {?string} + * @readonly + */ + get iconURL() { + if (!this.icon) return null; + return Constants.Endpoints.Guild(this).Icon(this.client.options.http.cdn, this.icon); + } + + /** + * The URL to this guild's banner. + * @type {?string} + * @readonly + */ + get bannerURL() { + if (!this.banner) return null; + return Constants.Endpoints.Guild(this).Banner(this.client.options.http.cdn, this.banner); + } + + /** + * The acronym that shows up in place of a guild icon. + * @type {string} + * @readonly + */ + get nameAcronym() { + return this.name.replace(/\w+/g, name => name[0]).replace(/\s/g, ''); + } + + /** + * The URL to this guild's splash + * @type {?string} + * @readonly + */ + get splashURL() { + if (!this.splash) return null; + return Constants.Endpoints.Guild(this).Splash(this.client.options.http.cdn, this.splash); + } + + /** + * The owner of the guild + * @type {?GuildMember} + * @readonly + */ + get owner() { + return this.members.get(this.ownerID); + } + + /** + * AFK voice channel for this guild + * @type {?VoiceChannel} + * @readonly + */ + get afkChannel() { + return this.client.channels.get(this.afkChannelID) || null; + } + + /** + * System channel for this guild + * @type {?GuildChannel} + * @readonly + */ + get systemChannel() { + return this.client.channels.get(this.systemChannelID) || null; + } + + /** + * 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 position of this guild + * <warn>This is only available when using a user account.</warn> + * @type {?number} + * @readonly + * @deprecated + */ + get position() { + if (this.client.user.bot) return null; + if (!this.client.user.settings.guildPositions) return null; + return this.client.user.settings.guildPositions.indexOf(this.id); + } + + /** + * Whether the guild is muted + * <warn>This is only available when using a user account.</warn> + * @type {?boolean} + * @readonly + * @deprecated + */ + get muted() { + if (this.client.user.bot) return null; + try { + return this.client.user.guildSettings.get(this.id).muted; + } catch (err) { + return false; + } + } + + /** + * The type of message that should notify you + * <warn>This is only available when using a user account.</warn> + * @type {?MessageNotificationType} + * @readonly + * @deprecated + */ + get messageNotifications() { + if (this.client.user.bot) return null; + try { + return this.client.user.guildSettings.get(this.id).messageNotifications; + } catch (err) { + return null; + } + } + + /** + * Whether to receive mobile push notifications + * <warn>This is only available when using a user account.</warn> + * @type {?boolean} + * @readonly + * @deprecated + */ + get mobilePush() { + if (this.client.user.bot) return null; + try { + return this.client.user.guildSettings.get(this.id).mobilePush; + } catch (err) { + return false; + } + } + + /** + * Whether to suppress everyone messages + * <warn>This is only available when using a user account.</warn> + * @type {?boolean} + * @readonly + * @deprecated + */ + get suppressEveryone() { + if (this.client.user.bot) return null; + try { + return this.client.user.guildSettings.get(this.id).suppressEveryone; + } catch (err) { + return null; + } + } + + /** + * The `@everyone` role of the guild + * @type {Role} + * @readonly + */ + get defaultRole() { + return this.roles.get(this.id); + } + + /** + * Rules channel for this guild + * <info>This is only available on guilds with the `PUBLIC` feature</info> + * @type {?TextChannel} + * @readonly + */ + get rulesChannel() { + return this.client.channels.get(this.rulesChannelID) || null; + } + + /** + * Public updates channel for this guild + * <info>This is only available on guilds with the `PUBLIC` feature</info> + * @type {?TextChannel} + * @readonly + */ + get publicUpdatesChannel() { + return this.client.channels.get(this.publicUpdatesChannelID) || null; + } + + /** + * The client user as a GuildMember of this guild + * @type {?GuildMember} + * @readonly + */ + get me() { + return this.members.get(this.client.user.id); + } + + /** + * Fetches a collection of roles in the current guild sorted by position + * @type {Collection<Snowflake, Role>} + * @readonly + * @private + */ + get _sortedRoles() { + return this._sortPositionWithID(this.roles); + } + + /** + * 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); + } + + /** + * Fetches this guild. + * @returns {Promise<Guild>} + */ + fetch() { + return this.client.rest.methods.getGuild(this).then(data => { + this.setup(data); + + return this; + }); + } + + /** + * An object containing information about a guild member's ban. + * @typedef {Object} BanInfo + * @property {User} user User that was banned + * @property {?string} reason Reason the user was banned + */ + + /** + * Fetch a ban for a user. + * @returns {Promise<BanInfo>} + * @param {UserResolvable} user The user to fetch the ban for + * @example + * // Get ban + * guild.fetchBan(message.author) + * .then(({ user, reason }) => console.log(`${user.tag} was banned for the reason: ${reason}.`)) + * .catch(console.error); + */ + fetchBan(user) { + return this.client.rest.methods.getGuildBan(this, user); + } + + /** + * Fetch a collection of banned users in this guild. + * @returns {Promise<Collection<Snowflake, User|BanInfo>>} + * @param {boolean} [withReasons=false] Whether or not to include the ban reason(s) + * @example + * // Fetch bans in guild + * guild.fetchBans() + * .then(bans => console.log(`This guild has ${bans.size} bans`)) + * .catch(console.error); + */ + fetchBans(withReasons = false) { + if (withReasons) return this.client.rest.methods.getGuildBans(this); + return this.client.rest.methods.getGuildBans(this) + .then(bans => { + const users = new Collection(); + for (const ban of bans.values()) users.set(ban.user.id, ban.user); + return users; + }); + } + + /** + * Fetches a collection of integrations to this guild. + * Resolves with a collection mapping integrations by their ids. + * @returns {Promise<Collection<string, Integration>>} + * @example + * // Fetch integrations + * guild.fetchIntegrations() + * .then(integrations => console.log(`Fetched ${integrations.size} integrations`)) + * .catch(console.error); + */ + fetchIntegrations() { + return this.client.rest.methods.getIntegrations(this).then(data => + data.reduce((collection, integration) => + collection.set(integration.id, new Integration(this.client, integration, this)), + new Collection()) + ); + } + + /** + * The data for creating an integration. + * @typedef {Object} IntegrationData + * @property {string} id The integration id + * @property {string} type The integration type + */ + + /** + * Creates an integration by attaching an integration object + * @param {IntegrationData} data The data for thes integration + * @param {string} reason Reason for creating the integration + * @returns {Promise<Guild>} + */ + createIntegration(data, reason) { + return this.client.rest.methods.createIntegration(this, data, reason) + .then(() => this); + } + + /** + * Fetch a collection of invites to this guild. + * Resolves with a collection mapping invites by their codes. + * @returns {Promise<Collection<string, Invite>>} + * @example + * // Fetch invites + * guild.fetchInvites() + * .then(invites => console.log(`Fetched ${invites.size} invites`)) + * .catch(console.error); + * @example + * // Fetch invite creator by their id + * guild.fetchInvites() + * .then(invites => console.log(invites.find(invite => invite.inviter.id === '84484653687267328'))) + * .catch(console.error); + */ + fetchInvites() { + return this.client.rest.methods.getGuildInvites(this); + } + + /** + * Fetches the vanity url invite code to this guild. + * Resolves with a string matching the vanity url invite code, not the full url. + * @returns {Promise<string>} + * @example + * // Fetch invites + * guild.fetchVanityCode() + * .then(code => { + * console.log(`Vanity URL: https://discord.gg/${code}`); + * }) + * .catch(console.error); + */ + fetchVanityCode() { + if (!this.features.includes('VANITY_URL')) { + return Promise.reject(new Error('This guild does not have the VANITY_URL feature enabled.')); + } + return this.client.rest.methods.getGuildVanityCode(this); + } + + + /** + * Fetch all webhooks for the guild. + * @returns {Promise<Collection<Snowflake, Webhook>>} + * @example + * // Fetch webhooks + * guild.fetchWebhooks() + * .then(webhooks => console.log(`Fetched ${webhooks.size} webhooks`)) + * .catch(console.error); + */ + fetchWebhooks() { + return this.client.rest.methods.getGuildWebhooks(this); + } + + /** + * Fetch available voice regions. + * @returns {Promise<Collection<string, VoiceRegion>>} + * @example + * // Fetch voice regions + * guild.fetchVoiceRegions() + * .then(console.log) + * .catch(console.error); + */ + fetchVoiceRegions() { + return this.client.rest.methods.fetchVoiceRegions(this.id); + } + + /** + * The Guild Embed object + * @typedef {Object} GuildEmbedData + * @property {boolean} enabled Whether the embed is enabled + * @property {?ChannelResolvable} channel The embed channel + */ + + /** + * Fetches the guild embed. + * @returns {Promise<GuildEmbedData>} + * @example + * // Fetches the guild embed + * guild.fetchEmbed() + * .then(embed => console.log(`The embed is ${embed.enabled ? 'enabled' : 'disabled'}`)) + * .catch(console.error); + */ + fetchEmbed() { + return this.client.rest.methods.fetchEmbed(this.id); + } + + /** + * Fetch audit logs for this guild. + * @param {Object} [options={}] Options for fetching audit logs + * @param {Snowflake|GuildAuditLogsEntry} [options.before] Limit to entries from before specified entry + * @param {Snowflake|GuildAuditLogsEntry} [options.after] Limit to entries from after specified entry + * @param {number} [options.limit] Limit number of entries + * @param {UserResolvable} [options.user] Only show entries involving this user + * @param {AuditLogAction} [options.type] Only show entries involving this action type + * @returns {Promise<GuildAuditLogs>} + * @example + * // Output audit log entries + * guild.fetchAuditLogs() + * .then(audit => console.log(audit.entries.first())) + * .catch(console.error); + */ + fetchAuditLogs(options) { + return this.client.rest.methods.getGuildAuditLogs(this, options); + } + + /** + * Adds a user to the guild using OAuth2. Requires the `CREATE_INSTANT_INVITE` permission. + * @param {UserResolvable} user User to add to the guild + * @param {Object} options Options for the addition + * @param {string} options.accessToken An OAuth2 access token for the user with the `guilds.join` scope granted to the + * bot's application + * @param {string} [options.nick] Nickname to give the member (requires `MANAGE_NICKNAMES`) + * @param {Collection<Snowflake, Role>|Role[]|Snowflake[]} [options.roles] Roles to add to the member + * (requires `MANAGE_ROLES`) + * @param {boolean} [options.mute] Whether the member should be muted (requires `MUTE_MEMBERS`) + * @param {boolean} [options.deaf] Whether the member should be deafened (requires `DEAFEN_MEMBERS`) + * @returns {Promise<GuildMember>} + */ + addMember(user, options) { + user = this.client.resolver.resolveUserID(user); + if (this.members.has(user)) return Promise.resolve(this.members.get(user)); + return this.client.rest.methods.putGuildMember(this, user, options); + } + + /** + * Fetch a single guild member from a user. + * @param {UserResolvable} user The user to fetch the member for + * @param {boolean} [cache=true] Insert the member into the members cache + * @returns {Promise<GuildMember>} + * @example + * // Fetch a guild member + * guild.fetchMember(message.author) + * .then(console.log) + * .catch(console.error); + */ + fetchMember(user, cache = true) { + const userID = this.client.resolver.resolveUserID(user); + if (!userID) return Promise.reject(new Error('Invalid id provided.')); + const member = this.members.get(userID); + if (member && member.joinedTimestamp) return Promise.resolve(member); + return this.client.rest.methods.getGuildMember(this, userID, cache); + } + + /** + * 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=''] Limit fetch to members with similar usernames + * @param {number} [limit=0] Maximum number of members to request + * @returns {Promise<Guild>} + * @example + * // Fetch guild members + * guild.fetchMembers() + * .then(console.log) + * .catch(console.error); + * @example + * // Fetches a maximum of 1 member with the given query + * guild.fetchMembers('hydrabolt', 1) + * .then(console.log) + * .catch(console.error); + */ + fetchMembers(query = '', limit = 0) { + return new Promise((resolve, reject) => { + if (this.memberCount === this.members.size) { + resolve(this); + return; + } + this.client.ws.send({ + op: Constants.OPCodes.REQUEST_GUILD_MEMBERS, + d: { + guild_id: this.id, + query, + limit, + }, + }); + const handler = (members, guild) => { + if (guild.id !== this.id) return; + if (this.memberCount === this.members.size || members.length < 1000) { + this.client.removeListener(Constants.Events.GUILD_MEMBERS_CHUNK, handler); + resolve(this); + } + }; + this.client.on(Constants.Events.GUILD_MEMBERS_CHUNK, handler); + this.client.setTimeout(() => reject(new Error('Members didn\'t arrive in time.')), 120 * 1000); + }); + } + + /** + * Performs a search within the entire guild. + * <warn>This is only available when using a user account.</warn> + * @param {MessageSearchOptions} [options={}] Options to pass to the search + * @returns {Promise<MessageSearchResult>} + * @deprecated + * @example + * guild.search({ + * content: 'discord.js', + * before: '2016-11-17' + * }) + * .then(res => { + * const hit = res.messages[0].find(m => m.hit).content; + * console.log(`I found: **${hit}**, total results: ${res.totalResults}`); + * }) + * .catch(console.error); + */ + search(options = {}) { + return this.client.rest.methods.search(this, options); + } + + /** + * 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 {number} [explicitContentFilter] The level of the explicit content filter + * @property {ChannelResolvable} [afkChannel] The AFK channel of the guild + * @property {ChannelResolvable} [systemChannel] The system channel of the guild + * @property {number} [afkTimeout] The AFK timeout of the guild + * @property {Base64Resolvable} [icon] The icon of the guild + * @property {Base64Resolvable} [banner] The banner of the guild + * @property {GuildMemberResolvable} [owner] The owner of the guild + * @property {Base64Resolvable} [splash] The splash screen of the guild + * @property {SystemChannelFlagsResolvable} [systemChannelFlags] The system channel flags of the guild + */ + + /** + * Updates the guild with new information - e.g. a new name. + * @param {GuildEditData} data The data to update the guild with + * @param {string} [reason] Reason for editing the guild + * @returns {Promise<Guild>} + * @example + * // Set the guild name and region + * guild.edit({ + * name: 'Discord Guild', + * region: 'london', + * }) + * .then(g => console.log(`Changed guild name to ${g} and region to ${g.region}`)) + * .catch(console.error); + */ + edit(data, reason) { + const _data = {}; + if (data.name) _data.name = data.name; + if (data.region) _data.region = data.region; + if (typeof data.verificationLevel !== 'undefined') _data.verification_level = Number(data.verificationLevel); + if (typeof data.afkChannel !== 'undefined') { + _data.afk_channel_id = this.client.resolver.resolveChannelID(data.afkChannel); + } + if (typeof data.systemChannel !== 'undefined') { + _data.system_channel_id = this.client.resolver.resolveChannelID(data.systemChannel); + } + if (data.afkTimeout) _data.afk_timeout = Number(data.afkTimeout); + if (typeof data.icon !== 'undefined') _data.icon = data.icon; + if (data.owner) _data.owner_id = this.client.resolver.resolveUser(data.owner).id; + if (typeof data.splash !== 'undefined') _data.splash = data.splash; + if (typeof data.banner !== 'undefined') _data.banner = data.banner; + if (typeof data.explicitContentFilter !== 'undefined') { + _data.explicit_content_filter = Number(data.explicitContentFilter); + } + if (typeof data.defaultMessageNotifications !== 'undefined') { + _data.default_message_notifications = typeof data.defaultMessageNotifications === 'string' ? + Constants.DefaultMessageNotifications.indexOf(data.defaultMessageNotifications) : + Number(data.defaultMessageNotifications); + } + if (typeof data.systemChannelFlags !== 'undefined') { + _data.system_channel_flags = SystemChannelFlags.resolve(data.systemChannelFlags); + } + return this.client.rest.methods.updateGuild(this, _data, reason); + } + + /** + * Sets a new guild banner. + * @param {BufferResolvable|Base64Resolvable} banner The new banner of the guild + * @param {string} [reason] Reason for changing the guild's banner + * @returns {Guild} + */ + setBanner(banner, reason) { + return this.client.resolver.resolveImage(banner).then(data => this.edit({ banner: data }, reason)); + } + + /** + * Edit the level of the explicit content filter. + * @param {number} explicitContentFilter The new level of the explicit content filter + * @param {string} [reason] Reason for changing the level of the guild's explicit content filter + * @returns {Promise<Guild>} + */ + setExplicitContentFilter(explicitContentFilter, reason) { + return this.edit({ explicitContentFilter }, reason); + } + + /** + * Edits the setting of the default message notifications of the guild. + * @param {DefaultMessageNotifications|number} defaultMessageNotifications + * The new setting for the default message notifications + * @param {string} [reason] Reason for changing the setting of the default message notifications + * @returns {Promise<Guild>} + */ + setDefaultMessageNotifications(defaultMessageNotifications, reason) { + return this.edit({ defaultMessageNotifications }, reason); + } + + /** + * Edits the flags of the default message notifications of the guild. + * @param {SystemChannelFlagsResolvable} systemChannelFlags The new flags for the default message notifications + * @param {string} [reason] Reason for changing the flags of the default message notifications + * @returns {Promise<Guild>} + */ + setSystemChannelFlags(systemChannelFlags, reason) { + return this.edit({ systemChannelFlags }, reason); + } + + /** + * Edit the name of the guild. + * @param {string} name The new name of the guild + * @param {string} [reason] Reason for changing the guild's name + * @returns {Promise<Guild>} + * @example + * // Edit the guild name + * guild.setName('Discord Guild') + * .then(g => console.log(`Updated guild name to ${g}`)) + * .catch(console.error); + */ + setName(name, reason) { + return this.edit({ name }, reason); + } + + /** + * Edit the region of the guild. + * @param {string} region The new region of the guild + * @param {string} [reason] Reason for changing the guild's region + * @returns {Promise<Guild>} + * @example + * // Edit the guild region + * guild.setRegion('london') + * .then(g => console.log(`Updated guild region to ${g.region}`)) + * .catch(console.error); + */ + setRegion(region, reason) { + return this.edit({ region }, reason); + } + + /** + * Edit the verification level of the guild. + * @param {number} verificationLevel The new verification level of the guild + * @param {string} [reason] Reason for changing the guild's verification level + * @returns {Promise<Guild>} + * @example + * // Edit the guild verification level + * guild.setVerificationLevel(1) + * .then(g => console.log(`Updated guild verification level to ${g.verificationLevel}`)) + * .catch(console.error); + */ + setVerificationLevel(verificationLevel, reason) { + return this.edit({ verificationLevel }, reason); + } + + /** + * Edit the AFK channel of the guild. + * @param {ChannelResolvable} afkChannel The new AFK channel + * @param {string} [reason] Reason for changing the guild's AFK channel + * @returns {Promise<Guild>} + * @example + * // Edit the guild AFK channel + * guild.setAFKChannel(channel) + * .then(g => console.log(`Updated guild AFK channel to ${g.afkChannel.name}`)) + * .catch(console.error); + */ + setAFKChannel(afkChannel, reason) { + return this.edit({ afkChannel }, reason); + } + + /** + * Edit the system channel of the guild. + * @param {ChannelResolvable} systemChannel The new system channel + * @param {string} [reason] Reason for changing the guild's system channel + * @returns {Promise<Guild>} + */ + setSystemChannel(systemChannel, reason) { + return this.edit({ systemChannel }, reason); + } + + /** + * Edit the AFK timeout of the guild. + * @param {number} afkTimeout The time in seconds that a user must be idle to be considered AFK + * @param {string} [reason] Reason for changing the guild's AFK timeout + * @returns {Promise<Guild>} + * @example + * // Edit the guild AFK channel + * guild.setAFKTimeout(60) + * .then(g => console.log(`Updated guild AFK timeout to ${g.afkTimeout}`)) + * .catch(console.error); + */ + setAFKTimeout(afkTimeout, reason) { + return this.edit({ afkTimeout }, reason); + } + + /** + * Set a new guild icon. + * @param {Base64Resolvable|BufferResolvable} icon The new icon of the guild + * @param {string} [reason] Reason for changing the guild's icon + * @returns {Promise<Guild>} + * @example + * // Edit the guild icon + * guild.setIcon('./icon.png') + * .then(console.log) + * .catch(console.error); + */ + setIcon(icon, reason) { + return this.client.resolver.resolveImage(icon).then(data => this.edit({ icon: data, reason })); + } + + /** + * Sets a new owner of the guild. + * @param {GuildMemberResolvable} owner The new owner of the guild + * @param {string} [reason] Reason for setting the new owner + * @returns {Promise<Guild>} + * @example + * // Edit the guild owner + * guild.setOwner(guild.members.first()) + * .then(g => console.log(`Updated the guild owner to ${g.owner.displayName}`)) + * .catch(console.error); + */ + setOwner(owner, reason) { + return this.edit({ owner }, reason); + } + + /** + * Set a new guild splash screen. + * @param {BufferResolvable|Base64Resolvable} splash The new splash screen of the guild + * @param {string} [reason] Reason for changing the guild's splash screen + * @returns {Promise<Guild>} + * @example + * // Edit the guild splash + * guild.setSplash('./splash.png') + * .then(console.log) + * .catch(console.error); + */ + setSplash(splash) { + return this.client.resolver.resolveImage(splash).then(data => this.edit({ splash: data })); + } + + /** + * Sets the position of the guild in the guild listing. + * <warn>This is only available when using a user account.</warn> + * @param {number} position Absolute or relative position + * @param {boolean} [relative=false] Whether to position relatively or absolutely + * @returns {Promise<Guild>} + * @deprecated + */ + setPosition(position, relative) { + if (this.client.user.bot) { + return Promise.reject(new Error('Setting guild position is only available for user accounts')); + } + return this.client.user.settings.setGuildPosition(this, position, relative); + } + + /** + * Marks all messages in this guild as read. + * <warn>This is only available when using a user account.</warn> + * @returns {Promise<Guild>} + * @deprecated + */ + acknowledge() { + return this.client.rest.methods.ackGuild(this); + } + + /** + * Allow direct messages from guild members. + * <warn>This is only available when using a user account.</warn> + * @param {boolean} allow Whether to allow direct messages + * @returns {Promise<Guild>} + * @deprecated + */ + allowDMs(allow) { + const settings = this.client.user.settings; + if (allow) return settings.removeRestrictedGuild(this); + else return settings.addRestrictedGuild(this); + } + + /** + * Bans a user from the guild. + * @param {UserResolvable} user The user to ban + * @param {Object|number|string} [options] Ban options. If a number, the number of days to delete messages for, if a + * string, the ban reason. Supplying an object allows you to do both. + * @param {number} [options.days=0] Number of days of messages to delete + * @param {string} [options.reason] Reason for banning + * @returns {Promise<GuildMember|User|string>} 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 by ID + * guild.ban('some user ID') + * .then(user => console.log(`Banned ${user.username || user.id || user} from ${guild}`)) + * .catch(console.error); + * @example + * // Ban a user by object with reason and days + * guild.ban(user, { days: 7, reason: 'He needed to go' }) + * .then(console.log) + * .catch(console.error); + */ + ban(user, options = {}) { + if (typeof options === 'number') { + options = { reason: null, 'delete-message-days': options }; + } else if (typeof options === 'string') { + options = { reason: options, 'delete-message-days': 0 }; + } + if (options.days) options['delete-message-days'] = options.days; + return this.client.rest.methods.banGuildMember(this, user, options); + } + + /** + * Unbans a user from the guild. + * @param {UserResolvable} user The user to unban + * @param {string} [reason] Reason for unbanning the user + * @returns {Promise<User>} + * @example + * // Unban a user by ID (or with a user/guild member object) + * guild.unban('some user ID') + * .then(user => console.log(`Unbanned ${user.username} from ${guild}`)) + * .catch(console.error); + */ + unban(user, reason) { + return this.client.rest.methods.unbanGuildMember(this, user, reason); + } + + /** + * 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 + * @param {string} [reason] Reason for this prune + * @returns {Promise<number>} 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, reason) { + if (typeof days !== 'number') throw new TypeError('Days must be a number.'); + return this.client.rest.methods.pruneGuildMembers(this, days, dry, reason); + } + + /** + * Syncs this guild (already done automatically every 30 seconds). + * <warn>This is only available when using a user account.</warn> + * @deprecated + */ + sync() { + if (!this.client.user.bot) this.client.syncGuilds([this]); + } + + /** + * Overwrites to use when creating a channel or replacing overwrites + * @typedef {Object} ChannelCreationOverwrites + * @property {PermissionResolvable} [allow] The permissions to allow + * @property {PermissionResolvable} [allowed] The permissions to allow + * **(deprecated)** + * @property {PermissionResolvable} [deny] The permissions to deny + * @property {PermissionResolvable} [denied] The permissions to deny + * **(deprecated)** + * @property {GuildMemberResolvable|RoleResolvable} id Member or role this overwrite is for + */ + + /** + * Creates a new channel in the guild. + * @param {string} name The name of the new channel + * @param {string|ChannelData} [typeOrOptions='text'] + * The type of the new channel, one of `text`, `voice`, `category`, `news`, or `store`. **(deprecated, use options)** + * Alternatively options for the new channel, overriding the following parameters. + * @param {ChannelCreationOverwrites[]|Collection<Snowflake, PermissionOverwrites>} [permissionOverwrites] + * Permission overwrites **(deprecated, use options)** + * @param {string} [reason] Reason for creating this channel **(deprecated, use options)** + * @returns {Promise<CategoryChannel|TextChannel|VoiceChannel>} + * @example + * // Create a new text channel + * guild.createChannel('new-general', { type: 'text' }) + * .then(console.log) + * .catch(console.error); + * @example + * // Create a new category channel with permission overwrites + * guild.createChannel('new-category', { + * type: 'category', + * permissionOverwrites: [{ + * id: guild.id, + * deny: ['MANAGE_MESSAGES'], + * allow: ['SEND_MESSAGES'] + * }] + * }) + * .then(console.log) + * .catch(console.error); + */ + createChannel(name, typeOrOptions, permissionOverwrites, reason) { + if (!typeOrOptions || (typeof typeOrOptions === 'string')) { + if (typeOrOptions) { + process.emitWarning( + 'Guild#createChannel: Create channels with an options object instead of separate parameters', + 'DeprecationWarning' + ); + } + typeOrOptions = { + type: typeOrOptions, + permissionOverwrites, + reason, + }; + } + return this.client.rest.methods.createChannel(this, name, typeOrOptions); + } + + /** + * The data needed for updating a channel's position. + * @typedef {Object} ChannelPosition + * @property {ChannelResolvable} channel Channel to update + * @property {number} position New position for the channel + */ + + /** + * Batch-updates the guild's channels' positions. + * @param {ChannelPosition[]} channelPositions Channel positions to update + * @returns {Promise<Guild>} + * @example + * guild.updateChannels([{ channel: channelID, position: newChannelIndex }]) + * .then(g => console.log(`Updated channel positions for ${g}`)) + * .catch(console.error); + */ + setChannelPositions(channelPositions) { + channelPositions = channelPositions.map(({ channel, position }) => ({ id: channel.id || channel, position })); + return this.client.rest.methods.setChannelPositions(this.id, channelPositions); + } + + /** + * The data needed for updating a role's position. + * @typedef {Object} RolePosition + * @property {RoleResolvable} role Role to update + * @property {number} position New position for the role + */ + + /** + * Batch-updates the guild's role's positions. + * @param {RolePosition[]} rolePositions Role positions to update + * @returns {Promise<Guild>} + */ + setRolePositions(rolePositions) { + rolePositions = rolePositions.map(({ role, position }) => ({ id: role.id || role, position })); + return this.client.rest.methods.setRolePositions(this.id, rolePositions); + } + + /** + * Edits the guild's embed. + * @param {GuildEmbedData} embed The embed for the guild + * @param {string} [reason] Reason for changing the guild's embed + * @returns {Promise<Guild>} + */ + setEmbed(embed, reason) { + return this.client.rest.methods.updateEmbed(this.id, embed, reason) + .then(() => this); + } + + /** + * Creates a new role in the guild with given information. + * @param {RoleData} [data] The data to update the role with + * @param {string} [reason] Reason for creating this role + * @returns {Promise<Role>} + * @example + * // Create a new role + * guild.createRole() + * .then(role => console.log(`Created new role with name ${role.name}`)) + * .catch(console.error); + * @example + * // Create a new role with data + * guild.createRole({ + * name: 'Super Cool People', + * color: 'BLUE', + * }) + * .then(role => console.log(`Created new role with name ${role.name} and color ${role.color}`)) + * .catch(console.error) + */ + createRole(data = {}, reason) { + return this.client.rest.methods.createGuildRole(this, data, reason); + } + + /** + * Creates a new custom emoji in the guild. + * @param {BufferResolvable|Base64Resolvable} attachment The image for the emoji + * @param {string} name The name for the emoji + * @param {Collection<Snowflake, Role>|Role[]} [roles] Roles to limit the emoji to + * @param {string} [reason] Reason for creating the emoji + * @returns {Promise<Emoji>} 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, roles, reason) { + if (typeof attachment === 'string' && attachment.startsWith('data:')) { + return this.client.rest.methods.createEmoji(this, attachment, name, roles, reason); + } else { + return this.client.resolver.resolveImage(attachment).then(data => + this.client.rest.methods.createEmoji(this, data, name, roles, reason) + ); + } + } + + /** + * Delete an emoji. + * @param {Emoji|string} emoji The emoji to delete + * @param {string} [reason] Reason for deleting the emoji + * @returns {Promise} + * @deprecated + */ + deleteEmoji(emoji, reason) { + if (typeof emoji === 'string') emoji = this.emojis.get(emoji); + if (!(emoji instanceof Emoji)) throw new TypeError('Emoji must be either an instance of Emoji or an ID'); + return emoji.delete(reason); + } + + /** + * Causes the client to leave the guild. + * @returns {Promise<Guild>} + * @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<Guild>} + * @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); + } + + /** + * 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 The 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 && + Util.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.selfStream = voiceState.self_stream || false; + 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.connection.status === Constants.Status.READY && emitEvent && !existing) { + this.client.emit(Constants.Events.GUILD_MEMBER_ADD, member); + } + + return member; + } + + _updateMember(member, data) { + const oldMember = Util.cloneObject(member); + + if (data.premium_since) member.premiumSinceTimestamp = new Date(data.premium_since).getTime(); + if (data.roles) member._roles = data.roles; + if (typeof data.nick !== 'undefined') member.nickname = data.nick; + + const notSame = member.nickname !== oldMember.nickname || + member.premiumSinceTimestamp !== oldMember.premiumSinceTimestamp || + !Util.arraysEqual(member._roles, oldMember._roles); + + if (this.client.ws.connection.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) { + if (guildMember.voiceChannel) guildMember.voiceChannel.members.delete(guildMember.id); + this.members.delete(guildMember.id); + } + + _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, this.client)); + } + + /** + * 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 + * @param {boolean} [relative=false] Position Moves the role relative to its current position + * @returns {Promise<Guild>} + */ + setRolePosition(role, position, relative = false) { + if (typeof role === 'string') { + role = this.roles.get(role); + if (!role) return Promise.reject(new Error('Supplied role is not a role or snowflake.')); + } + + position = Number(position); + if (isNaN(position)) return Promise.reject(new Error('Supplied position is not a number.')); + + let updatedRoles = this._sortedRoles.array(); + + Util.moveElementInArray(updatedRoles, role, position, relative); + + updatedRoles = updatedRoles.map((r, i) => ({ id: r.id, position: i })); + return this.client.rest.methods.setRolePositions(this.id, updatedRoles); + } + + /** + * Set the position of a channel in this guild. + * @param {string|GuildChannel} channel The channel to edit, can be a channel object or a channel ID + * @param {number} position The new position of the channel + * @param {boolean} [relative=false] Position Moves the channel relative to its current position + * @returns {Promise<Guild>} + */ + setChannelPosition(channel, position, relative = false) { + if (typeof channel === 'string') { + channel = this.channels.get(channel); + if (!channel) return Promise.reject(new Error('Supplied channel is not a channel or snowflake.')); + } + + position = Number(position); + if (isNaN(position)) return Promise.reject(new Error('Supplied position is not a number.')); + + let updatedChannels = this._sortedChannels(channel.type).array(); + + Util.moveElementInArray(updatedChannels, channel, position, relative); + + updatedChannels = updatedChannels.map((c, i) => ({ id: c.id, position: i })); + return this.client.rest.methods.setChannelPositions(this.id, updatedChannels); + } + + /** + * Fetches a collection of channels in the current guild sorted by position. + * @param {string} type The channel type + * @returns {Collection<Snowflake, GuildChannel>} + * @private + */ + _sortedChannels(type) { + return this._sortPositionWithID(this.channels.filter(c => { + if (type === 'voice' && c.type === 'voice') return true; + else if (type !== 'voice' && c.type !== 'voice') return true; + else return type === c.type; + })); + } + + /** + * Sorts a collection by object position or ID if the positions are equivalent. + * Intended to be identical to Discord's sorting method. + * @param {Collection} collection The collection to sort + * @returns {Collection} + * @private + */ + _sortPositionWithID(collection) { + return collection.sort((a, b) => + a.position !== b.position ? + a.position - b.position : + Long.fromString(b.id).sub(Long.fromString(a.id)).toNumber() + ); + } +} + +/** + * The `#general` TextChannel of the guild + * @name Guild#defaultChannel + * @type {TextChannel} + * @readonly + * @deprecated + */ +Object.defineProperty(Guild.prototype, 'defaultChannel', { + get: util.deprecate(function defaultChannel() { + return this.channels.get(this.id); + }, 'Guild#defaultChannel: This property is obsolete, will be removed in v12.0.0, and may not function as expected.'), +}); + +Guild.prototype.allowDMs = + util.deprecate(Guild.prototype.allowDMs, 'Guild#allowDMs: userbot methods will be removed'); + +Guild.prototype.acknowledge = + util.deprecate(Guild.prototype.acknowledge, 'Guild#acknowledge: userbot methods will be removed'); + +Guild.prototype.setPosition = + util.deprecate(Guild.prototype.setPosition, 'Guild#setPosition: userbot methods will be removed'); + +Guild.prototype.search = + util.deprecate(Guild.prototype.search, 'Guild#search: userbot methods will be removed'); + +Guild.prototype.sync = + util.deprecate(Guild.prototype.sync, 'Guild#sync:, userbot methods will be removed'); + +Guild.prototype.deleteEmoji = + util.deprecate(Guild.prototype.deleteEmoji, 'Guild#deleteEmoji: use Emoji#delete instead'); + +module.exports = Guild; diff --git a/node_modules/discord.js/src/structures/GuildAuditLogs.js b/node_modules/discord.js/src/structures/GuildAuditLogs.js new file mode 100644 index 0000000..5c98594 --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildAuditLogs.js @@ -0,0 +1,462 @@ +const Collection = require('../util/Collection'); +const Snowflake = require('../util/Snowflake'); +const Webhook = require('./Webhook'); +const Integration = require('./Integration'); +const Invite = require('./Invite'); + +/** + * The target type of an entry, e.g. `GUILD`. Here are the available types: + * * GUILD + * * CHANNEL + * * USER + * * ROLE + * * INVITE + * * WEBHOOK + * * EMOJI + * * MESSAGE + * * INTEGRATION + * @typedef {string} AuditLogTargetType + */ + +/** + * Key mirror of all available audit log targets. + * @name GuildAuditLogs.Targets + * @type {AuditLogTargetType} + */ +const Targets = { + ALL: 'ALL', + GUILD: 'GUILD', + CHANNEL: 'CHANNEL', + USER: 'USER', + ROLE: 'ROLE', + INVITE: 'INVITE', + WEBHOOK: 'WEBHOOK', + EMOJI: 'EMOJI', + MESSAGE: 'MESSAGE', + INTEGRATION: 'INTEGRATION', + UNKNOWN: 'UNKNOWN', +}; + +/** + * The action of an entry. Here are the available actions: + * * ALL: null + * * GUILD_UPDATE: 1 + * * CHANNEL_CREATE: 10 + * * CHANNEL_UPDATE: 11 + * * CHANNEL_DELETE: 12 + * * CHANNEL_OVERWRITE_CREATE: 13 + * * CHANNEL_OVERWRITE_UPDATE: 14 + * * CHANNEL_OVERWRITE_DELETE: 15 + * * MEMBER_KICK: 20 + * * MEMBER_PRUNE: 21 + * * MEMBER_BAN_ADD: 22 + * * MEMBER_BAN_REMOVE: 23 + * * MEMBER_UPDATE: 24 + * * MEMBER_ROLE_UPDATE: 25 + * * MEMBER_MOVE: 26 + * * MEMBER_DISCONNECT: 27 + * * BOT_ADD: 28, + * * ROLE_CREATE: 30 + * * ROLE_UPDATE: 31 + * * ROLE_DELETE: 32 + * * INVITE_CREATE: 40 + * * INVITE_UPDATE: 41 + * * INVITE_DELETE: 42 + * * WEBHOOK_CREATE: 50 + * * WEBHOOK_UPDATE: 51 + * * WEBHOOK_DELETE: 52 + * * EMOJI_CREATE: 60 + * * EMOJI_UPDATE: 61 + * * EMOJI_DELETE: 62 + * * MESSAGE_DELETE: 72 + * * MESSAGE_BULK_DELETE: 73 + * * MESSAGE_PIN: 74 + * * MESSAGE_UNPIN: 75 + * * INTEGRATION_CREATE: 80 + * * INTEGRATION_UPDATE: 81 + * * INTEGRATION_DELETE: 82 + * @typedef {?number|string} AuditLogAction + */ + +/** + * All available actions keyed under their names to their numeric values. + * @name GuildAuditLogs.Actions + * @type {AuditLogAction} + */ +const Actions = { + ALL: null, + GUILD_UPDATE: 1, + CHANNEL_CREATE: 10, + CHANNEL_UPDATE: 11, + CHANNEL_DELETE: 12, + CHANNEL_OVERWRITE_CREATE: 13, + CHANNEL_OVERWRITE_UPDATE: 14, + CHANNEL_OVERWRITE_DELETE: 15, + MEMBER_KICK: 20, + MEMBER_PRUNE: 21, + MEMBER_BAN_ADD: 22, + MEMBER_BAN_REMOVE: 23, + MEMBER_UPDATE: 24, + MEMBER_ROLE_UPDATE: 25, + MEMBER_MOVE: 26, + MEMBER_DISCONNECT: 27, + BOT_ADD: 28, + ROLE_CREATE: 30, + ROLE_UPDATE: 31, + ROLE_DELETE: 32, + INVITE_CREATE: 40, + INVITE_UPDATE: 41, + INVITE_DELETE: 42, + WEBHOOK_CREATE: 50, + WEBHOOK_UPDATE: 51, + WEBHOOK_DELETE: 52, + EMOJI_CREATE: 60, + EMOJI_UPDATE: 61, + EMOJI_DELETE: 62, + MESSAGE_DELETE: 72, + MESSAGE_BULK_DELETE: 73, + MESSAGE_PIN: 74, + MESSAGE_UNPIN: 75, + INTEGRATION_CREATE: 80, + INTEGRATION_UPDATE: 81, + INTEGRATION_DELETE: 82, +}; + + +/** + * Audit logs entries are held in this class. + */ +class GuildAuditLogs { + constructor(guild, data) { + if (data.users) for (const user of data.users) guild.client.dataManager.newUser(user); + + /** + * Cached webhooks + * @type {Collection<Snowflake, Webhook>} + * @private + */ + this.webhooks = new Collection(); + if (data.webhooks) { + for (const hook of data.webhooks) { + this.webhooks.set(hook.id, new Webhook(guild.client, hook)); + } + } + + /** + * Cached integrations + * @type {Collection<Snowflake, Integration>} + * @private + */ + this.integrations = new Collection(); + if (data.integrations) { + for (const integration of data.integrations) { + this.integrations.set(integration.id, new Integration(guild.client, integration, guild)); + } + } + + /** + * The entries for this guild's audit logs + * @type {Collection<Snowflake, GuildAuditLogsEntry>} + */ + this.entries = new Collection(); + for (const item of data.audit_log_entries) { + const entry = new GuildAuditLogsEntry(this, guild, item); + this.entries.set(entry.id, entry); + } + } + + /** + * Handles possible promises for entry targets. + * @returns {Promise<GuildAuditLogs>} + */ + static build(...args) { + const logs = new GuildAuditLogs(...args); + return Promise.all(logs.entries.map(e => e.target)).then(() => logs); + } + + /** + * The target of an entry. It can be one of: + * * A guild + * * A user + * * A role + * * An emoji + * * An invite + * * A webhook + * * An integration + * * An object with an id key if target was deleted + * * An object where the keys represent either the new value or the old value + * @typedef {?Object|Guild|User|Role|Emoji|Invite|Webhook|Integration} AuditLogEntryTarget + */ + + /** + * Find target type from entry action. + * @param {number} target The action target + * @returns {?string} + */ + static targetType(target) { + if (target < 10) return Targets.GUILD; + if (target < 20) return Targets.CHANNEL; + if (target < 30) return Targets.USER; + if (target < 40) return Targets.ROLE; + if (target < 50) return Targets.INVITE; + if (target < 60) return Targets.WEBHOOK; + if (target < 70) return Targets.EMOJI; + if (target < 80) return Targets.MESSAGE; + if (target < 90) return Targets.INTEGRATION; + return null; + } + + /** + * The action type of an entry, e.g. `CREATE`. Here are the available types: + * * CREATE + * * DELETE + * * UPDATE + * * ALL + * @typedef {string} AuditLogActionType + */ + + + /** + * Finds the action type from the entry action. + * @param {AuditLogAction} action The action target + * @returns {AuditLogActionType} + */ + static actionType(action) { + if ([ + Actions.CHANNEL_CREATE, + Actions.CHANNEL_OVERWRITE_CREATE, + Actions.MEMBER_BAN_REMOVE, + Actions.BOT_ADD, + Actions.ROLE_CREATE, + Actions.INVITE_CREATE, + Actions.WEBHOOK_CREATE, + Actions.EMOJI_CREATE, + Actions.MESSAGE_PIN, + Actions.INTEGRATION_CREATE, + ].includes(action)) return 'CREATE'; + + if ([ + Actions.CHANNEL_DELETE, + Actions.CHANNEL_OVERWRITE_DELETE, + Actions.MEMBER_KICK, + Actions.MEMBER_PRUNE, + Actions.MEMBER_BAN_ADD, + Actions.MEMBER_DISCONNECT, + Actions.ROLE_DELETE, + Actions.INVITE_DELETE, + Actions.WEBHOOK_DELETE, + Actions.EMOJI_DELETE, + Actions.MESSAGE_DELETE, + Actions.MESSAGE_BULK_DELETE, + Actions.MESSAGE_UNPIN, + Actions.INTEGRATION_DELETE, + ].includes(action)) return 'DELETE'; + + if ([ + Actions.GUILD_UPDATE, + Actions.CHANNEL_UPDATE, + Actions.CHANNEL_OVERWRITE_UPDATE, + Actions.MEMBER_UPDATE, + Actions.MEMBER_ROLE_UPDATE, + Actions.MEMBER_MOVE, + Actions.ROLE_UPDATE, + Actions.INVITE_UPDATE, + Actions.WEBHOOK_UPDATE, + Actions.EMOJI_UPDATE, + Actions.INTEGRATION_UPDATE, + ].includes(action)) return 'UPDATE'; + + return 'ALL'; + } +} + +/** + * Audit logs entry. + */ +class GuildAuditLogsEntry { + // eslint-disable-next-line complexity + constructor(logs, guild, data) { + const targetType = GuildAuditLogs.targetType(data.action_type); + /** + * The target type of this entry + * @type {AuditLogTargetType} + */ + this.targetType = targetType; + + /** + * The action type of this entry + * @type {AuditLogActionType} + */ + this.actionType = GuildAuditLogs.actionType(data.action_type); + + /** + * Specific action type of this entry in its string representation + * @type {AuditLogAction} + */ + this.action = Object.keys(Actions).find(k => Actions[k] === data.action_type); + + /** + * The reason of this entry + * @type {?string} + */ + this.reason = data.reason || null; + + /** + * The user that executed this entry + * @type {User} + */ + this.executor = guild.client.users.get(data.user_id); + + /** + * An entry in the audit log representing a specific change. + * @typedef {object} AuditLogChange + * @property {string} key The property that was changed, e.g. `nick` for nickname changes + * @property {*} [old] The old value of the change, e.g. for nicknames, the old nickname + * @property {*} [new] The new value of the change, e.g. for nicknames, the new nickname + */ + + /** + * Specific property changes + * @type {AuditLogChange[]} + */ + this.changes = data.changes ? data.changes.map(c => ({ key: c.key, old: c.old_value, new: c.new_value })) : null; + + /** + * The ID of this entry + * @type {Snowflake} + */ + this.id = data.id; + + /** + * Any extra data from the entry + * @type {?Object|Role|GuildMember} + */ + this.extra = null; + switch (data.action_type) { + case Actions.MEMBER_PRUNE: + this.extra = { + removed: Number(data.options.members_removed), + days: Number(data.options.delete_member_days), + }; + break; + + case Actions.MEMBER_MOVE: + case Actions.MESSAGE_DELETE: + case Actions.MESSAGE_BULK_DELETE: + this.extra = { + channel: guild.channels.get(data.options.channel_id) || { id: data.options.channel_id }, + count: Number(data.options.count), + }; + break; + + case Actions.MESSAGE_PIN: + case Actions.MESSAGE_UNPIN: + this.extra = { + channel: guild.client.channels.get(data.options.channel_id) || { id: data.options.channel_id }, + messageID: data.options.message_id, + }; + break; + + case Actions.MEMBER_DISCONNECT: + this.extra = { + count: Number(data.options.count), + }; + break; + + case Actions.CHANNEL_OVERWRITE_CREATE: + case Actions.CHANNEL_OVERWRITE_UPDATE: + case Actions.CHANNEL_OVERWRITE_DELETE: + switch (data.options.type) { + case 'member': + this.extra = guild.members.get(data.options.id) || + { id: data.options.id, type: 'member' }; + break; + case 'role': + this.extra = guild.roles.get(data.options.id) || + { id: data.options.id, name: data.options.role_name, type: 'role' }; + break; + default: + break; + } + break; + + default: + break; + } + + /** + * The target of this entry + * @type {AuditLogEntryTarget} + */ + this.target = null; + if (targetType === Targets.UNKNOWN) { + this.changes.reduce((o, c) => { + o[c.key] = c.new || c.old; + return o; + }, {}); + this.target.id = data.target_id; + // MEMBER_DISCONNECT and similar types do not provide a target_id. + } else if (targetType === Targets.USER && data.target_id) { + this.target = guild.client.users.get(data.target_id); + } else if (targetType === Targets.GUILD) { + this.target = guild.client.guilds.get(data.target_id); + } else if (targetType === Targets.WEBHOOK) { + this.target = logs.webhooks.get(data.target_id) || + new Webhook(guild.client, + this.changes.reduce((o, c) => { + o[c.key] = c.new || c.old; + return o; + }, { + id: data.target_id, + guild_id: guild.id, + })); + } else if (targetType === Targets.INVITE) { + const changes = this.changes.reduce((o, c) => { + o[c.key] = c.new || c.old; + return o; + }, { + id: data.target_id, + guild, + }); + changes.channel = { id: changes.channel_id }; + this.target = new Invite(guild.client, changes); + } else if (targetType === Targets.MESSAGE) { + // Discord sends a channel id for the MESSAGE_BULK_DELETE action type. + this.target = data.action_type === Actions.MESSAGE_BULK_DELETE ? + guild.channels.get(data.target_id) || { id: data.target_id } : + guild.client.users.get(data.target_id); + } else if (targetType === Targets.INTEGRATION) { + this.target = logs.integrations.get(data.target_id) || + new Integration(guild.client, this.changes.reduce((o, c) => { + o[c.key] = c.new || c.old; + return o; + }, { id: data.target_id }), guild); + } else if (data.target_id) { + this.target = guild[`${targetType.toLowerCase()}s`].get(data.target_id) || { id: data.target_id }; + } + } + + /** + * The timestamp this entry was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * The time this entry was created + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } +} + +GuildAuditLogs.Actions = Actions; +GuildAuditLogs.Targets = Targets; +GuildAuditLogs.Entry = GuildAuditLogsEntry; + +module.exports = GuildAuditLogs; 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..0dbc0e3 --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildChannel.js @@ -0,0 +1,601 @@ +const Channel = require('./Channel'); +const Role = require('./Role'); +const PermissionOverwrites = require('./PermissionOverwrites'); +const Permissions = require('../util/Permissions'); +const Collection = require('../util/Collection'); +const Constants = require('../util/Constants'); +const Invite = require('./Invite'); +const Util = require('../util/Util'); + +/** + * 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; + + /** + * The ID of the category parent of this channel + * @type {?Snowflake} + */ + this.parentID = data.parent_id; + + /** + * A map of permission overwrites in this channel for roles and users + * @type {Collection<Snowflake, PermissionOverwrites>} + */ + this.permissionOverwrites = new Collection(); + if (data.permission_overwrites) { + for (const overwrite of data.permission_overwrites) { + this.permissionOverwrites.set(overwrite.id, new PermissionOverwrites(this, overwrite)); + } + } + } + + /** + * The position of the channel + * @type {number} + * @readonly + */ + get calculatedPosition() { + const sorted = this.guild._sortedChannels(this.type); + return sorted.array().indexOf(sorted.get(this.id)); + } + + /** + * The category parent of this channel + * @type {?CategoryChannel} + * @readonly + */ + get parent() { + return this.guild.channels.get(this.parentID) || null; + } + + /** + * If the permissionOverwrites match the parent channel, null if no parent + * @type {?boolean} + * @readonly + */ + get permissionsLocked() { + if (!this.parent) return null; + if (this.permissionOverwrites.size !== this.parent.permissionOverwrites.size) return false; + return this.permissionOverwrites.every((value, key) => { + const testVal = this.parent.permissionOverwrites.get(key); + return testVal !== undefined && + testVal.deny === value.deny && + testVal.allow === value.allow; + }); + } + + /** + * Gets the overall set of permissions for a user in this channel, taking into account channel overwrites. + * @param {GuildMemberResolvable} member The user that you want to obtain the overall permissions for + * @returns {?Permissions} + */ + memberPermissions(member) { + member = this.client.resolver.resolveGuildMember(this.guild, member); + if (!member) return null; + + if (member.id === this.guild.ownerID) return new Permissions(member, Permissions.ALL); + + const roles = member.roles; + const permissions = new Permissions(roles.map(role => role.permissions)); + + if (permissions.has(Permissions.FLAGS.ADMINISTRATOR)) return new Permissions(Permissions.ALL).freeze(); + + const overwrites = this.overwritesFor(member, true, roles); + + return permissions + .remove(overwrites.everyone ? overwrites.everyone.deny : 0) + .add(overwrites.everyone ? overwrites.everyone.allow : 0) + .remove(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.deny) : 0) + .add(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.allow) : 0) + .remove(overwrites.member ? overwrites.member.deny : 0) + .add(overwrites.member ? overwrites.member.allow : 0) + .freeze(); + } + + /** + * Gets the overall set of permissions for a role in this channel, taking into account channel overwrites. + * @param {RoleResolvable} role The role that you want to obtain the overall permissions for + * @returns {?Permissions} + */ + rolePermissions(role) { + if (role.permissions & Permissions.FLAGS.ADMINISTRATOR) return new Permissions(Permissions.ALL).freeze(); + + const everyoneOverwrites = this.permissionOverwrites.get(this.guild.id); + const roleOverwrites = this.permissionOverwrites.get(role.id); + + return new Permissions(role.permissions) + .remove(everyoneOverwrites ? everyoneOverwrites.deny : 0) + .add(everyoneOverwrites ? everyoneOverwrites.allow : 0) + .remove(roleOverwrites ? roleOverwrites.deny : 0) + .add(roleOverwrites ? roleOverwrites.allow : 0) + .freeze(); + } + + /** + * Get the overall set of permissions for a member or role in this channel, taking into account channel overwrites. + * @param {GuildMemberResolvable|RoleResolvable} memberOrRole The member or role to obtain the overall permissions for + * @returns {?Permissions} + */ + permissionsFor(memberOrRole) { + const member = this.guild.member(memberOrRole); + if (member) return this.memberPermissions(member); + const role = this.client.resolver.resolveRole(this.guild, memberOrRole); + if (role) return this.rolePermissions(role); + return null; + } + + 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 = []; + let memberOverwrites; + let everyoneOverwrites; + + for (const overwrite of this.permissionOverwrites.values()) { + if (overwrite.id === this.guild.id) { + everyoneOverwrites = overwrite; + } else if (roles.has(overwrite.id)) { + roleOverwrites.push(overwrite); + } else if (overwrite.id === member.id) { + memberOverwrites = overwrite; + } + } + + return { + everyone: everyoneOverwrites, + roles: roleOverwrites, + member: memberOverwrites, + }; + } + + /** + * Replaces the permission overwrites for a channel + * @param {Object} [options] Options + * @param {ChannelCreationOverwrites[]|Collection<Snowflake, PermissionOverwrites>} [options.overwrites] + * Permission overwrites + * @param {string} [options.reason] Reason for updating the channel overwrites + * @returns {Promise<GuildChannel>} + * @example + * channel.replacePermissionOverwrites({ + * overwrites: [ + * { + * id: message.author.id, + * denied: ['VIEW_CHANNEL'], + * }, + * ], + * reason: 'Needed to change permissions' + * }); + */ + replacePermissionOverwrites({ overwrites, reason } = {}) { + return this.edit({ permissionOverwrites: overwrites, reason }) + .then(() => this); + } + + /** + * An object mapping permission flags to `true` (enabled), `null` (unset) or `false` (disabled). + * ```js + * { + * 'SEND_MESSAGES': true, + * 'EMBED_LINKS': null, + * 'ATTACH_FILES': false, + * } + * ``` + * @typedef {Object} PermissionOverwriteOptions + */ + + /** + * Overwrites the permissions for a user or role in this channel. + * @param {Role|Snowflake|UserResolvable} userOrRole The user or role to update + * @param {PermissionOverwriteOptions} options The configuration for the update + * @param {string} [reason] Reason for creating/editing this overwrite + * @returns {Promise<GuildChannel>} + * @example + * // Overwrite permissions for a message author + * message.channel.overwritePermissions(message.author, { + * SEND_MESSAGES: false + * }) + * .then(updated => console.log(updated.permissionOverwrites.get(message.author.id))) + * .catch(console.error); + * @example + * // Overwite permissions for a message author and reset some + * message.channel.overwritePermissions(message.author, { + * VIEW_CHANNEL: false, + * SEND_MESSAGES: null + * }) + * .then(updated => console.log(updated.permissionOverwrites.get(message.author.id))) + * .catch(console.error); + */ + overwritePermissions(userOrRole, options, reason) { + 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 of Object.keys(options)) { + if (options[perm] === true) { + payload.allow |= Permissions.FLAGS[perm] || 0; + payload.deny &= ~(Permissions.FLAGS[perm] || 0); + } else if (options[perm] === false) { + payload.allow &= ~(Permissions.FLAGS[perm] || 0); + payload.deny |= Permissions.FLAGS[perm] || 0; + } else if (options[perm] === null) { + payload.allow &= ~(Permissions.FLAGS[perm] || 0); + payload.deny &= ~(Permissions.FLAGS[perm] || 0); + } + } + + return this.client.rest.methods.setChannelOverwrite(this, payload, reason).then(() => this); + } + + /** + * Locks in the permission overwrites from the parent channel. + * @returns {Promise<GuildChannel>} + */ + lockPermissions() { + if (!this.parent) return Promise.reject(new TypeError('Could not find a parent to this guild channel.')); + const permissionOverwrites = this.parent.permissionOverwrites.map(overwrite => ({ + deny: overwrite.deny, + allow: overwrite.allow, + id: overwrite.id, + type: overwrite.type, + })); + return this.edit({ permissionOverwrites }); + } + + /** + * The data for a guild channel. + * @typedef {Object} ChannelData + * @property {string} [type] The type of the channel (Only when creating) + * @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 {boolean} [nsfw] Whether the channel is NSFW + * @property {number} [bitrate] The bitrate of the voice channel + * @property {number} [userLimit] The user limit of the channel + * @property {CategoryChannel|Snowflake} [parent] The parent or parent ID of the channel + * @property {ChannelCreationOverwrites[]|Collection<Snowflake, PermissionOverwrites>} [permissionOverwrites] + * Overwrites of the channel + * @property {number} [rateLimitPerUser] The rate limit per user of the channel in seconds + * @property {string} [reason] Reason for creating the channel (Only when creating) + */ + + /** + * Edits the channel. + * @param {ChannelData} data The new data for the channel + * @param {string} [reason] Reason for editing this channel + * @returns {Promise<GuildChannel>} + * @example + * // Edit a channel + * channel.edit({ name: 'new-channel' }) + * .then(console.log) + * .catch(console.error); + */ + edit(data, reason) { + return this.client.rest.methods.updateChannel(this, data, reason).then(() => this); + } + + /** + * Set a new name for the guild channel. + * @param {string} name The new name for the guild channel + * @param {string} [reason] Reason for changing the guild channel's name + * @returns {Promise<GuildChannel>} + * @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, reason) { + return this.edit({ name }, reason); + } + + /** + * Set a new position for the guild channel. + * @param {number} position The new position for the guild channel + * @param {boolean} [relative=false] Move the position relative to its current value + * @returns {Promise<GuildChannel>} + * @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, relative) { + return this.guild.setChannelPosition(this, position, relative).then(() => this); + } + + /** + * Set a new parent for the guild channel. + * @param {CategoryChannel|SnowFlake} parent The new parent for the guild channel + * @param {string} [reason] Reason for changing the guild channel's parent + * @returns {Promise<GuildChannel>} + * @example + * // Sets the parent of a channel + * channel.setParent('174674066072928256') + * .then(updated => console.log(`Set the category of ${updated.name} to ${updated.parent.name}`)) + * .catch(console.error); + */ + setParent(parent, reason) { + parent = this.client.resolver.resolveChannelID(parent); + return this.edit({ parent }, reason); + } + + /** + * Set a new topic for the guild channel. + * @param {string} topic The new topic for the guild channel + * @param {string} [reason] Reason for changing the guild channel's topic + * @returns {Promise<GuildChannel>} + * @example + * // Set a new channel topic + * channel.setTopic('Needs more rate limiting') + * .then(updated => console.log(`Channel's new topic is ${updated.topic}`)) + * .catch(console.error); + */ + setTopic(topic, reason) { + return this.edit({ topic }, reason); + } + + /** + * Create an invite to this guild channel. + * <warn>This is only available when using a bot account.</warn> + * @param {Object} [options={}] Options for the invite + * @param {boolean} [options.temporary=false] Whether members that joined via the invite should be automatically + * kicked after 24 hours if they have not yet received a role + * @param {number} [options.maxAge=86400] How long the invite should last (in seconds, 0 for forever) + * @param {number} [options.maxUses=0] Maximum number of uses + * @param {boolean} [options.unique=false] Create a unique invite, or use an existing one with similar settings + * @param {string} [reason] Reason for creating the invite + * @returns {Promise<Invite>} + * @example + * // Create an invite to a channel + * channel.createInvite() + * .then(invite => console.log(`Created an invite with a code of ${invite.code}`)) + * .catch(console.error); + */ + createInvite(options = {}, reason) { + return this.client.rest.methods.createChannelInvite(this, options, reason); + } + + /* eslint-disable max-len */ + /** + * Options to clone a guild channel. + * @typedef {Object} GuildChannelCloneOptions + * @property {string} [name=this.name] Name of the new channel + * @property {ChannelCreationOverwrites[]|Collection<Snowflake, PermissionOverwrites>} [permissionOverwrites=this.permissionOverwrites] + * Permission overwrites of the new channel + * @property {string} [type=this.type] Type of the new channel + * @property {string} [topic=this.topic] Topic of the new channel (only text) + * @property {boolean} [nsfw=this.nsfw] Whether the new channel is nsfw (only text) + * @property {number} [bitrate=this.bitrate] Bitrate of the new channel in bits (only voice) + * @property {number} [userLimit=this.userLimit] Maximum amount of users allowed in the new channel (only voice) + * @property {number} [rateLimitPerUser=ThisType.rateLimitPerUser] Ratelimit per user for the new channel (only text) + * @property {ChannelResolvable} [parent=this.parent] Parent of the new channel + * @property {string} [reason] Reason for cloning this channel + */ + /* eslint-enable max-len */ + + /** + * Clone this channel. + * @param {string|GuildChannelCloneOptions} [nameOrOptions={}] Name for the new channel. + * **(deprecated, use options)** + * Alternatively options for cloning the channel + * @param {boolean} [withPermissions=true] Whether to clone the channel with this channel's permission overwrites + * **(deprecated, use options)** + * @param {boolean} [withTopic=true] Whether to clone the channel with this channel's topic + * **(deprecated, use options)** + * @param {string} [reason] Reason for cloning this channel **(deprecated, user options)** + * @returns {Promise<GuildChannel>} + * @example + * // Clone a channel + * channel.clone({ topic: null, reason: 'Needed a clone' }) + * .then(clone => console.log(`Cloned ${channel.name} to make a channel called ${clone.name}`)) + * .catch(console.error); + */ + clone(nameOrOptions = {}, withPermissions = true, withTopic = true, reason) { + // If more than one parameter was specified or the first is a string, + // convert them to a compatible options object and issue a warning + if (arguments.length > 1 || typeof nameOrOptions === 'string') { + process.emitWarning( + 'GuildChannel#clone: Clone channels using an options object instead of separate parameters.', + 'Deprecation Warning' + ); + + nameOrOptions = { + name: nameOrOptions, + permissionOverwrites: withPermissions ? this.permissionOverwrites : null, + topic: withTopic ? this.topic : null, + reason: reason || null, + }; + } + + Util.mergeDefault({ + name: this.name, + permissionOverwrites: this.permissionOverwrites, + topic: this.topic, + type: this.type, + nsfw: this.nsfw, + parent: this.parent, + bitrate: this.bitrate, + userLimit: this.userLimit, + rateLimitPerUser: this.rateLimitPerUser, + reason: null, + }, nameOrOptions); + + return this.guild.createChannel(nameOrOptions.name, nameOrOptions); + } + + /** + * Fetches a collection of invites to this guild channel. + * Resolves with a collection mapping invites by their codes. + * @returns {Promise<Collection<string, Invite>>} + */ + fetchInvites() { + return this.client.rest.makeRequest('get', Constants.Endpoints.Channel(this.id).invites, true) + .then(data => { + const invites = new Collection(); + for (let invite of data) { + invite = new Invite(this.client, invite); + invites.set(invite.code, invite); + } + + return invites; + }); + } + + /** + * Deletes this channel. + * @param {string} [reason] Reason for deleting this channel + * @returns {Promise<GuildChannel>} + * @example + * // Delete the channel + * channel.delete('Making room for new channels') + * .then(deleted => console.log(`Deleted ${deleted.name} to make room for new channels`)) + * .catch(console.error); + */ + delete(reason) { + return this.client.rest.methods.deleteChannel(this, reason); + } + + /** + * 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; + } + + /** + * Whether the channel is deletable by the client user + * @type {boolean} + * @readonly + */ + get deletable() { + return this.id !== this.guild.id && + this.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_CHANNELS); + } + + /** + * Whether the channel is manageable by the client user + * @type {boolean} + * @readonly + */ + get manageable() { + if (this.client.user.id === this.guild.ownerID) return true; + const permissions = this.permissionsFor(this.client.user); + if (!permissions) return false; + return permissions.has([Permissions.FLAGS.MANAGE_CHANNELS, Permissions.FLAGS.VIEW_CHANNEL]); + } + + /** + * Whether the channel is muted + * <warn>This is only available when using a user account.</warn> + * @type {?boolean} + * @readonly + * @deprecated + */ + get muted() { + if (this.client.user.bot) return null; + try { + return this.client.user.guildSettings.get(this.guild.id).channelOverrides.get(this.id).muted; + } catch (err) { + return false; + } + } + + /** + * The type of message that should notify you + * <warn>This is only available when using a user account.</warn> + * @type {?MessageNotificationType} + * @readonly + * @deprecated + */ + get messageNotifications() { + if (this.client.user.bot) return null; + try { + return this.client.user.guildSettings.get(this.guild.id).channelOverrides.get(this.id).messageNotifications; + } catch (err) { + return Constants.MessageNotificationTypes[3]; + } + } + + /** + * When concatenated with a string, this automatically returns the channel's mention instead of the Channel object. + * @returns {string} + * @example + * // Logs: Hello from <#123456789012345678> + * console.log(`Hello from ${channel}`); + * @example + * // Logs: Hello from <#123456789012345678> + * 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..45b75b3 --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildMember.js @@ -0,0 +1,636 @@ +const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const Role = require('./Role'); +const Permissions = require('../util/Permissions'); +const Collection = require('../util/Collection'); +const { Presence } = require('./Presence'); +const util = require('util'); + +/** + * 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 member instance Represents + * @type {User} + */ + this.user = {}; + + /** + * The timestamp this member joined the guild at + * @type {number} + */ + this.joinedTimestamp = null; + + /** + * The timestamp of when the member used their Nitro boost on the guild, if it was used + * @type {?number} + */ + this.premiumSinceTimestamp = null; + + this._roles = []; + if (data) this.setup(data); + + /** + * The ID of the last message sent by this member in their guild, if one was sent + * @type {?Snowflake} + */ + this.lastMessageID = null; + + /** + * The Message object of the last message sent by this member in their guild, if one was sent + * @type {?Message} + */ + this.lastMessage = null; + + /** + * Whether the member has been removed from the guild + * @type {boolean} + */ + this.deleted = false; + } + + 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; + + /** + * Whether this member is streaming using "Go Live" + * @type {boolean} + */ + this.selfStream = data.self_stream || false; + + /** + * The voice session ID of this member, if any + * @type {?Snowflake} + */ + this.voiceSessionID = data.session_id; + + /** + * The voice channel ID of this member, if any + * @type {?Snowflake} + */ + this.voiceChannelID = data.channel_id; + + /** + * Whether this member is speaking and the client is in the same channel + * @type {boolean} + */ + this.speaking = false; + + /** + * The nickname of this member, if they have one + * @type {?string} + */ + this.nickname = data.nick || null; + + if (data.joined_at) this.joinedTimestamp = new Date(data.joined_at).getTime(); + if (data.premium_since) this.premiumSinceTimestamp = new Date(data.premium_since).getTime(); + + this.user = data.user; + this._roles = data.roles; + } + + /** + * The time this member joined the guild + * @type {?Date} + * @readonly + */ + get joinedAt() { + return this.joinedTimestamp ? new Date(this.joinedTimestamp) : null; + } + + /** + * The time of when the member used their Nitro boost on the guild, if it was used + * @type {?Date} + * @readonly + */ + get premiumSince() { + return this.premiumSinceTimestamp ? new Date(this.premiumSinceTimestamp) : null; + } + + /** + * The presence of this member + * @type {Presence} + * @readonly + */ + get presence() { + return this.frozenPresence || this.guild.presences.get(this.id) || new Presence(undefined, this.client); + } + + /** + * A list of roles that are applied to this member, mapped by the role ID + * @type {Collection<Snowflake, Role>} + * @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 this member with the highest position + * @type {Role} + * @readonly + */ + get highestRole() { + return this.roles.reduce((prev, role) => !prev || role.comparePositionTo(prev) > 0 ? role : prev); + } + + /** + * The role of this member used to set their color + * @type {?Role} + * @readonly + */ + get colorRole() { + const coloredRoles = this.roles.filter(role => role.color); + if (!coloredRoles.size) return null; + return coloredRoles.reduce((prev, role) => !prev || role.comparePositionTo(prev) > 0 ? role : prev); + } + + /** + * The displayed color of this member in base 10 + * @type {number} + * @readonly + */ + get displayColor() { + const role = this.colorRole; + return (role && role.color) || 0; + } + + /** + * The displayed color of this member in hexadecimal + * @type {string} + * @readonly + */ + get displayHexColor() { + const role = this.colorRole; + return (role && role.hexColor) || '#000000'; + } + + /** + * The role of this member used to hoist them in a separate category in the users list + * @type {?Role} + * @readonly + */ + get hoistRole() { + const hoistedRoles = this.roles.filter(role => role.hoist); + if (!hoistedRoles.size) return null; + return hoistedRoles.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 {Snowflake} + * @readonly + */ + get id() { + return this.user.id; + } + + /** + * The nickname of this 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 this member, taking only roles into account + * @type {Permissions} + * @readonly + */ + get permissions() { + if (this.user.id === this.guild.ownerID) return new Permissions(this, Permissions.ALL); + + let permissions = 0; + const roles = this.roles; + for (const role of roles.values()) permissions |= role.permissions; + + return new Permissions(this, permissions); + } + + /** + * Whether this member is manageable in terms of role hierarchy by the client user + * @type {boolean} + * @readonly + */ + get manageable() { + if (this.user.id === this.guild.ownerID) return false; + if (this.user.id === this.client.user.id) return false; + if (this.client.user.id === this.guild.ownerID) return true; + return this.guild.me.highestRole.comparePositionTo(this.highestRole) > 0; + } + + /** + * Whether this member is kickable by the client user + * @type {boolean} + * @readonly + */ + get kickable() { + return this.manageable && this.guild.me.permissions.has(Permissions.FLAGS.KICK_MEMBERS); + } + + /** + * Whether this member is bannable by the client user + * @type {boolean} + * @readonly + */ + get bannable() { + return this.manageable && this.guild.me.permissions.has(Permissions.FLAGS.BAN_MEMBERS); + } + + /** + * Returns `channel.permissionsFor(guildMember)`. Returns permissions for this member in a guild channel, + * taking into account roles and permission overwrites. + * @param {ChannelResolvable} channel The guild channel to use as context + * @returns {?Permissions} + */ + 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 this member's roles have a permission. + * @param {PermissionResolvable} permission Permission(s) to check for + * @param {boolean} [explicit=false] Whether to require the role to explicitly have the exact permission + * **(deprecated)** + * @param {boolean} [checkAdmin] Whether to allow the administrator permission to override + * (takes priority over `explicit`) + * @param {boolean} [checkOwner] Whether to allow being the guild's owner to override + * (takes priority over `explicit`) + * @returns {boolean} + */ + hasPermission(permission, explicit = false, checkAdmin, checkOwner) { + if (typeof checkAdmin === 'undefined') checkAdmin = !explicit; + if (typeof checkOwner === 'undefined') checkOwner = !explicit; + if (checkOwner && this.user.id === this.guild.ownerID) return true; + return this.roles.some(r => r.hasPermission(permission, undefined, checkAdmin)); + } + + /** + * Checks whether the roles of this 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} + * @deprecated + */ + hasPermissions(permissions, explicit = false) { + if (!explicit && this.user.id === this.guild.ownerID) return true; + return this.hasPermission(permissions, explicit); + } + + /** + * Checks whether the roles of this 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) { + if (!(permissions instanceof Array)) permissions = [permissions]; + return this.permissions.missing(permissions, explicit); + } + + /** + * The data for editing this member. + * @typedef {Object} GuildMemberEditData + * @property {string} [nick] The nickname to set for the member + * @property {Collection<Snowflake, Role>|RoleResolvable[]} [roles] The roles or role IDs to apply + * @property {boolean} [mute] Whether or not the member should be muted + * @property {boolean} [deaf] Whether or not the member should be deafened + * @property {ChannelResolvable|null} [channel] Channel to move member to (if they are connected to voice), or `null` + * if you want to kick them from voice + */ + + /** + * Edits this member. + * @param {GuildMemberEditData} data The data to edit the member with + * @param {string} [reason] Reason for editing this user + * @returns {Promise<GuildMember>} + * @example + * // Set a member's nickname and clear their roles + * message.member.edit({ + * nick: 'Cool Name', + * roles: [] + * }) + * .then(console.log) + * .catch(console.error); + */ + edit(data, reason) { + return this.client.rest.methods.updateGuildMember(this, data, reason); + } + + /** + * Mute/unmute this member. + * @param {boolean} mute Whether or not the member should be muted + * @param {string} [reason] Reason for muting or unmuting + * @returns {Promise<GuildMember>} + * @example + * // Mute a member with a reason + * message.member.setMute(true, 'It needed to be done') + * .then(() => console.log(`Muted ${message.member.displayName}`))) + * .catch(console.error); + */ + setMute(mute, reason) { + return this.edit({ mute }, reason); + } + + /** + * Deafen/undeafen this member. + * @param {boolean} deaf Whether or not the member should be deafened + * @param {string} [reason] Reason for deafening or undeafening + * @returns {Promise<GuildMember>} + * @example + * // Deafen a member + * message.member.setDeaf(true) + * .then(() => console.log(`Deafened ${message.member.displayName}`)) + * .catch(console.error); + */ + setDeaf(deaf, reason) { + return this.edit({ deaf }, reason); + } + + /** + * Moves this member to the given channel. + * @param {ChannelResolvable|null} channel Channel to move the member to, or `null` if you want to kick them from + * voice + * @returns {Promise<GuildMember>} + * @example + * // Moves a member to a voice channel + * member.setVoiceChannel('174674066072928256') + * .then(() => console.log(`Moved ${member.displayName}`)) + * .catch(console.error); + */ + setVoiceChannel(channel) { + return this.edit({ channel }); + } + + /** + * Sets the roles applied to this member. + * @param {Collection<Snowflake, Role>|RoleResolvable[]} roles The roles or role IDs to apply + * @param {string} [reason] Reason for applying the roles + * @returns {Promise<GuildMember>} + * @example + * // Set the member's roles to a single role + * guildMember.setRoles(['391156570408615936']) + * .then(console.log) + * .catch(console.error); + * @example + * // Remove all of the member's roles + * guildMember.setRoles([]) + * .then(member => console.log(`${member.displayName} now has ${member.roles.size} roles`)) + * .catch(console.error); + */ + setRoles(roles, reason) { + return this.edit({ roles }, reason); + } + + /** + * Adds a single role to this member. + * @param {RoleResolvable} role The role or ID of the role to add + * @param {string} [reason] Reason for adding the role + * @returns {Promise<GuildMember>} + * @example + * // Give a role to a member + * message.member.addRole('193654001089118208') + * .then(console.log) + * .catch(console.error); + */ + addRole(role, reason) { + if (!(role instanceof Role)) role = this.guild.roles.get(role); + if (!role) return Promise.reject(new TypeError('Supplied parameter was neither a Role nor a Snowflake.')); + return this.client.rest.methods.addMemberRole(this, role, reason); + } + + /** + * Adds multiple roles to this member. + * @param {Collection<Snowflake, Role>|RoleResolvable[]} roles The roles or role IDs to add + * @param {string} [reason] Reason for adding the roles + * @returns {Promise<GuildMember>} + * @example + * // Gives a member a few roles + * message.member.addRoles(['193654001089118208', '369308579892690945']) + * .then(console.log) + * .catch(console.error); + */ + addRoles(roles, reason) { + 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 }, reason); + } + + /** + * Removes a single role from this member. + * @param {RoleResolvable} role The role or ID of the role to remove + * @param {string} [reason] Reason for removing the role + * @returns {Promise<GuildMember>} + * @example + * // Remove a role from a member + * message.member.removeRole('193654001089118208') + * .then(console.log) + * .catch(console.error); + */ + removeRole(role, reason) { + if (!(role instanceof Role)) role = this.guild.roles.get(role); + if (!role) return Promise.reject(new TypeError('Supplied parameter was neither a Role nor a Snowflake.')); + return this.client.rest.methods.removeMemberRole(this, role, reason); + } + + /** + * Removes multiple roles from this member. + * @param {Collection<Snowflake, Role>|RoleResolvable[]} roles The roles or role IDs to remove + * @param {string} [reason] Reason for removing the roles + * @returns {Promise<GuildMember>} + * @example + * // Removes a few roles from the member + * message.member.removeRoles(['193654001089118208', '369308579892690945']) + * .then(console.log) + * .catch(console.error); + */ + removeRoles(roles, reason) { + const allRoles = this._roles.slice(); + if (roles instanceof Collection) { + for (const role of roles.values()) { + const index = allRoles.indexOf(role.id); + if (index >= 0) allRoles.splice(index, 1); + } + } else { + for (const role of roles) { + const index = allRoles.indexOf(role instanceof Role ? role.id : role); + if (index >= 0) allRoles.splice(index, 1); + } + } + return this.edit({ roles: allRoles }, reason); + } + + /** + * Set the nickname for this member. + * @param {string} nick The nickname for the guild member + * @param {string} [reason] Reason for setting the nickname + * @returns {Promise<GuildMember>} + * @example + * // Update the member's nickname + * message.member.setNickname('Cool Name') + * .then(console.log) + * .catch(console.error); + */ + setNickname(nick, reason) { + return this.edit({ nick }, reason); + } + + /** + * Creates a DM channel between the client and this member. + * @returns {Promise<DMChannel>} + */ + createDM() { + return this.user.createDM(); + } + + /** + * Deletes any DMs with this guild member. + * @returns {Promise<DMChannel>} + */ + deleteDM() { + return this.user.deleteDM(); + } + + /** + * Kick this member from the guild. + * @param {string} [reason] Reason for kicking user + * @returns {Promise<GuildMember>} + * @example + * // Kick a member + * member.kick() + * .then(() => console.log(`Kicked ${member.displayName}`)) + * .catch(console.error); + */ + kick(reason) { + return this.client.rest.methods.kickGuildMember(this.guild, this, reason); + } + + /** + * Ban this member. + * @param {Object|number|string} [options] Ban options. If a number, the number of days to delete messages for, if a + * string, the ban reason. Supplying an object allows you to do both. + * @param {number} [options.days=0] Number of days of messages to delete + * @param {string} [options.reason] Reason for banning + * @returns {Promise<GuildMember>} + * @example + * // Ban a guild member + * member.ban(7) + * .then(() => console.log(`Banned ${member.displayName}`)) + * .catch(console.error); + */ + ban(options) { + return this.guild.ban(this, options); + } + + /** + * 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 + /* eslint-disable no-empty-function */ + send() {} + sendMessage() {} + sendEmbed() {} + sendFile() {} + sendCode() {} +} + +TextBasedChannel.applyToClass(GuildMember); + +GuildMember.prototype.hasPermissions = util.deprecate(GuildMember.prototype.hasPermissions, + 'GuildMember#hasPermissions is deprecated - use GuildMember#hasPermission, it now takes an array'); +GuildMember.prototype.missingPermissions = util.deprecate(GuildMember.prototype.missingPermissions, + 'GuildMember#missingPermissions is deprecated - use GuildMember#permissions.missing, it now takes an array'); + +module.exports = GuildMember; diff --git a/node_modules/discord.js/src/structures/Integration.js b/node_modules/discord.js/src/structures/Integration.js new file mode 100644 index 0000000..96af017 --- /dev/null +++ b/node_modules/discord.js/src/structures/Integration.js @@ -0,0 +1,151 @@ +/** + * The information account for an integration + * @typedef {Object} IntegrationAccount + * @property {string} id The id of the account + * @property {string} name The name of the account + */ + +/** + * Represents a guild integration. + */ +class Integration { + constructor(client, data, guild) { + /** + * The client that created this integration + * @name Integration#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + /** + * The guild this integration belongs to + * @type {Guild} + */ + this.guild = guild; + + /** + * The integration id + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The integration name + * @type {string} + */ + this.name = data.name; + /** + * The integration type (twitch, youtube, etc) + * @type {string} + */ + this.type = data.type; + + /** + * Whether this integration is enabled + * @type {boolean} + */ + this.enabled = data.enabled; + + /** + * Whether this integration is syncing + * @type {boolean} + */ + this.syncing = data.syncing; + + /** + * The role that this integration uses for subscribers + * @type {Role} + */ + this.role = this.guild.roles.get(data.role_id); + + /** + * The user for this integration + * @type {User} + */ + this.user = this.client.dataManager.newUser(data.user); + + /** + * The account integration information + * @type {IntegrationAccount} + */ + this.account = data.account; + + /** + * The last time this integration was last synced + * @type {number} + */ + this.syncedAt = data.synced_at; + this._patch(data); + } + + _patch(data) { + /** + * The behavior of expiring subscribers + * @type {number} + */ + this.expireBehavior = data.expire_behavior; + + /** + * The grace period before expiring subscribers + * @type {number} + */ + this.expireGracePeriod = data.expire_grace_period; + } + + /** + * Syncs this integration + * @returns {Promise<Integration>} + */ + sync() { + this.syncing = true; + return this.client.rest.methods.syncIntegration(this) + .then(() => { + this.syncing = false; + this.syncedAt = Date.now(); + return this; + }); + } + + /** + * The data for editing an integration. + * @typedef {Object} IntegrationEditData + * @property {number} [expireBehavior] The new behaviour of expiring subscribers + * @property {number} [expireGracePeriod] The new grace period before expiring subscribers + */ + + /** + * Edits this integration. + * @param {IntegrationEditData} data The data to edit this integration with + * @param {string} reason Reason for editing this integration + * @returns {Promise<Integration>} + */ + edit(data, reason) { + if ('expireBehavior' in data) { + data.expire_behavior = data.expireBehavior; + data.expireBehavior = undefined; + } + if ('expireGracePeriod' in data) { + data.expire_grace_period = data.expireGracePeriod; + data.expireGracePeriod = undefined; + } + // The option enable_emoticons is only available for Twitch at this moment + return this.client.rest.methods.editIntegration(this, data, reason) + .then(() => { + this._patch(data); + return this; + }); + } + + /** + * Deletes this integration. + * @returns {Promise<Integration>} + * @param {string} [reason] Reason for deleting this integration + */ + delete(reason) { + return this.client.rest.methods.deleteIntegration(this, reason) + .then(() => this); + } +} + +module.exports = Integration; 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..1d84cf5 --- /dev/null +++ b/node_modules/discord.js/src/structures/Invite.js @@ -0,0 +1,164 @@ +const PartialGuild = require('./PartialGuild'); +const PartialGuildChannel = require('./PartialGuildChannel'); +const Constants = require('../util/Constants'); + +/** + * Represents an invitation to a guild channel. + * <warn>The only guaranteed properties are `code`, `url`, `guild`, and `channel`. + * Other properties can be missing.</warn> + */ +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; + + /** + * The approximate number of online members of the guild this invite is for + * @type {number} + */ + this.presenceCount = data.approximate_presence_count; + + /** + * The approximate total number of members of the guild this invite is for + * @type {number} + */ + this.memberCount = data.approximate_member_count; + + /** + * The number of text channels the guild this invite goes to has + * @type {number} + */ + this.textChannelCount = data.guild.text_channel_count; + + /** + * The number of voice channels the guild this invite goes to has + * @type {number} + */ + this.voiceChannelCount = data.guild.voice_channel_count; + + /** + * 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. + * @param {string} [reason] Reason for deleting this invite + * @returns {Promise<Invite>} + */ + delete(reason) { + return this.client.rest.methods.deleteInvite(this, reason); + } + + /** + * 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..c71844f --- /dev/null +++ b/node_modules/discord.js/src/structures/Message.js @@ -0,0 +1,661 @@ +const Mentions = require('./MessageMentions'); +const Attachment = require('./MessageAttachment'); +const Embed = require('./MessageEmbed'); +const RichEmbed = require('./RichEmbed'); +const MessageReaction = require('./MessageReaction'); +const ReactionCollector = require('./ReactionCollector'); +const Util = require('../util/Util'); +const Collection = require('../util/Collection'); +const Constants = require('../util/Constants'); +const Permissions = require('../util/Permissions'); +const MessageFlags = require('../util/MessageFlags'); +let GuildMember; + +/** + * 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; + + /** + * Whether this message has been deleted + * @type {boolean} + */ + this.deleted = false; + + if (data) this.setup(data); + } + + setup(data) { // eslint-disable-line complexity + /** + * The ID of the message + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The type of the message + * @type {MessageType} + */ + 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, !data.webhook_id); + + /** + * 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 or string used for checking message delivery + * <warn>This is only received after the message was sent successfully, and + * lost if re-fetched</warn> + * @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 !== 0; + + /** + * 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<Snowflake, MessageAttachment>} + */ + 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; + + /** + * A collection of reactions to this message, mapped by the reaction ID + * @type {Collection<Snowflake, MessageReaction>} + */ + 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)); + } + } + + /** + * All valid mentions that the message contains + * @type {MessageMentions} + */ + this.mentions = new Mentions(this, data.mentions, data.mention_roles, data.mention_everyone, data.mention_channels); + + /** + * ID of the webhook that sent the message, if applicable + * @type {?Snowflake} + */ + this.webhookID = data.webhook_id || null; + + /** + * Whether this message is a hit in a search + * @type {?boolean} + */ + this.hit = typeof data.hit === 'boolean' ? data.hit : null; + + /** + * Flags that are applied to the message + * @type {Readonly<MessageFlags>} + */ + this.flags = new MessageFlags(data.flags).freeze(); + + /** + * Reference data sent in a crossposted message. + * @typedef {Object} MessageReference + * @property {string} channelID ID of the channel the message was crossposted from + * @property {?string} guildID ID of the guild the message was crossposted from + * @property {?string} messageID ID of the message that was crossposted + */ + + /** + * Message reference data + * @type {?MessageReference} + */ + this.reference = data.message_reference ? { + channelID: data.message_reference.channel_id, + guildID: data.message_reference.guild_id, + messageID: data.message_reference.message_id, + } : null; + + /** + * The previous versions of the message, sorted with the most recent first + * @type {Message[]} + * @private + */ + this._edits = []; + + if (data.member && this.guild && this.author && !this.guild.members.has(this.author.id)) { + this.guild._addMember(Object.assign(data.member, { user: this.author }), false); + } + + /** + * 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; + } + + /** + * Updates the message. + * @param {Object} data Raw Discord message update data + * @private + */ + patch(data) { + const clone = Util.cloneObject(this); + this._edits.unshift(clone); + + if ('edited_timestamp' in data) this.editedTimestamp = new Date(data.edited_timestamp).getTime(); + if ('content' in data) this.content = data.content; + if ('pinned' in data) this.pinned = data.pinned; + if ('tts' in data) this.tts = data.tts; + if ('embeds' in data) this.embeds = data.embeds.map(e => new Embed(this, e)); + else this.embeds = this.embeds.slice(); + + if ('attachments' in data) { + this.attachments = new Collection(); + for (const attachment of data.attachments) this.attachments.set(attachment.id, new Attachment(this, attachment)); + } else { + this.attachments = new Collection(this.attachments); + } + + this.mentions = new Mentions( + this, + 'mentions' in data ? data.mentions : this.mentions.users, + 'mentions_roles' in data ? data.mentions_roles : this.mentions.roles, + 'mention_everyone' in data ? data.mention_everyone : this.mentions.everyone, + 'mention_channels' in data ? data.mention_channels : this.mentions.crosspostedChannels + ); + + this.flags = new MessageFlags('flags' in data ? data.flags : 0).freeze(); + } + + /** + * 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 url to jump to the message + * @type {string} + * @readonly + */ + get url() { + return `https://discordapp.com/channels/${this.guild ? this.guild.id : '@me'}/${this.channel.id}/${this.id}`; + } + + /** + * 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; + }); + } + + /** + * Creates a reaction collector. + * @param {CollectorFilter} filter The filter to apply + * @param {ReactionCollectorOptions} [options={}] Options to send to the collector + * @returns {ReactionCollector} + * @example + * // Create a reaction collector + * const filter = (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someID' + * const collector = message.createReactionCollector(filter, { time: 15000 }); + * collector.on('collect', r => console.log(`Collected ${r.emoji.name}`)); + * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); + */ + createReactionCollector(filter, options = {}) { + return new ReactionCollector(this, filter, options); + } + + /** + * An object containing the same properties as CollectorOptions, but a few more: + * @typedef {ReactionCollectorOptions} AwaitReactionsOptions + * @property {string[]} [errors] Stop/end reasons that cause the promise to reject + */ + + /** + * Similar to createMessageCollector but in promise form. + * Resolves with a collection of reactions that pass the specified filter. + * @param {CollectorFilter} filter The filter function to use + * @param {AwaitReactionsOptions} [options={}] Optional options to pass to the internal collector + * @returns {Promise<Collection<string, MessageReaction>>} + * @example + * // Create a reaction collector + * const filter = (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someID' + * message.awaitReactions(filter, { time: 15000 }) + * .then(collected => console.log(`Collected ${collected.size} reactions`)) + * .catch(console.error); + */ + awaitReactions(filter, options = {}) { + return new Promise((resolve, reject) => { + const collector = this.createReactionCollector(filter, options); + collector.once('end', (reactions, reason) => { + if (options.errors && options.errors.includes(reason)) reject(reactions); + else resolve(reactions); + }); + }); + } + + /** + * 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.deleted && (this.author.id === this.client.user.id || (this.guild && + this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_MESSAGES) + )); + } + + /** + * Whether the message is pinnable by the client user + * @type {boolean} + * @readonly + */ + get pinnable() { + return this.type === 'DEFAULT' && (!this.guild || + this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.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 The member/user to check for a mention of + * @returns {boolean} + */ + isMemberMentioned(member) { + // Lazy-loading is used here to get around a circular dependency that breaks things + if (!GuildMember) GuildMember = require('./GuildMember'); + if (this.mentions.everyone) return true; + if (this.mentions.users.has(member.id)) return true; + if (member instanceof 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 + * @property {MessageFlagsResolvable} [flags] Message flags to apply + */ + + /** + * Edit the content of the message. + * @param {StringResolvable} [content] The new content for the message + * @param {MessageEditOptions|RichEmbed} [options] The options to provide + * @returns {Promise<Message>} + * @example + * // Update the content of a message + * message.edit('This is my new content!') + * .then(msg => console.log(`New message content: ${msg}`)) + * .catch(console.error); + */ + edit(content, options) { + if (!options && typeof content === 'object' && !(content instanceof Array)) { + options = content; + content = ''; + } else if (!options) { + options = {}; + } + if (options instanceof RichEmbed) options = { embed: options }; + return this.client.rest.methods.updateMessage(this, content, options); + } + + /** + * Edit the content of the message, with a code block. + * @param {string} lang The language for the code block + * @param {StringResolvable} content The new content for the message + * @returns {Promise<Message>} + * @deprecated + */ + editCode(lang, content) { + content = Util.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<Message>} + */ + pin() { + return this.client.rest.methods.pinMessage(this); + } + + /** + * Unpins this message from the channel's pinned messages. + * @returns {Promise<Message>} + */ + unpin() { + return this.client.rest.methods.unpinMessage(this); + } + + /** + * Add a reaction to the message. + * @param {string|Emoji|ReactionEmoji} emoji The emoji to react with + * @returns {Promise<MessageReaction>} + * @example + * // React to a message with a unicode emoji + * message.react('🤔') + * .then(console.log) + * .catch(console.error); + * @example + * // React to a message with a custom emoji + * message.react(message.guild.emojis.get('123456789012345678')) + * .then(console.log) + * .catch(console.error); + */ + 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<Message>} + */ + 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<Message>} + * @example + * // Delete a message + * message.delete() + * .then(msg => console.log(`Deleted message from ${msg.author.username}`)) + * .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<Message|Message[]>} + * @example + * // Reply to a message + * message.reply('Hey, I\'m a reply!') + * .then(sent => console.log(`Sent a reply to ${sent.author.username}`)) + * .catch(console.error); + */ + reply(content, options) { + if (!options && typeof content === 'object' && !(content instanceof Array)) { + options = content; + content = ''; + } else if (!options) { + options = {}; + } + return this.channel.send(content, Object.assign(options, { reply: this.member || this.author })); + } + + /** + * Marks the message as read. + * <warn>This is only available when using a user account.</warn> + * @returns {Promise<Message>} + * @deprecated + */ + acknowledge() { + return this.client.rest.methods.ackMessage(this); + } + + /** + * Fetches the webhook used to create this message. + * @returns {Promise<?Webhook>} + */ + fetchWebhook() { + if (!this.webhookID) return Promise.reject(new Error('The message was not sent by a webhook.')); + return this.client.fetchWebhook(this.webhookID); + } + + /** + * Suppresses or unsuppresses embeds on a message + * @param {boolean} [suppress=true] If the embeds should be suppressed or not + * @returns {Promise<Message>} + */ + suppressEmbeds(suppress = true) { + const flags = new MessageFlags(this.flags.bitfield); + + if (suppress) { + flags.add(MessageFlags.FLAGS.SUPPRESS_EMBEDS); + } else { + flags.remove(MessageFlags.FLAGS.SUPPRESS_EMBEDS); + } + + return this.edit(undefined, { flags }); + } + + /** + * 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; + } + + _removeReaction(emoji, user) { + const emojiID = emoji.id ? `${emoji.name}:${emoji.id}` : emoji.name; + if (this.reactions.has(emojiID)) { + const reaction = this.reactions.get(emojiID); + if (!user) { + this.reactions.delete(emojiID); + return reaction; + } + if (reaction.users.has(user.id)) { + reaction.users.delete(user.id); + reaction.count--; + if (user.id === this.client.user.id) reaction.me = false; + if (reaction.count <= 0) this.reactions.delete(emojiID); + 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..6db15c8 --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageAttachment.js @@ -0,0 +1,79 @@ +const { basename } = require('path'); + +/** + * 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 {Snowflake} + */ + 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; + } + + /** + * Whether or not this attachment has been marked as a spoiler + * @type {boolean} + * @readonly + */ + get spoiler() { + return basename(this.url).startsWith('SPOILER_'); + } +} + +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..acd82be --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageCollector.js @@ -0,0 +1,97 @@ +const Collector = require('./interfaces/Collector'); +const util = require('util'); + +/** + * @typedef {CollectorOptions} MessageCollectorOptions + * @property {number} max The maximum amount of messages to process + * @property {number} maxMatches The maximum amount of messages to collect + */ + +/** + * Collects messages on a channel. + * @extends {Collector} + */ +class MessageCollector extends Collector { + /** + * @param {TextChannel|DMChannel|GroupDMChannel} channel The channel + * @param {CollectorFilter} filter The filter to be applied to this collector + * @param {MessageCollectorOptions} options The options to be applied to this collector + * @emits MessageCollector#message + */ + constructor(channel, filter, options = {}) { + super(channel.client, filter, options); + + /** + * The channel + * @type {TextBasedChannel} + */ + this.channel = channel; + + /** + * Total number of messages that were received in the channel during message collection + * @type {number} + */ + this.received = 0; + + if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() + 1); + this.client.on('message', this.listener); + + this._reEmitter = message => { + /** + * Emitted when the collector receives a message. + * @event MessageCollector#message + * @param {Message} message The message + * @deprecated + */ + this.emit('message', message); + }; + this.on('collect', this._reEmitter); + } + + // Remove in v12 + on(eventName, listener) { + if (eventName === 'message') { + listener = util.deprecate(listener, 'MessageCollector will soon no longer emit "message", use "collect" instead'); + } + super.on(eventName, listener); + } + + /** + * Handle an incoming message for possible collection. + * @param {Message} message The message that could be collected + * @returns {?{key: Snowflake, value: Message}} + * @private + */ + handle(message) { + if (message.channel.id !== this.channel.id) return null; + this.received++; + return { + key: message.id, + value: message, + }; + } + + /** + * Check after collection to see if the collector is done. + * @returns {?string} Reason to end the collector, if any + * @private + */ + postCheck() { + // Consider changing the end reasons for v12 + if (this.options.maxMatches && this.collected.size >= this.options.maxMatches) return 'matchesLimit'; + if (this.options.max && this.received >= this.options.max) return 'limit'; + return null; + } + + /** + * Removes event listeners. + * @private + */ + cleanup() { + this.removeListener('collect', this._reEmitter); + this.client.removeListener('message', this.listener); + if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() - 1); + } +} + +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..2a87679 --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageEmbed.js @@ -0,0 +1,386 @@ +/** + * Represents an embed in a message (image/video preview, rich embed, etc.) + * <info>This class is only used for *received* embeds. If you wish to send one, use the {@link RichEmbed} class.</info> + */ +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 + * @type {?string} + */ + this.title = data.title; + + /** + * The description of this embed + * @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.timestamp = data.timestamp; + + /** + * The thumbnail of this embed + * @type {?MessageEmbedThumbnail} + */ + this.thumbnail = data.thumbnail ? new MessageEmbedThumbnail(this, data.thumbnail) : null; + + /** + * The image of this embed + * @type {?MessageEmbedImage} + */ + this.image = data.image ? new MessageEmbedImage(this, data.image) : null; + + /** + * The video of this embed + * @type {?MessageEmbedVideo} + */ + this.video = data.video ? new MessageEmbedVideo(this, data.video) : null; + + /** + * The author of this embed + * @type {?MessageEmbedAuthor} + */ + this.author = data.author ? new MessageEmbedAuthor(this, data.author) : null; + + /** + * The provider of this embed + * @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} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The hexadecimal version of the embed color, with a leading hash + * @type {?string} + * @readonly + */ + get hexColor() { + if (!this.color) return null; + 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 an image for a message embed. + */ +class MessageEmbedImage { + constructor(embed, data) { + /** + * The embed this image is part of + * @type {MessageEmbed} + */ + this.embed = embed; + + this.setup(data); + } + + setup(data) { + /** + * The URL for this image + * @type {string} + */ + this.url = data.url; + + /** + * The Proxy URL for this image + * @type {string} + */ + this.proxyURL = data.proxy_url; + + /** + * The height of the image + * @type {number} + */ + this.height = data.height; + + /** + * The width of the image + * @type {number} + */ + this.width = data.width; + } +} + +/** + * Represents a video for a message embed. + */ +class MessageEmbedVideo { + constructor(embed, data) { + /** + * The embed this video is part of + * @type {MessageEmbed} + */ + this.embed = embed; + + this.setup(data); + } + + setup(data) { + /** + * The source URL for this video + * @type {string} + */ + this.url = data.url; + + /** + * The height of the video + * @type {number} + */ + this.height = data.height; + + /** + * The width of the video + * @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.Image = MessageEmbedImage; +MessageEmbed.Video = MessageEmbedVideo; +MessageEmbed.Provider = MessageEmbedProvider; +MessageEmbed.Author = MessageEmbedAuthor; +MessageEmbed.Field = MessageEmbedField; +MessageEmbed.Footer = MessageEmbedFooter; + +module.exports = MessageEmbed; diff --git a/node_modules/discord.js/src/structures/MessageMentions.js b/node_modules/discord.js/src/structures/MessageMentions.js new file mode 100644 index 0000000..47aff55 --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageMentions.js @@ -0,0 +1,185 @@ +const Collection = require('../util/Collection'); +const { ChannelTypes } = require('../util/Constants'); + +/** + * Keeps track of mentions in a {@link Message}. + */ +class MessageMentions { + constructor(message, users, roles, everyone, crosspostedChannels) { + /** + * Whether `@everyone` or `@here` were mentioned + * @type {boolean} + */ + this.everyone = Boolean(everyone); + + if (users) { + if (users instanceof Collection) { + /** + * Any users that were mentioned + * <info>Order as received from the API, not as they appear in the message content</info> + * @type {Collection<Snowflake, User>} + */ + this.users = new Collection(users); + } else { + this.users = new Collection(); + for (const mention of users) { + let user = message.client.users.get(mention.id); + if (!user) user = message.client.dataManager.newUser(mention); + this.users.set(user.id, user); + if (mention.member && message.guild && !message.guild.members.has(mention.id)) { + message.guild._addMember(Object.assign(mention.member, { user }), false); + } + } + } + } else { + this.users = new Collection(); + } + + if (roles) { + if (roles instanceof Collection) { + /** + * Any roles that were mentioned + * <info>Order as received from the API, not as they appear in the message content</ + * @type {Collection<Snowflake, Role>} + */ + this.roles = new Collection(roles); + } else { + this.roles = new Collection(); + for (const mention of roles) { + const role = message.channel.guild.roles.get(mention); + if (role) this.roles.set(role.id, role); + } + } + } else { + this.roles = new Collection(); + } + + /** + * Content of the message + * @type {Message} + * @private + */ + this._content = message.content; + + /** + * The client the message is from + * @type {Client} + * @private + */ + this._client = message.client; + + /** + * The guild the message is in + * @type {?Guild} + * @private + */ + this._guild = message.channel.guild; + + /** + * Cached members for {@MessageMention#members} + * @type {?Collection<Snowflake, GuildMember>} + * @private + */ + this._members = null; + + /** + * Cached channels for {@MessageMention#channels} + * @type {?Collection<Snowflake, GuildChannel>} + * @private + */ + this._channels = null; + + /** + * Crossposted channel data. + * @typedef {Object} CrosspostedChannel + * @property {Snowflake} channelID ID of the mentioned channel + * @property {Snowflake} guildID ID of the guild that has the channel + * @property {string} type Type of the channel + * @property {string} name Name of the channel + */ + + if (crosspostedChannels) { + if (crosspostedChannels instanceof Collection) { + /** + * A collection of crossposted channels + * @type {Collection<Snowflake, CrosspostedChannel>} + */ + this.crosspostedChannels = new Collection(crosspostedChannels); + } else { + this.crosspostedChannels = new Collection(); + const channelTypes = Object.keys(ChannelTypes); + for (const d of crosspostedChannels) { + const type = channelTypes[d.type]; + this.crosspostedChannels.set(d.id, { + channelID: d.id, + guildID: d.guild_id, + type: type ? type.toLowerCase() : 'unknown', + name: d.name, + }); + } + } + } else { + this.crosspostedChannels = new Collection(); + } + } + + /** + * Any members that were mentioned (only in {@link TextChannel}s) + * <info>Order as received from the API, not as they appear in the message content</ + * @type {?Collection<Snowflake, GuildMember>} + * @readonly + */ + get members() { + if (this._members) return this._members; + if (!this._guild) return null; + this._members = new Collection(); + this.users.forEach(user => { + const member = this._guild.member(user); + if (member) this._members.set(member.user.id, member); + }); + return this._members; + } + + /** + * Any channels that were mentioned + * <info>Order as they appear first in the message content</info> + * @type {Collection<Snowflake, GuildChannel>} + * @readonly + */ + get channels() { + if (this._channels) return this._channels; + this._channels = new Collection(); + let matches; + while ((matches = this.constructor.CHANNELS_PATTERN.exec(this._content)) !== null) { + const chan = this._client.channels.get(matches[1]); + if (chan) this._channels.set(chan.id, chan); + } + return this._channels; + } +} + +/** + * Regular expression that globally matches `@everyone` and `@here` + * @type {RegExp} + */ +MessageMentions.EVERYONE_PATTERN = /@(everyone|here)/g; + +/** + * Regular expression that globally matches user mentions like `<@81440962496172032>` + * @type {RegExp} + */ +MessageMentions.USERS_PATTERN = /<@!?[0-9]+>/g; + +/** + * Regular expression that globally matches role mentions like `<@&297577916114403338>` + * @type {RegExp} + */ +MessageMentions.ROLES_PATTERN = /<@&[0-9]+>/g; + +/** + * Regular expression that globally matches channel mentions like `<#222079895583457280>` + * @type {RegExp} + */ +MessageMentions.CHANNELS_PATTERN = /<#([0-9]+)>/g; + +module.exports = MessageMentions; 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..9109e18 --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageReaction.js @@ -0,0 +1,107 @@ +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<Snowflake, User>} + */ + this.users = new Collection(); + + this._emoji = new ReactionEmoji(this, emoji); + } + + /** + * 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} + * @readonly + */ + 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] The user to remove the reaction of + * @returns {Promise<MessageReaction>} + */ + remove(user = this.message.client.user) { + const message = this.message; + const userID = this.message.client.resolver.resolveUserID(user); + if (!userID) return Promise.reject(new Error('Couldn\'t resolve the user ID to remove from the reaction.')); + return message.client.rest.methods.removeMessageReaction( + message, this.emoji.identifier, userID + ); + } + + /** + * Removes this reaction from the message + * @returns {Promise<MessageReaction>} + */ + removeAll() { + const message = this.message; + return message.client.rest.methods.removeMessageReactionEmoji( + message, this.emoji.identifier + ); + } + + /** + * 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 + * @param {Object} [options] Options to fetch users + * @param {Snowflake} [options.before] Limit fetching users to those with an id lower than the supplied id + * @param {Snowflake} [options.after] Limit fetching users to those with an id greater than the supplied id + * @returns {Promise<Collection<Snowflake, User>>} + */ + fetchUsers(limit = 100, { after, before } = {}) { + const message = this.message; + return message.client.rest.methods.getMessageReactionUsers( + message, this.emoji.identifier, { after, before, limit } + ).then(data => { + const users = new Collection(); + for (const rawUser of data) { + const user = this.message.client.dataManager.newUser(rawUser); + this.users.set(user.id, user); + users.set(user.id, user); + } + return users; + }); + } +} + +module.exports = MessageReaction; diff --git a/node_modules/discord.js/src/structures/NewsChannel.js b/node_modules/discord.js/src/structures/NewsChannel.js new file mode 100644 index 0000000..bcbfbf3 --- /dev/null +++ b/node_modules/discord.js/src/structures/NewsChannel.js @@ -0,0 +1,24 @@ +const TextChannel = require('./TextChannel'); + +/** + * Represents a guild news channel on Discord. + * @extends {TextChannel} + */ +class NewsChannel extends TextChannel { + constructor(guild, data) { + super(guild, data); + this.type = 'news'; + } + + setup(data) { + super.setup(data); + + /** + * The ratelimit per user for this channel (always 0) + * @type {number} + */ + this.rateLimitPerUser = 0; + } +} + +module.exports = NewsChannel; 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..710aae8 --- /dev/null +++ b/node_modules/discord.js/src/structures/OAuth2Application.js @@ -0,0 +1,157 @@ +const Snowflake = require('../util/Snowflake'); +const Team = require('./Team'); +const util = require('util'); + +/** + * 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 {Snowflake} + */ + 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 {?string[]} + */ + this.rpcOrigins = data.rpc_origins; + + /** + * The app's redirect URIs + * @type {string[]} + */ + this.redirectURIs = data.redirect_uris; + + /** + * If this app's bot requires a code grant when using the OAuth2 flow + * @type {boolean} + */ + this.botRequireCodeGrant = data.bot_require_code_grant; + + /** + * If this app's bot is public + * @type {boolean} + */ + this.botPublic = data.bot_public; + + /** + * If this app can use rpc + * @type {boolean} + */ + this.rpcApplicationState = data.rpc_application_state; + + /** + * Object containing basic info about this app's bot + * @type {Object} + */ + this.bot = data.bot; + + /** + * The flags for the app + * @type {number} + */ + this.flags = data.flags; + + /** + * OAuth2 secret for the application + * @type {boolean} + */ + this.secret = data.secret; + + if (data.owner) { + /** + * The owner of this OAuth application + * @type {?User} + */ + this.owner = this.client.dataManager.newUser(data.owner); + } + + /** + * The owning team of this OAuth application + * <info>In v12.0.0 this property moves to `Team#owner`.</info> + * @type {?Team} + * @deprecated + */ + this.team = data.team ? new Team(this.client, data.team) : null; + } + + /** + * The timestamp the app was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * The time the app was created + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * Reset the app's secret and bot token. + * <warn>This is only available when using a user account.</warn> + * @returns {OAuth2Application} + * @deprecated + */ + reset() { + return this.client.rest.methods.resetApplication(this.id); + } + + /** + * When concatenated with a string, this automatically concatenates the app name rather than the app object. + * @returns {string} + */ + toString() { + return this.name; + } +} + +OAuth2Application.prototype.reset = + util.deprecate(OAuth2Application.prototype.reset, 'OAuth2Application#reset: userbot methods will be removed'); + +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..3eb64f2 --- /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 {Snowflake} + */ + this.id = data.id; + + /** + * The name of this guild + * @type {string} + */ + this.name = data.name; + + /** + * The hash of this guild's icon + * @type {?string} + */ + this.icon = data.icon; + + /** + * The hash of the guild splash image (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..c30c054 --- /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 {Snowflake} + */ + 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..ebb78fb --- /dev/null +++ b/node_modules/discord.js/src/structures/PermissionOverwrites.js @@ -0,0 +1,69 @@ +const Permissions = require('../util/Permissions'); + +/** + * 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 {Snowflake} + */ + this.id = data.id; + + /** + * The type of this overwrite + * @type {string} + */ + this.type = data.type; + + /** + * The permissions that are denied for the user or role as a bitfield. + * @type {number} + */ + this.deny = data.deny; + + /** + * The permissions that are allowed for the user or role as a bitfield. + * @type {number} + */ + this.allow = data.allow; + + /** + * The permissions that are denied for the user or role. + * @type {Permissions} + * @deprecated + */ + this.denied = new Permissions(data.deny).freeze(); + + /** + * The permissions that are allowed for the user or role. + * @type {Permissions} + * @deprecated + */ + this.allowed = new Permissions(data.allow).freeze(); + } + + /** + * Delete this Permission Overwrite. + * @param {string} [reason] Reason for deleting this overwrite + * @returns {Promise<PermissionOverwrites>} + */ + delete(reason) { + return this.channel.client.rest.methods.deletePermissionOverwrites(this, reason); + } +} + +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..18cc3cc --- /dev/null +++ b/node_modules/discord.js/src/structures/Presence.js @@ -0,0 +1,301 @@ +const { ActivityFlags, Endpoints } = require('../util/Constants'); +const ReactionEmoji = require('./ReactionEmoji'); + +/** + * The status of this presence: + * * **`online`** - user is online + * * **`idle`** - user is AFK + * * **`offline`** - user is offline or invisible + * * **`dnd`** - user is in Do Not Disturb + * @typedef {string} PresenceStatus + */ + +/** + * The status of this presence: + * * **`online`** - user is online + * * **`idle`** - user is AFK + * * **`dnd`** - user is in Do Not Disturb + * @typedef {string} ClientPresenceStatus + */ + +/** + * Represents a user's presence. + */ +class Presence { + constructor(data = {}, client) { + /** + * The client that instantiated this + * @name Presence#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + this.update(data); + } + + update(data) { + /** + * The status of this presence: + * @type {PresenceStatus} + */ + this.status = data.status || this.status || 'offline'; + + /** + * The game that the user is playing + * @type {?Game} + * @deprecated + */ + this.game = data.game ? new Game(data.game, this) : null; + + if (data.activities) { + /** + * The activities of this presence + * @type {Game[]} + */ + this.activities = data.activities.map(activity => new Game(activity, this)); + } else if (data.activity || data.game) { + this.activities = [new Game(data.activity || data.game, this)]; + } else { + this.activities = []; + } + + /** + * The devices this presence is on + * @type {?Object} + * @property {?ClientPresenceStatus} web The current presence in the web application + * @property {?ClientPresenceStatus} mobile The current presence in the mobile application + * @property {?ClientPresenceStatus} desktop The current presence in the desktop application + */ + this.clientStatus = data.client_status || null; + } + + /** + * Whether this presence is equal to another + * @param {Presence} presence The presence to compare with + * @returns {boolean} + */ + equals(presence) { + return this === presence || ( + presence && + this.status === presence.status && + this.activities.length === presence.activities.length && + this.activities.every((activity, index) => activity.equals(presence.activities[index])) && + this.clientStatus.web === presence.clientStatus.web && + this.clientStatus.mobile === presence.clientStatus.mobile && + this.clientStatus.desktop === presence.clientStatus.desktop + ); + } +} + +/** + * Represents a game that is part of a user's presence. + */ +class Game { + constructor(data, presence) { + Object.defineProperty(this, 'presence', { value: presence }); + + /** + * The name of the game being played + * @type {string} + */ + this.name = data.name; + + /** + * The type of the game status, its possible values: + * - 0: Playing + * - 1: Streaming + * - 2: Listening + * - 3: Watching + * @type {number} + */ + this.type = data.type; + + /** + * If the game is being streamed, a link to the stream + * @type {?string} + */ + this.url = data.url || null; + + /** + * Details about the activity + * @type {?string} + */ + this.details = data.details || null; + + /** + * State of the activity + * @type {?string} + */ + this.state = data.state || null; + + /** + * Application ID associated with this activity + * @type {?Snowflake} + */ + this.applicationID = data.application_id || null; + + /** + * Timestamps for the activity + * @type {?Object} + * @prop {?Date} start When the activity started + * @prop {?Date} end When the activity will end + */ + this.timestamps = data.timestamps ? { + start: data.timestamps.start ? new Date(Number(data.timestamps.start)) : null, + end: data.timestamps.end ? new Date(Number(data.timestamps.end)) : null, + } : null; + + /** + * Party of the activity + * @type {?Object} + * @prop {?string} id ID of the party + * @prop {number[]} size Size of the party as `[current, max]` + */ + this.party = data.party || null; + + /** + * Assets for rich presence + * @type {?RichPresenceAssets} + */ + this.assets = data.assets ? new RichPresenceAssets(this, data.assets) : null; + + if (data.emoji) { + /** + * Emoji for a custom activity + * <warn>There is no `reaction` property for this emoji.</warn> + * @type {?ReactionEmoji} + */ + this.emoji = new ReactionEmoji({ message: { client: this.presence.client } }, data.emoji); + this.emoji.reaction = null; + } else { + this.emoji = null; + } + + + /** + * Creation date of the activity + * @type {number} + */ + this.createdTimestamp = new Date(data.created_at).getTime(); + + this.syncID = data.sync_id; + this._flags = data.flags; + } + + /** + * The time the activity was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * Flags that describe the activity + * @type {ActivityFlags[]} + */ + get flags() { + const flags = []; + for (const [name, flag] of Object.entries(ActivityFlags)) { + if ((this._flags & flag) === flag) flags.push(name); + } + return flags; + } + + /** + * Whether or not the game is being streamed + * @type {boolean} + * @readonly + */ + get streaming() { + return this.type === 1; + } + + /** + * When concatenated with a string, this automatically returns the game's name instead of the Game object. + * @returns {string} + */ + toString() { + return this.name; + } + + /** + * Whether this game is equal to another game + * @param {Game} game The game to compare with + * @returns {boolean} + */ + equals(game) { + return this === game || ( + game && + this.name === game.name && + this.type === game.type && + this.url === game.url + ); + } +} + +/** + * Assets for a rich presence + */ +class RichPresenceAssets { + constructor(game, assets) { + Object.defineProperty(this, 'game', { value: game }); + + /** + * Hover text for the large image + * @type {?string} + */ + this.largeText = assets.large_text || null; + + /** + * Hover text for the small image + * @type {?string} + */ + this.smallText = assets.small_text || null; + + /** + * ID of the large image asset + * @type {?Snowflake} + */ + this.largeImage = assets.large_image || null; + + /** + * ID of the small image asset + * @type {?Snowflake} + */ + this.smallImage = assets.small_image || null; + } + + /** + * The URL of the small image asset + * @type {?string} + * @readonly + */ + get smallImageURL() { + if (!this.smallImage) return null; + return Endpoints.CDN(this.game.presence.client.options.http.cdn) + .AppAsset(this.game.applicationID, this.smallImage); + } + + /** + * The URL of the large image asset + * @type {?string} + * @readonly + */ + get largeImageURL() { + if (!this.largeImage) return null; + if (/^spotify:/.test(this.largeImage)) { + return `https://i.scdn.co/image/${this.largeImage.slice(8)}`; + } else if (/^twitch:/.test(this.largeImage)) { + return `https://static-cdn.jtvnw.net/previews-ttv/live_user_${this.largeImage.slice(7)}.png`; + } + return Endpoints.CDN(this.game.presence.client.options.http.cdn) + .AppAsset(this.game.applicationID, this.largeImage); + } +} + +exports.Presence = Presence; +exports.Game = Game; +exports.RichPresenceAssets = RichPresenceAssets; diff --git a/node_modules/discord.js/src/structures/ReactionCollector.js b/node_modules/discord.js/src/structures/ReactionCollector.js new file mode 100644 index 0000000..2b3235e --- /dev/null +++ b/node_modules/discord.js/src/structures/ReactionCollector.js @@ -0,0 +1,89 @@ +const Collector = require('./interfaces/Collector'); +const Collection = require('../util/Collection'); + +/** + * @typedef {CollectorOptions} ReactionCollectorOptions + * @property {number} max The maximum total amount of reactions to collect + * @property {number} maxEmojis The maximum number of emojis to collect + * @property {number} maxUsers The maximum number of users to react + */ + +/** + * Collects reactions on messages. + * @extends {Collector} + */ +class ReactionCollector extends Collector { + /** + * @param {Message} message The message upon which to collect reactions + * @param {CollectorFilter} filter The filter to apply to this collector + * @param {ReactionCollectorOptions} [options={}] The options to apply to this collector + */ + constructor(message, filter, options = {}) { + super(message.client, filter, options); + + /** + * The message + * @type {Message} + */ + this.message = message; + + /** + * The users which have reacted + * @type {Collection} + */ + this.users = new Collection(); + + /** + * The total number of reactions collected + * @type {number} + */ + this.total = 0; + + if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() + 1); + this.client.on('messageReactionAdd', this.listener); + + this.on('fullCollect', (reaction, user) => { + this.users.set(user.id, user); + this.total++; + }); + } + + /** + * Handle an incoming reaction for possible collection. + * @param {MessageReaction} reaction The reaction to possibly collect + * @returns {?{key: Snowflake, value: MessageReaction}} + * @private + */ + handle(reaction) { + if (reaction.message.id !== this.message.id) return null; + return { + key: reaction.emoji.id || reaction.emoji.name, + value: reaction, + }; + } + + /** + * Check after collection to see if the collector is done. + * @param {MessageReaction} reaction The reaction that was collected + * @param {User} user The user that reacted + * @returns {?string} Reason to end the collector, if any + * @private + */ + postCheck() { + if (this.options.max && this.total >= this.options.max) return 'limit'; + if (this.options.maxEmojis && this.collected.size >= this.options.maxEmojis) return 'emojiLimit'; + if (this.options.maxUsers && this.users.size >= this.options.maxUsers) return 'userLimit'; + return null; + } + + /** + * Remove event listeners. + * @private + */ + cleanup() { + this.client.removeListener('messageReactionAdd', this.listener); + if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() - 1); + } +} + +module.exports = ReactionCollector; 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..9f7597e --- /dev/null +++ b/node_modules/discord.js/src/structures/ReactionEmoji.js @@ -0,0 +1,98 @@ +const Constants = require('../util/Constants'); +const Snowflake = require('../util/Snowflake'); + +/** + * 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, emoji) { + /** + * The client that instantiated this object + * @name ReactionEmoji#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: reaction.message.client }); + + /** + * The message reaction this emoji refers to + * @type {MessageReaction} + */ + this.reaction = reaction; + + /** + * The name of this reaction emoji + * @type {string} + */ + this.name = emoji.name; + + /** + * The ID of this reaction emoji + * @type {?Snowflake} + */ + this.id = emoji.id; + + /** + * Whether this reaction emoji is animated + * @type {boolean} + */ + this.animated = emoji.animated || false; + } + + /** + * The timestamp the reaction emoji was created at, or null if unicode + * @type {?number} + * @readonly + */ + get createdTimestamp() { + if (!this.id) return null; + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * The time the reaction emoji was created, or null if unicode + * @type {?Date} + * @readonly + */ + get createdAt() { + if (!this.id) return null; + return new Date(this.createdTimestamp); + } + + /** + * The URL to the reaction emoji file, or null if unicode + * @type {string} + * @readonly + */ + get url() { + if (!this.id) return null; + return Constants.Endpoints.CDN(this.client.options.http.cdn).Emoji(this.id, this.animated ? 'gif' : 'png'); + } + + /** + * The identifier of this emoji, used for message reactions + * @type {string} + * @readonly + */ + get identifier() { + if (this.id) return `${this.name}:${this.id}`; + return encodeURIComponent(this.name); + } + + /** + * 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.send(`The emoji used is ${reaction.emoji}`); + * @returns {string} + */ + toString() { + if (!this.id) return this.name; + + return `<${this.animated ? 'a' : ''}:${this.name}:${this.id}>`; + } +} + +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..5ccb22d --- /dev/null +++ b/node_modules/discord.js/src/structures/RichEmbed.js @@ -0,0 +1,331 @@ +const Attachment = require('./Attachment'); +const MessageEmbed = require('./MessageEmbed'); +const util = require('../util/Util'); +let ClientDataResolver; + +/** + * A rich embed to be sent with a message with a fluent interface for creation. + * @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; + + /** + * File to upload alongside this Embed + * @type {FileOptions|string|Attachment} + */ + this.file = data.file; + + /** + * The files to upload alongside this Embed + * @type {Array<FileOptions|string|Attachment>} + */ + this.files = []; + } + + /** + * Sets the title of this embed. + * @param {StringResolvable} title The title + * @returns {RichEmbed} This embed + */ + setTitle(title) { + title = util.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 = util.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 {ColorResolvable} color The color of the embed + * @returns {RichEmbed} This embed + */ + setColor(color) { + if (!ClientDataResolver) ClientDataResolver = require('../client/ClientDataResolver'); + this.color = ClientDataResolver.resolveColor(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: util.resolveString(name), icon_url: icon, url }; + return this; + } + + /** + * Sets the timestamp of this embed. + * @param {Date|number} [timestamp=Date.now()] The timestamp or date + * @returns {RichEmbed} This embed + */ + setTimestamp(timestamp = Date.now()) { + if (timestamp instanceof Date) timestamp = timestamp.getTime(); + 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.'); + this.fields.push(this.constructor.normalizeField(name, value, inline)); + return this; + } + + /** + * Convenience function for `<RichEmbed>.addField('\u200B', '\u200B', inline)`. + * @param {boolean} [inline=false] Set the field to display inline + * @returns {RichEmbed} This embed + */ + addBlankField(inline = false) { + return this.addField('\u200B', '\u200B', inline); + } + + /** + * @typedef {Object} EmbedField + * @property {string} name The name of this field + * @property {string} value The value of this field + * @property {boolean} inline If this field will be displayed inline + */ + + /** + * @typedef {Object} EmbedFieldData + * @property {StringResolvable} name The name of this field + * @property {StringResolvable} value The value of this field + * @property {boolean} [inline=false] If this field will be displayed inline + */ + + /** + * Removes, replaces, and inserts fields in the embed (max 25). + * @param {number} index The index to start at + * @param {number} deleteCount The number of fields to remove + * @param {...EmbedFieldData} [fields] The replacing field objects + * @returns {RichEmbed} + */ + spliceFields(index, deleteCount, ...fields) { + if (fields) { + const mapper = ({ name, value, inline }) => this.constructor.normalizeField(name, value, inline); + this.fields.splice(index, deleteCount, ...fields.map(mapper)); + } else { + this.fields.splice(index, deleteCount); + } + 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 image + * @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 = util.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; + } + + /** + * Sets the file to upload alongside the embed. This file can be accessed via `attachment://fileName.extension` when + * setting an embed image or author/footer icons. Only one file may be attached. + * @param {FileOptions|string|Attachment} file Local path or URL to the file to attach, + * or valid FileOptions for a file to attach + * @returns {RichEmbed} This embed + */ + attachFile(file) { + if (this.file) throw new RangeError('You may not upload more than one file at once.'); + if (file instanceof Attachment) file = file.file; + this.file = file; + return this; + } + + /** + * Sets the files to upload alongside the embed. A file can be accessed via `attachment://fileName.extension` when + * setting an embed image or author/footer icons. Multiple files can be attached. + * @param {Array<FileOptions|string|Attachment>} files Files to attach + * @returns {RichEmbed} + */ + attachFiles(files) { + files = files.map(file => file instanceof Attachment ? file.file : file); + this.files = this.files.concat(files); + return this; + } + + /** + * The accumulated length for the embed title, description, fields, author and footer text + * @type {number} + * @readonly + */ + get length() { + return ( + (this.title ? this.title.length : 0) + + (this.description ? this.description.length : 0) + + (this.fields.length >= 1 ? this.fields.reduce((prev, curr) => + prev + curr.name.length + curr.value.length, 0) : 0) + + (this.footer ? this.footer.text.length : 0) + + (this.author ? this.author.name.length : 0)); + } + + /** + * Transforms the embed to a plain object. + * @returns {Object} The raw data of this embed + */ + toJSON() { + return { + title: this.title, + type: 'rich', + description: this.description, + url: this.url, + timestamp: this.timestamp ? new Date(this.timestamp) : null, + color: this.color, + fields: this.fields ? + this.fields.map(field => ({ name: field.name, value: field.value, inline: field.inline })) : + null, + thumbnail: this.thumbnail ? { + url: this.thumbnail.url, + } : null, + image: this.image ? { + url: this.image.url, + } : null, + author: this.author ? { + name: this.author.name, + url: this.author.url, + icon_url: this.author instanceof MessageEmbed.Author ? this.author.iconURL : this.author.icon_url, + } : null, + footer: this.footer ? { + text: this.footer.text, + icon_url: this.footer instanceof MessageEmbed.Footer ? this.footer.iconURL : this.footer.icon_url, + } : null, + }; + } + + /** + * Normalizes field input and resolves strings. + * @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 {EmbedField} + */ + static normalizeField(name, value, inline = false) { + name = util.resolveString(name); + if (name.length > 256) throw new RangeError('RichEmbed field names may not exceed 256 characters.'); + if (!/\S/.test(name)) throw new RangeError('RichEmbed field names may not be empty.'); + value = util.resolveString(value); + if (value.length > 1024) throw new RangeError('RichEmbed field values may not exceed 1024 characters.'); + if (!/\S/.test(value)) throw new RangeError('RichEmbed field values may not be empty.'); + return { name, value, inline }; + } +} + +module.exports = RichEmbed; 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..056cf98 --- /dev/null +++ b/node_modules/discord.js/src/structures/Role.js @@ -0,0 +1,376 @@ +const Snowflake = require('../util/Snowflake'); +const Permissions = require('../util/Permissions'); +const util = require('util'); + +/** + * 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; + + /** + * Whether the role has been deleted + * @type {boolean} + */ + this.deleted = false; + + if (data) this.setup(data); + } + + setup(data) { + /** + * The ID of the role (unique to the guild it is part of) + * @type {Snowflake} + */ + 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 from the API + * @type {number} + */ + this.position = data.position; + + /** + * The permissions bitfield of the role + * @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 Snowflake.deconstruct(this.id).timestamp; + } + + /** + * 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<Snowflake, GuildMember>} + * @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.permissions.has(Permissions.FLAGS.MANAGE_ROLES_OR_PERMISSIONS)) return false; + return clientMember.highestRole.comparePositionTo(this) > 0; + } + + /** + * The position of the role in the role manager + * @type {number} + * @readonly + */ + get calculatedPosition() { + const sorted = this.guild._sortedRoles; + return sorted.array().indexOf(sorted.get(this.id)); + } + + /** + * Get an object mapping permission names to whether or not the role enables that permission. + * @returns {Object<string, boolean>} + * @example + * // Print the serialized role permissions + * console.log(role.serialize()); + */ + serialize() { + return new Permissions(this.permissions).serialize(); + } + + /** + * Checks if the role has a permission. + * @param {PermissionResolvable} permission Permission(s) to check for + * @param {boolean} [explicit=false] Whether to require the role to explicitly have the exact permission + * **(deprecated)** + * @param {boolean} [checkAdmin] Whether to allow the administrator permission to override + * (takes priority over `explicit`) + * @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, checkAdmin) { + return new Permissions(this.permissions).has( + permission, typeof checkAdmin !== 'undefined' ? checkAdmin : !explicit + ); + } + + /** + * 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} + * @deprecated + */ + hasPermissions(permissions, explicit = false) { + return new Permissions(this.permissions).has(permissions, !explicit); + } + + /** + * Compares this role's position to another role's. + * @param {Role} role Role to compare to this one + * @returns {number} Negative number if this role's position is lower (other role's is higher), + * positive number if 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 {ColorResolvable} [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 {PermissionResolvable|number} [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 + * @param {string} [reason] The reason for editing this role + * @returns {Promise<Role>} + * @example + * // Edit name of a role + * role.edit({ name: 'New Name' }) + * .then(updated => console.log(`Edited role name from ${role.name} to ${updated.name}`)) + * .catch(console.error); + */ + edit(data, reason) { + return this.client.rest.methods.updateGuildRole(this, data, reason); + } + + /** + * Set a new name for the role. + * @param {string} name The new name of the role + * @param {string} [reason] Reason for changing the role's name + * @returns {Promise<Role>} + * @example + * // Set the name of the role + * role.setName('New Name') + * .then(updated => console.log(`Edited role name from ${role.name} to ${updated.name}`)) + * .catch(console.error); + */ + setName(name, reason) { + return this.edit({ name }, reason); + } + + /** + * Set a new color for the role. + * @param {ColorResolvable} color The color of the role + * @param {string} [reason] Reason for changing the role's color + * @returns {Promise<Role>} + * @example + * // Set the color of a role + * role.setColor('#FF0000') + * .then(updated => console.log(`Set color of role to ${role.color}`)) + * .catch(console.error); + */ + setColor(color, reason) { + return this.edit({ color }, reason); + } + + /** + * Set whether or not the role should be hoisted. + * @param {boolean} hoist Whether or not to hoist the role + * @param {string} [reason] Reason for setting whether or not the role should be hoisted + * @returns {Promise<Role>} + * @example + * // Set the hoist of the role + * role.setHoist(true) + * .then(updated => console.log(`Role hoisted: ${updated.hoist}`)) + * .catch(console.error); + */ + setHoist(hoist, reason) { + return this.edit({ hoist }, reason); + } + + /** + * Set the position of the role. + * @param {number} position The position of the role + * @param {boolean} [relative=false] Move the position relative to its current value + * @returns {Promise<Role>} + * @example + * // Set the position of the role + * role.setPosition(1) + * .then(updated => console.log(`Role position: ${updated.position}`)) + * .catch(console.error); + */ + setPosition(position, relative) { + return this.guild.setRolePosition(this, position, relative).then(() => this); + } + + /** + * Set the permissions of the role. + * @param {PermissionResolvable} permissions The permissions of the role + * @param {string} [reason] Reason for changing the role's permissions + * @returns {Promise<Role>} + * @example + * // Set the permissions of the role + * role.setPermissions(['KICK_MEMBERS', 'BAN_MEMBERS']) + * .then(updated => console.log(`Updated permissions to ${updated.permissions.bitfield}`)) + * .catch(console.error); + * @example + * // Remove all permissions from a role + * role.setPermissions(0) + * .then(updated => console.log(`Updated permissions to ${updated.permissions.bitfield}`)) + * .catch(console.error); + */ + setPermissions(permissions, reason) { + return this.edit({ permissions }, reason); + } + + /** + * Set whether this role is mentionable. + * @param {boolean} mentionable Whether this role should be mentionable + * @param {string} [reason] Reason for setting whether or not this role should be mentionable + * @returns {Promise<Role>} + * @example + * // Make the role mentionable + * role.setMentionable(true, 'Role needs to be pinged') + * .then(updated => console.log(`Role mentionable: ${updated.mentionable}`)) + * .catch(console.error); + */ + setMentionable(mentionable, reason) { + return this.edit({ mentionable }, reason); + } + + /** + * Deletes the role. + * @param {string} [reason] Reason for deleting the role + * @returns {Promise<Role>} + * @example + * // Delete a role + * role.delete('The role needed to go') + * .then(deleted => console.log(`Deleted role ${deleted.name}`)) + * .catch(console.error); + */ + delete(reason) { + return this.client.rest.methods.deleteGuildRole(this, reason); + } + + /** + * 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; + } +} + +Role.prototype.hasPermissions = util + .deprecate(Role.prototype.hasPermissions, + 'Role#hasPermissions is deprecated - use Role#hasPermission instead, it now takes an array'); + +module.exports = Role; diff --git a/node_modules/discord.js/src/structures/StoreChannel.js b/node_modules/discord.js/src/structures/StoreChannel.js new file mode 100644 index 0000000..8985c0f --- /dev/null +++ b/node_modules/discord.js/src/structures/StoreChannel.js @@ -0,0 +1,25 @@ +const GuildChannel = require('./GuildChannel'); + +/** + * Represents a guild store channel on Discord. + * @extends {GuildChannel} + */ +class StoreChannel extends GuildChannel { + constructor(guild, data) { + super(guild, data); + this.type = 'store'; + } + + setup(data) { + super.setup(data); + + /** + * If the guild considers this channel NSFW + * @type {boolean} + * @readonly + */ + this.nsfw = data.nsfw; + } +} + +module.exports = StoreChannel; diff --git a/node_modules/discord.js/src/structures/Team.js b/node_modules/discord.js/src/structures/Team.js new file mode 100644 index 0000000..e0dc0e0 --- /dev/null +++ b/node_modules/discord.js/src/structures/Team.js @@ -0,0 +1,109 @@ +const Snowflake = require('../util/Snowflake'); +const Collection = require('../util/Collection'); +const TeamMember = require('./TeamMember'); +const Constants = require('../util/Constants'); + +/** + * Represents a Client OAuth2 Application Team. + */ +class Team { + constructor(client, data) { + /** + * The client that instantiated the team + * @name Team#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + this._patch(data); + } + + _patch(data) { + /** + * The ID of the Team + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The name of the Team + * @type {string} + */ + this.name = data.name; + + /** + * The Team's icon hash + * @type {?string} + */ + this.icon = data.icon || null; + + /** + * The Team's owner id + * @type {?string} + */ + this.ownerID = data.owner_user_id || null; + + /** + * The Team's members + * @type {Collection<Snowflake, TeamMember>} + */ + this.members = new Collection(); + + for (const memberData of data.members) { + const member = new TeamMember(this.client, this, memberData); + this.members.set(member.id, member); + } + } + + /** + * The owner of the team + * @type {?TeamMember} + * @readonly + */ + get owner() { + return this.members.get(this.ownerID) || null; + } + + /** + * The timestamp the team was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * The time the team was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * A link to the teams's icon. + * @type {?string} + * @readonly + */ + get iconURL() { + if (!this.icon) return null; + return Constants.Endpoints.CDN(this.client.options.http.cdn).TeamIcon(this.id, this.icon); + } + + /** + * When concatenated with a string, this automatically returns the Team's name instead of the + * Team object. + * @returns {string} + * @example + * // Logs: Team name: My Team + * console.log(`Team name: ${team}`); + */ + toString() { + return this.name; + } +} + +module.exports = Team; diff --git a/node_modules/discord.js/src/structures/TeamMember.js b/node_modules/discord.js/src/structures/TeamMember.js new file mode 100644 index 0000000..5dbc2ce --- /dev/null +++ b/node_modules/discord.js/src/structures/TeamMember.js @@ -0,0 +1,67 @@ +const { MembershipStates } = require('../util/Constants'); + +/** + * Represents a Client OAuth2 Application Team Member. + */ +class TeamMember { + constructor(client, team, data) { + /** + * The client that instantiated the Team Member + * @name TeamMember#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + /** + * The Team this member is part of + * @type {Team} + */ + this.team = team; + + this._patch(data); + } + + _patch(data) { + /** + * The permissions this Team Member has with regard to the team + * @type {string[]} + */ + this.permissions = data.permissions; + + /** + * The membership state this Team Member has with regard to the team + * @type {MembershipStates} + */ + this.membershipState = MembershipStates[data.membership_state]; + + /** + * The user for this Team Member + * @type {User} + */ + this.user = this.client.dataManager.newUser(data.user); + } + + /** + * The ID of the Team Member + * @type {Snowflake} + * @readonly + */ + get id() { + return this.user.id; + } + + /** + * When concatenated with a string, this automatically returns the team members's mention instead of the + * TeamMember object. + * @returns {string} + * @example + * // Logs: Team Member's mention: <@123456789> + * console.log(`Team Member's mention: ${teamMember}`); + */ + toString() { + return this.user.toString(); + } +} + +module.exports = TeamMember; 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..d16dac8 --- /dev/null +++ b/node_modules/discord.js/src/structures/TextChannel.js @@ -0,0 +1,154 @@ +const GuildChannel = require('./GuildChannel'); +const TextBasedChannel = require('./interfaces/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'; + /** + * A collection containing the messages sent to this channel + * @type {Collection<Snowflake, Message>} + */ + this.messages = new Collection(); + this._typing = new Map(); + } + + setup(data) { + super.setup(data); + + /** + * The topic of the text channel + * @type {?string} + */ + this.topic = data.topic; + + /** + * If the Discord considers this channel NSFW + * @type {boolean} + * @readonly + */ + this.nsfw = Boolean(data.nsfw); + + /** + * The ID of the last message sent in this channel, if one was sent + * @type {?Snowflake} + */ + this.lastMessageID = data.last_message_id; + + /** + * The timestamp when the last pinned message was pinned, if there was one + * @type {?number} + */ + this.lastPinTimestamp = data.last_pin_timestamp ? new Date(data.last_pin_timestamp).getTime() : null; + + /** + * The ratelimit per user for this channel in seconds + * @type {number} + */ + this.rateLimitPerUser = data.rate_limit_per_user || 0; + } + + /** + * A collection of members that can see this channel, mapped by their ID + * @type {Collection<Snowflake, GuildMember>} + * @readonly + */ + get members() { + const members = new Collection(); + for (const member of this.guild.members.values()) { + if (this.permissionsFor(member).has('READ_MESSAGES')) { + members.set(member.id, member); + } + } + return members; + } + + /** + * Fetch all webhooks for the channel. + * @returns {Promise<Collection<Snowflake, Webhook>>} + * @example + * // Fetch webhooks + * channel.fetchWebhooks() + * .then(hooks => console.log(`This channel has ${hooks.size} hooks`)) + * .catch(console.error); + */ + fetchWebhooks() { + return this.client.rest.methods.getChannelWebhooks(this); + } + + /** + * Sets whether this channel is flagged as NSFW. + * @param {boolean} nsfw Whether the channel should be considered NSFW + * @param {string} [reason] Reason for changing the channel's NSFW flag + * @returns {Promise<TextChannel>} + */ + setNSFW(nsfw, reason) { + return this.edit({ nsfw }, reason); + } + + /** + * Create a webhook for the channel. + * @param {string} name The name of the webhook + * @param {BufferResolvable|Base64Resolvable} [avatar] The avatar for the webhook + * @param {string} [reason] Reason for creating this webhook + * @returns {Promise<Webhook>} webhook The created webhook + * @example + * channel.createWebhook('Snek', 'https://i.imgur.com/mI8XcpG.jpg') + * .then(webhook => console.log(`Created webhook ${webhook}`)) + * .catch(console.error) + */ + createWebhook(name, avatar, reason) { + if (typeof avatar === 'string' && avatar.startsWith('data:')) { + return this.client.rest.methods.createWebhook(this, name, avatar, reason); + } else { + return this.client.resolver.resolveImage(avatar).then(data => + this.client.rest.methods.createWebhook(this, name, data, reason) + ); + } + } + + /** + * Sets the rate limit per user for this channel. + * @param {number} rateLimitPerUser The new ratelimit in seconds + * @param {string} [reason] Reason for changing the channel's ratelimits + * @returns {Promise<TextChannel>} + */ + setRateLimitPerUser(rateLimitPerUser, reason) { + return this.edit({ rateLimitPerUser }, reason); + } + + // These are here only for documentation purposes - they are implemented by TextBasedChannel + /* eslint-disable no-empty-function */ + get lastMessage() {} + get lastPinAt() {} + send() { } + sendMessage() { } + sendEmbed() { } + sendFile() { } + sendFiles() { } + sendCode() { } + fetchMessage() { } + fetchMessages() { } + fetchPinnedMessages() { } + search() { } + startTyping() { } + stopTyping() { } + get typing() { } + get typingCount() { } + createCollector() { } + createMessageCollector() { } + awaitMessages() { } + bulkDelete() { } + acknowledge() { } + _cacheMessage() { } +} + +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..86c2bc0 --- /dev/null +++ b/node_modules/discord.js/src/structures/User.js @@ -0,0 +1,336 @@ +const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const Constants = require('../util/Constants'); +const Presence = require('./Presence').Presence; +const Snowflake = require('../util/Snowflake'); +const util = require('util'); + +/** + * Represents a user on Discord. + * @implements {TextBasedChannel} + */ +class User { + constructor(client, data) { + /** + * The client that created the instance of 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 {Snowflake} + */ + 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); + + /** + * Whether this is an Official Discord System user (part of the urgent message system) + * @type {?boolean} + * @name User#system + */ + if (typeof data.system !== 'undefined') this.system = Boolean(data.system); + + /** + * The ID of the last message sent by the user, if one was sent + * @type {?Snowflake} + */ + this.lastMessageID = null; + + /** + * The Message object of the last message sent by the user, if one was sent + * @type {?Message} + */ + this.lastMessage = 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 Snowflake.deconstruct(this.id).timestamp; + } + + /** + * 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(undefined, this.client); + } + + /** + * A link to the user's avatar + * @type {?string} + * @readonly + */ + get avatarURL() { + if (!this.avatar) return null; + return Constants.Endpoints.User(this).Avatar(this.client.options.http.cdn, this.avatar); + } + + /** + * A link to the user's default avatar + * @type {string} + * @readonly + */ + get defaultAvatarURL() { + const avatars = Object.keys(Constants.DefaultAvatars); + const avatar = avatars[this.discriminator % avatars.length]; + return Constants.Endpoints.CDN(this.client.options.http.host).Asset(`${Constants.DefaultAvatars[avatar]}.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 Discord "tag" (e.g. `hydrabolt#0001`) for this user + * @type {string} + * @readonly + */ + get tag() { + return `${this.username}#${this.discriminator}`; + } + + /** + * The note that is set for the user + * <warn>This is only available when using a user account.</warn> + * @type {?string} + * @readonly + * @deprecated + */ + 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} + * @readonly + */ + get dmChannel() { + return this.client.channels.find(c => c.type === 'dm' && c.recipient.id === this.id); + } + + /** + * Creates a DM channel between the client and the user. + * @returns {Promise<DMChannel>} + */ + createDM() { + return this.client.rest.methods.createDM(this); + } + + /** + * Deletes a DM channel (if one exists) between the client and the user. Resolves with the channel if successful. + * @returns {Promise<DMChannel>} + */ + deleteDM() { + return this.client.rest.methods.deleteChannel(this); + } + + /** + * Sends a friend request to the user. + * <warn>This is only available when using a user account.</warn> + * @returns {Promise<User>} + * @deprecated + */ + addFriend() { + return this.client.rest.methods.addFriend(this); + } + + /** + * Removes the user from your friends. + * <warn>This is only available when using a user account.</warn> + * @returns {Promise<User>} + * @deprecated + */ + removeFriend() { + return this.client.rest.methods.removeFriend(this); + } + + /** + * Blocks the user. + * <warn>This is only available when using a user account.</warn> + * @returns {Promise<User>} + * @deprecated + */ + block() { + return this.client.rest.methods.blockUser(this); + } + + /** + * Unblocks the user. + * <warn>This is only available when using a user account.</warn> + * @returns {Promise<User>} + * @deprecated + */ + unblock() { + return this.client.rest.methods.unblockUser(this); + } + + /** + * Get the profile of the user. + * <warn>This is only available when using a user account.</warn> + * @returns {Promise<UserProfile>} + * @deprecated + */ + fetchProfile() { + return this.client.rest.methods.fetchUserProfile(this); + } + + /** + * Sets a note for the user. + * <warn>This is only available when using a user account.</warn> + * @param {string} note The note to set for the user + * @returns {Promise<User>} + * @deprecated + */ + 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 + /* eslint-disable no-empty-function */ + send() {} + sendMessage() {} + sendEmbed() {} + sendFile() {} + sendCode() {} +} + +TextBasedChannel.applyToClass(User); + +User.prototype.block = + util.deprecate(User.prototype.block, 'User#block: userbot methods will be removed'); + +User.prototype.unblock = + util.deprecate(User.prototype.unblock, 'User#unblock: userbot methods will be removed'); + +User.prototype.addFriend = + util.deprecate(User.prototype.addFriend, 'User#addFriend: userbot methods will be removed'); + +User.prototype.removeFriend = + util.deprecate(User.prototype.removeFriend, 'User#removeFriend: userbot methods will be removed'); + +User.prototype.setNote = + util.deprecate(User.prototype.setNote, 'User#setNote, userbot methods will be removed'); + +User.prototype.fetchProfile = + util.deprecate(User.prototype.fetchProfile, 'User#fetchProfile: userbot methods will be removed'); + +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..ea6c65f --- /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; + + /** + * Partial server integrations (not yet implemented) + * @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..a27d4d0 --- /dev/null +++ b/node_modules/discord.js/src/structures/UserProfile.js @@ -0,0 +1,62 @@ +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 UserProfile + * @name UserProfile#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: user.client }); + + /** + * The guilds that the client user and the user share + * @type {Collection<Snowflake, Guild>} + */ + this.mutualGuilds = new Collection(); + + /** + * The user's connections + * @type {Collection<Snowflake, UserConnection>} + */ + this.connections = new Collection(); + + this.setup(data); + } + + setup(data) { + /** + * If the user has Discord Premium + * @type {boolean} + */ + this.premium = data.premium; + + /** + * The date since which the user has had Discord Premium + * @type {?Date} + */ + this.premiumSince = data.premium_since ? new Date(data.premium_since) : null; + + 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..a89dafa --- /dev/null +++ b/node_modules/discord.js/src/structures/VoiceChannel.js @@ -0,0 +1,146 @@ +const GuildChannel = require('./GuildChannel'); +const Collection = require('../util/Collection'); +const Permissions = require('../util/Permissions'); + +/** + * 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<Snowflake, GuildMember>} + */ + this.members = new Collection(); + + this.type = 'voice'; + } + + setup(data) { + super.setup(data); + + /** + * The bitrate of this voice channel + * @type {number} + */ + this.bitrate = data.bitrate * 0.001; + + /** + * 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 voice channel is full + * @type {boolean} + * @readonly + */ + get full() { + return this.userLimit > 0 && this.members.size >= this.userLimit; + } + + /** + * Whether the channel is deletable by the client user + * @type {boolean} + * @readonly + */ + get deletable() { + return super.deletable && this.permissionsFor(this.client.user).has(Permissions.FLAGS.CONNECT); + } + + /** + * Checks if the client has permission join the voice channel + * @type {boolean} + * @readonly + */ + get joinable() { + if (this.client.browser) return false; + if (!this.permissionsFor(this.client.user).has('CONNECT')) return false; + if (this.full && !this.permissionsFor(this.client.user).has('MOVE_MEMBERS')) return false; + return true; + } + + /** + * Checks if the client has permission to send audio to the voice channel + * @type {boolean} + * @readonly + */ + get speakable() { + return this.permissionsFor(this.client.user).has('SPEAK'); + } + + /** + * Sets the bitrate of the channel (in kbps). + * @param {number} bitrate The new bitrate + * @param {string} [reason] Reason for changing the channel's bitrate + * @returns {Promise<VoiceChannel>} + * @example + * // Set the bitrate of a voice channel + * voiceChannel.setBitrate(48) + * .then(vc => console.log(`Set bitrate to ${vc.bitrate}kbps for ${vc.name}`)) + * .catch(console.error); + */ + setBitrate(bitrate, reason) { + bitrate *= 1000; + return this.edit({ bitrate }, reason); + } + + /** + * Sets the user limit of the channel. + * @param {number} userLimit The new user limit + * @param {string} [reason] Reason for changing the user limit + * @returns {Promise<VoiceChannel>} + * @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, reason) { + return this.edit({ userLimit }, reason); + } + + /** + * Attempts to join this voice channel. + * @returns {Promise<VoiceConnection>} + * @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/VoiceRegion.js b/node_modules/discord.js/src/structures/VoiceRegion.js new file mode 100644 index 0000000..dc6b461 --- /dev/null +++ b/node_modules/discord.js/src/structures/VoiceRegion.js @@ -0,0 +1,50 @@ +/** + * Represents a Discord voice region for guilds. + */ +class VoiceRegion { + constructor(data) { + /** + * The ID of the region + * @type {string} + */ + this.id = data.id; + + /** + * Name of the region + * @type {string} + */ + this.name = data.name; + + /** + * Whether the region is VIP-only + * @type {boolean} + */ + this.vip = data.vip; + + /** + * Whether the region is deprecated + * @type {boolean} + */ + this.deprecated = data.deprecated; + + /** + * Whether the region is optimal + * @type {boolean} + */ + this.optimal = data.optimal; + + /** + * Whether the region is custom + * @type {boolean} + */ + this.custom = data.custom; + + /** + * A sample hostname for what a connection might look like + * @type {string} + */ + this.sampleHostname = data.sample_hostname; + } +} + +module.exports = VoiceRegion; 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..5d82b0c --- /dev/null +++ b/node_modules/discord.js/src/structures/Webhook.js @@ -0,0 +1,377 @@ +const EventEmitter = require('events'); +const path = require('path'); +const Util = require('../util/Util'); +const Attachment = require('./Attachment'); +const RichEmbed = require('./RichEmbed'); +const Constants = require('../util/Constants'); +const Snowflake = require('../util/Snowflake'); + +/** + * Represents a webhook. + */ +class Webhook extends EventEmitter { + constructor(client, dataOrID, token) { + super(); + 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 + * @name Webhook#token + * @type {?string} + */ + Object.defineProperty(this, 'token', { value: data.token || null, writable: true, configurable: true }); + + /** + * The avatar for the webhook + * @type {?string} + */ + this.avatar = data.avatar; + + /** + * The ID of the webhook + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The type of the webhook + * @type {WebhookTypes} + */ + this.type = Constants.WebhookTypes[data.type]; + + /** + * The guild the webhook belongs to + * @type {Snowflake} + */ + this.guildID = data.guild_id; + + /** + * The channel the webhook belongs to + * @type {Snowflake} + */ + this.channelID = data.channel_id; + + if (data.user) { + /** + * The owner of the webhook + * @type {?User|Object} + */ + this.owner = this.client.users ? this.client.users.get(data.user.id) : data.user; + } else { + this.owner = null; + } + } + + /** + * The timestamp the webhook was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * The time the webhook was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * A link to the webhook user's avatar + * @type {?stirng} + * @readonly + */ + get avatarURL() { + if (!this.avatar) return null; + return Constants.Endpoints.CDN(this.client.options.http.cdn).Avatar(this.id, this.avatar); + } + + /** + * The url of this webhook + * @type {string} + * @readonly + */ + get url() { + const API = `${this.client.options.http.host}/api/v${this.client.options.http.version}`; + return API + Constants.Endpoints.Webhook(this.id, this.token); + } + + /** + * Options that can be passed into send, sendMessage, sendFile, sendEmbed, and sendCode. + * @typedef {Object} WebhookMessageOptions + * @property {string} [username=this.name] Username override for the message + * @property {string} [avatarURL] Avatar URL override for the message + * @property {boolean} [tts=false] Whether or not the message should be spoken aloud + * @property {string} [nonce=''] The nonce for the message + * @property {Array<RichEmbed|Object>} [embeds] An array of embeds 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|BufferResolvable|Attachment} [file] A file to send with the message **(deprecated)** + * @property {FileOptions[]|BufferResolvable[]|Attachment[]} [files] Files 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. + */ + + /** + * Send a message with this webhook. + * @param {StringResolvable} content The content to send + * @param {WebhookMessageOptions|Attachment|RichEmbed} [options] The options to provide, + * can also be just a RichEmbed or Attachment + * @returns {Promise<Message|Message[]|Object|Object[]>} + * @example + * // Send a basic message + * webhook.send('hello!') + * .then(message => console.log(`Sent message: ${message.content}`)) + * .catch(console.error); + * @example + * // Send a remote file + * webhook.send({ + * files: ['https://cdn.discordapp.com/icons/222078108977594368/6e1019b3179d71046e463a75915e7244.png?size=2048'] + * }) + * .then(console.log) + * .catch(console.error); + * @example + * // Send a local file + * webhook.send({ + * files: [{ + * attachment: 'entire/path/to/file.jpg', + * name: 'file.jpg' + * }] + * }) + * .then(console.log) + * .catch(console.error); + * @example + * // Send an embed with a local image inside + * webhook.send('This is an embed', { + * embeds: [{ + * thumbnail: { + * url: 'attachment://file.jpg' + * } + * }], + * files: [{ + * attachment: 'entire/path/to/file.jpg', + * name: 'file.jpg' + * }] + * }) + * .then(console.log) + * .catch(console.error); + */ + send(content, options) { // eslint-disable-line complexity + if (!options && typeof content === 'object' && !(content instanceof Array)) { + options = content; + content = ''; + } else if (!options) { + options = {}; + } + + if (options instanceof Attachment) options = { files: [options] }; + if (options instanceof RichEmbed) options = { embeds: [options] }; + + if (content) { + content = this.client.resolver.resolveString(content); + let { split, code, disableEveryone } = options; + if (split && typeof split !== 'object') split = {}; + if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) { + content = Util.escapeMarkdown(content, true); + content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``; + if (split) { + split.prepend = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n`; + split.append = '\n```'; + } + } + if (disableEveryone || (typeof disableEveryone === 'undefined' && this.client.options.disableEveryone)) { + content = content.replace(/@(everyone|here)/g, '@\u200b$1'); + } + + if (split) content = Util.splitMessage(content, split); + } + + if (options.file) { + if (options.files) options.files.push(options.file); + else options.files = [options.file]; + } + + if (options.embeds) { + const files = []; + for (const embed of options.embeds) { + if (embed.file) files.push(embed.file); + } + if (options.files) options.files.push(...files); + else options.files = files; + } + + if (options.embeds) options.embeds = options.embeds.map(e => new RichEmbed(e).toJSON()); + + if (options.files) { + for (let i = 0; i < options.files.length; i++) { + let file = options.files[i]; + if (typeof file === 'string' || Buffer.isBuffer(file)) file = { attachment: file }; + if (!file.name) { + if (typeof file.attachment === 'string') { + file.name = path.basename(file.attachment); + } else if (file.attachment && file.attachment.path) { + file.name = path.basename(file.attachment.path); + } else if (file instanceof Attachment) { + file = { attachment: file.file, name: path.basename(file.file) || 'file.jpg' }; + } else { + file.name = 'file.jpg'; + } + } else if (file instanceof Attachment) { + file = file.file; + } + options.files[i] = file; + } + + return Promise.all(options.files.map(file => + this.client.resolver.resolveFile(file.attachment).then(resource => { + file.file = resource; + return file; + }) + )).then(files => this.client.rest.methods.sendWebhookMessage(this, content, options, files)); + } + + return this.client.rest.methods.sendWebhookMessage(this, content, options); + } + + /** + * Send a message with this webhook + * @param {StringResolvable} content The content to send + * @param {WebhookMessageOptions} [options={}] The options to provide + * @returns {Promise<Message|Message[]>} + * @deprecated + * @example + * // Send a message + * webhook.sendMessage('hello!') + * .then(message => console.log(`Sent message: ${message.content}`)) + * .catch(console.error); + */ + sendMessage(content, options = {}) { + return this.send(content, options); + } + + /** + * Send a file with this webhook. + * @param {BufferResolvable} attachment The file to send + * @param {string} [name='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<Message>} + * @deprecated + */ + sendFile(attachment, name, content, options = {}) { + return this.send(content, Object.assign(options, { file: { attachment, name } })); + } + + /** + * 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<Message|Message[]>} + * @deprecated + */ + sendCode(lang, content, options = {}) { + return this.send(content, Object.assign(options, { code: lang })); + } + + /** + * 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); + } + + /** + * Options provided to edit a webhook. + * @property {string} [name] The new name for the webhook + * @property {BufferResolvable} [avatar] The new avatar for the webhook + * @property {ChannelResolvable} [channel] The new channel for the webhook + * @typedef {Object} WebhookEditOptions + */ + + /** + * Edit the webhook. + * @param {string|WebhookEditOptions} nameOrOptions The new name for the webhook **(deprecated, use options)** + * Alternatively options for the webhook, overriding the avatar parameter. + * @param {BufferResolvable|string} [avatarOrReason] The new avatar for the webhook **(deprecated, use options)** + * Alternatively a reason to edit, if using options as first parameter. + * @returns {Promise<Webhook>} + */ + edit(nameOrOptions = this.name, avatarOrReason) { + if (typeof nameOrOptions !== 'object') { + process.emitWarning('Webhook#edit: Use options object instead of separate parameters.'); + nameOrOptions = { + name: nameOrOptions, + avatar: avatarOrReason, + }; + // Parameter was an avatar here; Clear the now reason parameter + avatarOrReason = undefined; + } + + if (nameOrOptions.channel) { + nameOrOptions.channel_id = this.client.resolver.resolveChannelID(nameOrOptions.channel); + nameOrOptions.channel = undefined; + } + + if (nameOrOptions.avatar) { + return this.client.resolver.resolveImage(nameOrOptions.avatar).then(data => { + nameOrOptions.avatar = data; + return this.client.rest.methods.editWebhook(this, nameOrOptions, avatarOrReason); + }); + } + + return this.client.rest.methods.editWebhook(this, nameOrOptions, avatarOrReason); + } + + /** + * Delete the webhook. + * @param {string} [reason] Reason for deleting the webhook + * @returns {Promise} + */ + delete(reason) { + return this.client.rest.methods.deleteWebhook(this, reason); + } +} + +module.exports = Webhook; diff --git a/node_modules/discord.js/src/structures/interfaces/Collector.js b/node_modules/discord.js/src/structures/interfaces/Collector.js new file mode 100644 index 0000000..6767b42 --- /dev/null +++ b/node_modules/discord.js/src/structures/interfaces/Collector.js @@ -0,0 +1,208 @@ +const Collection = require('../../util/Collection'); +const EventEmitter = require('events').EventEmitter; + +/** + * Filter to be applied to the collector. + * @typedef {Function} CollectorFilter + * @param {...*} args Any arguments received by the listener + * @param {Collection} collection The items collected by this collector + * @returns {boolean} + */ + +/** + * Options to be applied to the collector. + * @typedef {Object} CollectorOptions + * @property {number} [time] How long to run the collector for + * @property {number} [idle] How long to stop the collector after inactivity in milliseconds + */ + +/** + * Abstract class for defining a new Collector. + * @abstract + */ +class Collector extends EventEmitter { + constructor(client, filter, options = {}) { + super(); + + /** + * The client + * @name Collector#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + /** + * The filter applied to this collector + * @type {CollectorFilter} + */ + this.filter = filter; + + /** + * The options of this collector + * @type {CollectorOptions} + */ + this.options = options; + + /** + * The items collected by this collector + * @type {Collection} + */ + this.collected = new Collection(); + + /** + * Whether this collector has finished collecting + * @type {boolean} + */ + this.ended = false; + + /** + * Timeout for cleanup + * @type {?Timeout} + * @private + */ + this._timeout = null; + + /** + * Timeout for cleanup due to inactivity + * @type {?Timeout} + * @private + */ + this._idletimeout = null; + + /** + * Call this to handle an event as a collectable element + * Accepts any event data as parameters + * @type {Function} + * @private + */ + this.listener = this._handle.bind(this); + if (options.time) this._timeout = this.client.setTimeout(() => this.stop('time'), options.time); + if (options.idle) this._idletimeout = this.client.setTimeout(() => this.stop('idle'), options.idle); + } + + /** + * @param {...*} args The arguments emitted by the listener + * @emits Collector#collect + * @private + */ + _handle(...args) { + const collect = this.handle(...args); + if (collect && this.filter(...args, this.collected)) { + this.collected.set(collect.key, collect.value); + + /** + * Emitted whenever an element is collected. + * @event Collector#collect + * @param {*} element The element that got collected + * @param {Collector} collector The collector + */ + this.emit('collect', collect.value, this); + + /** + * Emitted whenever an element is collected. + * @event Collector#fullCollect + * @param {...*} args The arguments emitted by the listener + * @private + */ + this.emit('fullCollect', ...args, this); + + if (this._idletimeout) { + this.client.clearTimeout(this._idletimeout); + this._idletimeout = this.client.setTimeout(() => this.stop('idle'), this.options.idle); + } + } + + const post = this.postCheck(...args); + if (post) this.stop(post); + } + + /** + * Return a promise that resolves with the next collected element; + * rejects with collected elements if the collector finishes without receiving a next element + * @type {Promise} + * @readonly + */ + get next() { + return new Promise((resolve, reject) => { + if (this.ended) { + reject(this.collected); + return; + } + + const cleanup = () => { + this.removeListener('collect', onCollect); + this.removeListener('end', onEnd); + }; + + const onCollect = item => { + cleanup(); + resolve(item); + }; + + const onEnd = () => { + cleanup(); + reject(this.collected); // eslint-disable-line prefer-promise-reject-errors + }; + + this.on('collect', onCollect); + this.on('end', onEnd); + }); + } + + /** + * Stop this collector and emit the `end` event. + * @param {string} [reason='user'] The reason this collector is ending + * @emits Collector#end + */ + stop(reason = 'user') { + if (this.ended) return; + + if (this._timeout) { + this.client.clearTimeout(this._timeout); + this._timeout = null; + } + if (this._idletimeout) { + this.client.clearTimeout(this._idletimeout); + this._idletimeout = null; + } + this.ended = true; + this.cleanup(); + + /** + * Emitted when the collector is finished collecting. + * @event Collector#end + * @param {Collection} collected The elements collected by the collector + * @param {string} reason The reason the collector ended + */ + this.emit('end', this.collected, reason); + } + + /* eslint-disable no-empty-function, valid-jsdoc */ + /** + * Handles incoming events from the `listener` function. Returns null if the event should not be collected, + * or returns an object describing the data that should be stored. + * @see Collector#listener + * @param {...*} args Any args the event listener emits + * @returns {?{key: string, value}} Data to insert into collection, if any + * @abstract + */ + handle() {} + + /** + * This method runs after collection to see if the collector should finish. + * @param {...*} args Any args the event listener emits + * @returns {?string} Reason to end the collector, if any + * @abstract + */ + postCheck() {} + + /** + * Called when the collector is ending. + * @abstract + */ + cleanup() {} + /* eslint-enable no-empty-function, valid-jsdoc */ +} + +module.exports = Collector; diff --git a/node_modules/discord.js/src/structures/interfaces/TextBasedChannel.js b/node_modules/discord.js/src/structures/interfaces/TextBasedChannel.js new file mode 100644 index 0000000..ceaa73b --- /dev/null +++ b/node_modules/discord.js/src/structures/interfaces/TextBasedChannel.js @@ -0,0 +1,635 @@ +const path = require('path'); +const Message = require('../Message'); +const MessageCollector = require('../MessageCollector'); +const Collection = require('../../util/Collection'); +const Attachment = require('../../structures/Attachment'); +const RichEmbed = require('../../structures/RichEmbed'); +const Snowflake = require('../../util/Snowflake'); +const util = require('util'); + +/** + * Interface for classes that have text-channel-like features. + * @interface + */ +class TextBasedChannel { + constructor() { + /** + * A collection containing the messages sent to this channel + * @type {Collection<Snowflake, Message>} + */ + this.messages = new Collection(); + + /** + * The ID of the last message in the channel, if one was sent + * @type {?Snowflake} + */ + this.lastMessageID = null; + + /** + * The Message object of the last message in the channel, if one was sent + * @type {?Message} + */ + this.lastMessage = null; + + /** + * The timestamp when the last pinned message was pinned, if there was one + * @type {?number} + */ + this.lastPinTimestamp = null; + } + + /** + * Options provided when sending or editing a message. + * @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 {RichEmbed|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|BufferResolvable|Attachment} [file] A file to send with the message **(deprecated)** + * @property {FileOptions[]|BufferResolvable[]|Attachment[]} [files] Files 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 + * @property {UserResolvable} [reply] User to reply to (prefixes the message with a mention, except in DMs) + */ + + /** + * @typedef {Object} FileOptions + * @property {BufferResolvable} attachment File to attach + * @property {string} [name='file.jpg'] Filename of the attachment + */ + + /** + * 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|Attachment|RichEmbed} [options] Options for the message, + * can also be just a RichEmbed or Attachment + * @returns {Promise<Message|Message[]>} + * @example + * // Send a basic message + * channel.send('hello!') + * .then(message => console.log(`Sent message: ${message.content}`)) + * .catch(console.error); + * @example + * // Send a remote file + * channel.send({ + * files: ['https://cdn.discordapp.com/icons/222078108977594368/6e1019b3179d71046e463a75915e7244.png?size=2048'] + * }) + * .then(console.log) + * .catch(console.error); + * @example + * // Send a local file + * channel.send({ + * files: [{ + * attachment: 'entire/path/to/file.jpg', + * name: 'file.jpg' + * }] + * }) + * .then(console.log) + * .catch(console.error); + * @example + * // Send an embed with a local image inside + * channel.send('This is an embed', { + * embed: { + * thumbnail: { + * url: 'attachment://file.jpg' + * } + * }, + * files: [{ + * attachment: 'entire/path/to/file.jpg', + * name: 'file.jpg' + * }] + * }) + * .then(console.log) + * .catch(console.error); + */ + // eslint-disable-next-line complexity + send(content, options) { + if (!options && typeof content === 'object' && !(content instanceof Array)) { + options = content; + content = ''; + } else if (!options) { + options = {}; + } + + const { reply } = options; + if (options instanceof Attachment) options = { files: [options.file] }; + if (options instanceof RichEmbed) { + if (options.reply) options.reply = undefined; + options = { embed: options }; + } + options.reply = reply; + + if (options.embed) { + if (options.embed.file) { + if (options.files) options.files.push(options.embed.file); + else options.files = [options.embed.file]; + } + if (options.embed.files) { + if (options.files) options.files = options.files.concat(options.embed.files); + else options.files = options.embed.files; + } + } + + if (options.file) { + if (options.files) options.files.push(options.file); + else options.files = [options.file]; + } + + if (options.embed) options.embed = new RichEmbed(options.embed).toJSON(); + + if (options.files) { + for (let i = 0; i < options.files.length; i++) { + let file = options.files[i]; + if (!file || typeof file === 'string' || Buffer.isBuffer(file)) file = { attachment: file }; + if (!file.name) { + if (typeof file.attachment === 'string') { + file.name = path.basename(file.attachment); + } else if (file.attachment && file.attachment.path) { + file.name = path.basename(file.attachment.path); + } else if (file instanceof Attachment) { + file = { attachment: file.file, name: path.basename(file.file) || 'file.jpg' }; + } else { + file.name = 'file.jpg'; + } + } else if (file instanceof Attachment) { + file = file.file; + } + options.files[i] = file; + } + + return Promise.all(options.files.map(file => + this.client.resolver.resolveFile(file.attachment).then(resource => { + file.file = resource; + return file; + }) + )).then(files => this.client.rest.methods.sendMessage(this, content, options, files)); + } + + return this.client.rest.methods.sendMessage(this, content, options); + } + + /** + * Gets a single message from this channel, regardless of it being cached or not. + * @param {Snowflake} messageID ID of the message to get + * @returns {Promise<Message>} + * @example + * // Get message + * channel.fetchMessage('99539446449315840') + * .then(message => console.log(message.content)) + * .catch(console.error); + */ + fetchMessage(messageID) { + if (!this.client.user.bot) { + return this.fetchMessages({ limit: 1, around: messageID }).then(messages => { + const msg = messages.get(messageID); + if (!msg) throw new Error('Message not found.'); + return msg; + }); + } + 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 {Snowflake} [before] ID of a message to get the messages that were posted before it + * @property {Snowflake} [after] ID of a message to get the messages that were posted after it + * @property {Snowflake} [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. + * <info>The returned Collection does not contain reaction users of the messages if they were not cached. + * Those need to be fetched separately in such a case.</info> + * @param {ChannelLogsQueryOptions} [options={}] Query parameters to pass in + * @returns {Promise<Collection<Snowflake, Message>>} + * @example + * // Get messages + * channel.fetchMessages({ limit: 10 }) + * .then(messages => console.log(`Received ${messages.size} messages`)) + * .catch(console.error); + * @example + * // Get messages and filter by user ID + * channel.fetchMessages() + * .then(messages => console.log(`${messages.filter(m => m.author.id === '84484653687267328').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. + * <info>The returned Collection does not contain any reaction data of the messages. + * Those need to be fetched separately.</info> + * @returns {Promise<Collection<Snowflake, Message>>} + * @example + * // Get pinned messages + * channel.fetchPinnedMessages() + * .then(messages => console.log(`Received ${messages.size} messages`)) + * .catch(console.error); + */ + 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; + }); + } + + /** + * @typedef {Object} MessageSearchOptions + * @property {string} [content] Message content + * @property {Snowflake} [maxID] Maximum ID for the filter + * @property {Snowflake} [minID] Minimum ID for the filter + * @property {string} [has] One of `link`, `embed`, `file`, `video`, `image`, or `sound`, + * or add `-` to negate (e.g. `-file`) + * @property {ChannelResolvable} [channel] Channel to limit search to (only for guild search endpoint) + * @property {UserResolvable} [author] Author to limit search + * @property {string} [authorType] One of `user`, `bot`, `webhook`, or add `-` to negate (e.g. `-webhook`) + * @property {string} [sortBy='recent'] `recent` or `relevant` + * @property {string} [sortOrder='desc'] `asc` or `desc` + * @property {number} [contextSize=2] How many messages to get around the matched message (0 to 2) + * @property {number} [limit=25] Maximum number of results to get (1 to 25) + * @property {number} [offset=0] Offset the "pages" of results (since you can only see 25 at a time) + * @property {UserResolvable} [mentions] Mentioned user filter + * @property {boolean} [mentionsEveryone] If everyone is mentioned + * @property {string} [linkHostname] Filter links by hostname + * @property {string} [embedProvider] The name of an embed provider + * @property {string} [embedType] one of `image`, `video`, `url`, `rich` + * @property {string} [attachmentFilename] The name of an attachment + * @property {string} [attachmentExtension] The extension of an attachment + * @property {Date} [before] Date to find messages before + * @property {Date} [after] Date to find messages before + * @property {Date} [during] Date to find messages during (range of date to date + 24 hours) + * @property {boolean} [nsfw=false] Include results from NSFW channels + */ + + /** + * @typedef {Object} MessageSearchResult + * @property {number} totalResults Total result count + * @property {Message[][]} messages Array of message results + * The message which has triggered the result will have the `hit` property set to `true` + */ + + /** + * Performs a search within the channel. + * <warn>This is only available when using a user account.</warn> + * @param {MessageSearchOptions} [options={}] Options to pass to the search + * @returns {Promise<MessageSearchResult>} + * @deprecated + * @example + * channel.search({ + * content: 'discord.js', + * before: '2016-11-17' + * }).then(res => { + * const hit = res.messages[0].find(m => m.hit).content; + * console.log(`I found: **${hit}**, total results: ${res.totalResults}`); + * }).catch(console.error); + */ + search(options = {}) { + return this.client.rest.methods.search(this, options); + } + + /** + * 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)) { + const entry = this.client.user._typing.get(this.id); + entry.count = count || entry.count + 1; + return; + } + + const entry = { + count: count || 1, + interval: this.client.setInterval(() => { + this.client.rest.methods.sendTyping(this.id).catch(() => { + this.client.clearInterval(entry.interval); + this.client.user._typing.delete(this.id); + }); + }, 9000), + }; + this.client.rest.methods.sendTyping(this.id).catch(() => { + this.client.clearInterval(entry.interval); + this.client.user._typing.delete(this.id); + }); + this.client.user._typing.set(this.id, entry); + } + + /** + * Stops the typing indicator in the channel. + * The indicator will only stop if this is called as many times as startTyping(). + * <info>It can take a few seconds for the client user to stop typing.</info> + * @param {boolean} [force=false] Whether or not to reset the call count and force the indicator to stop + * @example + * // Reduce the typing count by one and stop typing if it reached 0 + * 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; + } + + /** + * The Message object of the last message in the channel, if one was sent + * @type {?Message} + * @readonly + */ + get lastMessage() { + return this.messages.get(this.lastMessageID) || null; + } + + /** + * The date when the last pinned message was pinned, if there was one + * @type {?Date} + * @readonly + */ + get lastPinAt() { + return this.lastPinTimestamp ? new Date(this.lastPinTimestamp) : null; + } + + /** + * Creates a Message Collector + * @param {CollectorFilter} filter The filter to create the collector with + * @param {MessageCollectorOptions} [options={}] The options to pass to the collector + * @returns {MessageCollector} + * @deprecated + */ + createCollector(filter, options) { + return this.createMessageCollector(filter, options); + } + + /** + * Creates a Message Collector. + * @param {CollectorFilter} filter The filter to create the collector with + * @param {MessageCollectorOptions} [options={}] The options to pass to the collector + * @returns {MessageCollector} + * @example + * // Create a message collector + * const filter = m => m.content.includes('discord'); + * const collector = channel.createMessageCollector(filter, { time: 15000 }); + * collector.on('collect', m => console.log(`Collected ${m.content}`)); + * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); + */ + createMessageCollector(filter, options = {}) { + return new MessageCollector(this, filter, options); + } + + /** + * An object containing the same properties as CollectorOptions, but a few more: + * @typedef {MessageCollectorOptions} 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 {CollectorFilter} filter The filter function to use + * @param {AwaitMessagesOptions} [options={}] Optional options to pass to the internal collector + * @returns {Promise<Collection<Snowflake, Message>>} + * @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.once('end', (collection, reason) => { + if (options.errors && options.errors.includes(reason)) { + reject(collection); + } else { + resolve(collection); + } + }); + }); + } + + /** + * Bulk delete given messages that are newer than two weeks. + * <warn>This is only available when using a bot account.</warn> + * @param {Collection<Snowflake, Message>|Message[]|Snowflake[]|number} messages + * Messages or number of messages to delete + * @param {boolean} [filterOld=false] Filter messages to remove those which are older than two weeks automatically + * @returns {Promise<Collection<Snowflake, Message>>} Deleted messages + * @example + * // Bulk delete messages + * channel.bulkDelete(5) + * .then(messages => console.log(`Bulk deleted ${messages.size} messages`)) + * .catch(console.error); + */ + bulkDelete(messages, filterOld = false) { + if (messages instanceof Array || messages instanceof Collection) { + let messageIDs = messages instanceof Collection ? messages.keyArray() : messages.map(m => m.id || m); + if (filterOld) { + messageIDs = messageIDs.filter(id => Date.now() - Snowflake.deconstruct(id).date.getTime() < 1209600000); + } + if (messageIDs.length === 0) return Promise.resolve(new Collection()); + if (messageIDs.length === 1) { + return this.fetchMessage(messageIDs[0]).then(m => m.delete()).then(m => new Collection([[m.id, m]])); + } + return this.client.rest.methods.bulkDeleteMessages(this, messageIDs); + } + if (!isNaN(messages)) return this.fetchMessages({ limit: messages }).then(msgs => this.bulkDelete(msgs, filterOld)); + throw new TypeError('The messages must be an Array, Collection, or number.'); + } + + /** + * Marks all messages in this channel as read. + * <warn>This is only available when using a user account.</warn> + * @returns {Promise<TextChannel|GroupDMChannel|DMChannel>} + * @deprecated + */ + acknowledge() { + if (!this.lastMessageID) return Promise.resolve(this); + return this.client.rest.methods.ackTextChannel(this); + } + + _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; + } +} + +/** @lends TextBasedChannel.prototype */ +const Deprecated = { + /** + * Send a message to this channel. + * @param {StringResolvable} [content] Text for the message + * @param {MessageOptions} [options={}] Options for the message + * @returns {Promise<Message|Message[]>} + * @deprecated + * @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<Message>} + * @deprecated + */ + sendEmbed(embed, content, options) { + if (!options && typeof content === 'object' && !(content instanceof Array)) { + options = content; + content = ''; + } else if (!options) { + options = {}; + } + return this.send(content, Object.assign(options, { embed })); + }, + + /** + * Send files to this channel. + * @param {FileOptions[]|string[]} files Files to send with the message + * @param {StringResolvable} [content] Text for the message + * @param {MessageOptions} [options] Options for the message + * @returns {Promise<Message>} + * @deprecated + */ + sendFiles(files, content, options = {}) { + return this.send(content, Object.assign(options, { files })); + }, + + /** + * 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<Message>} + * @deprecated + */ + sendFile(attachment, name, content, options = {}) { + return this.send({ files: [{ attachment, name }], content, options }); + }, + + /** + * 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<Message|Message[]>} + * @deprecated + */ + sendCode(lang, content, options = {}) { + return this.send(content, Object.assign(options, { code: lang })); + }, +}; + +for (const key of Object.keys(Deprecated)) { + TextBasedChannel.prototype[key] = util.deprecate(Deprecated[key], `TextChannel#${key}: use TextChannel#send instead`); +} + +exports.applyToClass = (structure, full = false, ignore = []) => { + const props = ['send', 'sendMessage', 'sendEmbed', 'sendFile', 'sendFiles', 'sendCode']; + if (full) { + props.push( + '_cacheMessage', + 'acknowledge', + 'fetchMessages', + 'fetchMessage', + 'search', + 'lastMessage', + 'lastPinAt', + 'bulkDelete', + 'startTyping', + 'stopTyping', + 'typing', + 'typingCount', + 'fetchPinnedMessages', + 'createCollector', + 'createMessageCollector', + 'awaitMessages' + ); + } + for (const prop of props) { + if (ignore.includes(prop)) continue; + Object.defineProperty(structure.prototype, prop, Object.getOwnPropertyDescriptor(TextBasedChannel.prototype, prop)); + } +}; + +TextBasedChannel.prototype.acknowledge = util.deprecate( + TextBasedChannel.prototype.acknowledge, 'TextBasedChannel#acknowledge: userbot methods will be removed' +); + +TextBasedChannel.prototype.search = + util.deprecate(TextBasedChannel.prototype.search, 'TextBasedChannel#search: userbot methods will be removed'); diff --git a/node_modules/discord.js/src/structures/shared/resolvePermissions.js b/node_modules/discord.js/src/structures/shared/resolvePermissions.js new file mode 100644 index 0000000..c266d46 --- /dev/null +++ b/node_modules/discord.js/src/structures/shared/resolvePermissions.js @@ -0,0 +1,26 @@ +const Permissions = require('../../util/Permissions'); +const Collection = require('../../util/Collection'); + +module.exports = function resolvePermissions(overwrites, guild) { + if (overwrites instanceof Collection || overwrites instanceof Array) { + overwrites = overwrites.map(overwrite => { + const role = this.client.resolver.resolveRole(guild, overwrite.id); + if (role) { + overwrite.id = role.id; + overwrite.type = 'role'; + } else { + overwrite.id = this.client.resolver.resolveUserID(overwrite.id); + overwrite.type = 'member'; + } + + return { + allow: Permissions.resolve(overwrite.allow || overwrite.allowed || 0), + deny: Permissions.resolve(overwrite.deny || overwrite.denied || 0), + type: overwrite.type, + id: overwrite.id, + }; + }); + } + + return overwrites; +}; |