diff options
| author | 8cy <[email protected]> | 2020-04-30 15:46:16 -0700 |
|---|---|---|
| committer | 8cy <[email protected]> | 2020-04-30 15:46:16 -0700 |
| commit | 3a4deac89054021b56ad5bd8005b2044cc085c98 (patch) | |
| tree | 3dd6af8503e497e46180b6b5231674f36bdce9f2 /node_modules/discord.js/src/structures | |
| download | uppity-3a4deac89054021b56ad5bd8005b2044cc085c98.tar.xz uppity-3a4deac89054021b56ad5bd8005b2044cc085c98.zip | |
Up, up, uppity.
Diffstat (limited to 'node_modules/discord.js/src/structures')
43 files changed, 9767 insertions, 0 deletions
diff --git a/node_modules/discord.js/src/structures/APIMessage.js b/node_modules/discord.js/src/structures/APIMessage.js new file mode 100644 index 0000000..86dab74 --- /dev/null +++ b/node_modules/discord.js/src/structures/APIMessage.js @@ -0,0 +1,379 @@ +'use strict'; + +const MessageAttachment = require('./MessageAttachment'); +const MessageEmbed = require('./MessageEmbed'); +const { RangeError } = require('../errors'); +const { browser } = require('../util/Constants'); +const DataResolver = require('../util/DataResolver'); +const MessageFlags = require('../util/MessageFlags'); +const Util = require('../util/Util'); + +/** + * Represents a message to be sent to the API. + */ +class APIMessage { + /** + * @param {MessageTarget} target - The target for this message to be sent to + * @param {MessageOptions|WebhookMessageOptions} options - Options passed in from send + */ + constructor(target, options) { + /** + * The target for this message to be sent to + * @type {MessageTarget} + */ + this.target = target; + + /** + * Options passed in from send + * @type {MessageOptions|WebhookMessageOptions} + */ + this.options = options; + + /** + * Data sendable to the API + * @type {?Object} + */ + this.data = null; + + /** + * Files sendable to the API + * @type {?Object[]} + */ + this.files = null; + } + + /** + * Whether or not the target is a webhook + * @type {boolean} + * @readonly + */ + get isWebhook() { + const Webhook = require('./Webhook'); + const WebhookClient = require('../client/WebhookClient'); + return this.target instanceof Webhook || this.target instanceof WebhookClient; + } + + /** + * Whether or not the target is a user + * @type {boolean} + * @readonly + */ + get isUser() { + const User = require('./User'); + const GuildMember = require('./GuildMember'); + return this.target instanceof User || this.target instanceof GuildMember; + } + + /** + * Whether or not the target is a message + * @type {boolean} + * @readonly + */ + get isMessage() { + const Message = require('./Message'); + return this.target instanceof Message; + } + + /** + * Makes the content of this message. + * @returns {?(string|string[])} + */ + makeContent() { + const GuildMember = require('./GuildMember'); + + let content; + if (this.options.content === null) { + content = ''; + } else if (typeof this.options.content !== 'undefined') { + content = Util.resolveString(this.options.content); + } + + const disableMentions = + typeof this.options.disableMentions === 'undefined' + ? this.target.client.options.disableMentions + : this.options.disableMentions; + if (disableMentions === 'all') { + content = Util.removeMentions(content || ''); + } else if (disableMentions === 'everyone') { + content = (content || '').replace(/@([^<>@ ]*)/gmsu, (match, target) => { + if (target.match(/^[&!]?\d+$/)) { + return `@${target}`; + } else { + return `@\u200b${target}`; + } + }); + } + + const isSplit = typeof this.options.split !== 'undefined' && this.options.split !== false; + const isCode = typeof this.options.code !== 'undefined' && this.options.code !== false; + const splitOptions = isSplit ? { ...this.options.split } : undefined; + + let mentionPart = ''; + if (this.options.reply && !this.isUser && this.target.type !== 'dm') { + const id = this.target.client.users.resolveID(this.options.reply); + mentionPart = `<@${this.options.reply instanceof GuildMember && this.options.reply.nickname ? '!' : ''}${id}>, `; + if (isSplit) { + splitOptions.prepend = `${mentionPart}${splitOptions.prepend || ''}`; + } + } + + if (content || mentionPart) { + if (isCode) { + const codeName = typeof this.options.code === 'string' ? this.options.code : ''; + content = `${mentionPart}\`\`\`${codeName}\n${Util.cleanCodeBlockContent(content || '')}\n\`\`\``; + if (isSplit) { + splitOptions.prepend = `${splitOptions.prepend || ''}\`\`\`${codeName}\n`; + splitOptions.append = `\n\`\`\`${splitOptions.append || ''}`; + } + } else if (mentionPart) { + content = `${mentionPart}${content || ''}`; + } + + if (isSplit) { + content = Util.splitMessage(content || '', splitOptions); + } + } + + return content; + } + + /** + * Resolves data. + * @returns {APIMessage} + */ + resolveData() { + if (this.data) return this; + + const content = this.makeContent(); + const tts = Boolean(this.options.tts); + + let nonce; + if (typeof this.options.nonce !== 'undefined') { + nonce = parseInt(this.options.nonce); + if (isNaN(nonce) || nonce < 0) throw new RangeError('MESSAGE_NONCE_TYPE'); + } + + const embedLikes = []; + if (this.isWebhook) { + if (this.options.embeds) { + embedLikes.push(...this.options.embeds); + } + } else if (this.options.embed) { + embedLikes.push(this.options.embed); + } + const embeds = embedLikes.map(e => new MessageEmbed(e).toJSON()); + + let username; + let avatarURL; + if (this.isWebhook) { + username = this.options.username || this.target.name; + if (this.options.avatarURL) avatarURL = this.options.avatarURL; + } + + let flags; + if (this.isMessage) { + // eslint-disable-next-line eqeqeq + flags = this.options.flags != null ? new MessageFlags(this.options.flags).bitfield : this.target.flags.bitfield; + } + + const allowedMentions = + typeof this.options.allowedMentions === 'undefined' + ? this.target.client.options.allowedMentions + : this.options.allowedMentions; + + this.data = { + content, + tts, + nonce, + embed: this.options.embed === null ? null : embeds[0], + embeds, + username, + avatar_url: avatarURL, + allowed_mentions: allowedMentions, + flags, + }; + return this; + } + + /** + * Resolves files. + * @returns {Promise<APIMessage>} + */ + async resolveFiles() { + if (this.files) return this; + + const embedLikes = []; + if (this.isWebhook) { + if (this.options.embeds) { + embedLikes.push(...this.options.embeds); + } + } else if (this.options.embed) { + embedLikes.push(this.options.embed); + } + + const fileLikes = []; + if (this.options.files) { + fileLikes.push(...this.options.files); + } + for (const embed of embedLikes) { + if (embed.files) { + fileLikes.push(...embed.files); + } + } + + this.files = await Promise.all(fileLikes.map(f => this.constructor.resolveFile(f))); + return this; + } + + /** + * Converts this APIMessage into an array of APIMessages for each split content + * @returns {APIMessage[]} + */ + split() { + if (!this.data) this.resolveData(); + + if (!Array.isArray(this.data.content)) return [this]; + + const apiMessages = []; + + for (let i = 0; i < this.data.content.length; i++) { + let data; + let opt; + + if (i === this.data.content.length - 1) { + data = { ...this.data, content: this.data.content[i] }; + opt = { ...this.options, content: this.data.content[i] }; + } else { + data = { content: this.data.content[i], tts: this.data.tts }; + opt = { content: this.data.content[i], tts: this.data.tts }; + } + + const apiMessage = new APIMessage(this.target, opt); + apiMessage.data = data; + apiMessages.push(apiMessage); + } + + return apiMessages; + } + + /** + * Resolves a single file into an object sendable to the API. + * @param {BufferResolvable|Stream|FileOptions|MessageAttachment} fileLike Something that could be resolved to a file + * @returns {Object} + */ + static async resolveFile(fileLike) { + let attachment; + let name; + + const findName = thing => { + if (typeof thing === 'string') { + return Util.basename(thing); + } + + if (thing.path) { + return Util.basename(thing.path); + } + + return 'file.jpg'; + }; + + const ownAttachment = + typeof fileLike === 'string' || + fileLike instanceof (browser ? ArrayBuffer : Buffer) || + typeof fileLike.pipe === 'function'; + if (ownAttachment) { + attachment = fileLike; + name = findName(attachment); + } else { + attachment = fileLike.attachment; + name = fileLike.name || findName(attachment); + } + + const resource = await DataResolver.resolveFile(attachment); + return { attachment, name, file: resource }; + } + + /** + * Partitions embeds and attachments. + * @param {Array<MessageEmbed|MessageAttachment>} items Items to partition + * @returns {Array<MessageEmbed[], MessageAttachment[]>} + */ + static partitionMessageAdditions(items) { + const embeds = []; + const files = []; + for (const item of items) { + if (item instanceof MessageEmbed) { + embeds.push(item); + } else if (item instanceof MessageAttachment) { + files.push(item); + } + } + + return [embeds, files]; + } + + /** + * Transforms the user-level arguments into a final options object. Passing a transformed options object alone into + * this method will keep it the same, allowing for the reuse of the final options object. + * @param {StringResolvable} [content] Content to send + * @param {MessageOptions|WebhookMessageOptions|MessageAdditions} [options={}] Options to use + * @param {MessageOptions|WebhookMessageOptions} [extra={}] Extra options to add onto transformed options + * @param {boolean} [isWebhook=false] Whether or not to use WebhookMessageOptions as the result + * @returns {MessageOptions|WebhookMessageOptions} + */ + static transformOptions(content, options, extra = {}, isWebhook = false) { + if (!options && typeof content === 'object' && !Array.isArray(content)) { + options = content; + content = undefined; + } + + if (!options) { + options = {}; + } else if (options instanceof MessageEmbed) { + return isWebhook ? { content, embeds: [options], ...extra } : { content, embed: options, ...extra }; + } else if (options instanceof MessageAttachment) { + return { content, files: [options], ...extra }; + } + + if (Array.isArray(options)) { + const [embeds, files] = this.partitionMessageAdditions(options); + return isWebhook ? { content, embeds, files, ...extra } : { content, embed: embeds[0], files, ...extra }; + } else if (Array.isArray(content)) { + const [embeds, files] = this.partitionMessageAdditions(content); + if (embeds.length || files.length) { + return isWebhook ? { embeds, files, ...extra } : { embed: embeds[0], files, ...extra }; + } + } + + return { content, ...options, ...extra }; + } + + /** + * Creates an `APIMessage` from user-level arguments. + * @param {MessageTarget} target Target to send to + * @param {StringResolvable} [content] Content to send + * @param {MessageOptions|WebhookMessageOptions|MessageAdditions} [options={}] Options to use + * @param {MessageOptions|WebhookMessageOptions} [extra={}] - Extra options to add onto transformed options + * @returns {MessageOptions|WebhookMessageOptions} + */ + static create(target, content, options, extra = {}) { + const Webhook = require('./Webhook'); + const WebhookClient = require('../client/WebhookClient'); + + const isWebhook = target instanceof Webhook || target instanceof WebhookClient; + const transformed = this.transformOptions(content, options, extra, isWebhook); + return new this(target, transformed); + } +} + +module.exports = APIMessage; + +/** + * A target for a message. + * @typedef {TextChannel|DMChannel|User|GuildMember|Webhook|WebhookClient} MessageTarget + */ + +/** + * Additional items that can be sent with a message. + * @typedef {MessageEmbed|MessageAttachment|Array<MessageEmbed|MessageAttachment>} MessageAdditions + */ diff --git a/node_modules/discord.js/src/structures/Base.js b/node_modules/discord.js/src/structures/Base.js new file mode 100644 index 0000000..65c1fa5 --- /dev/null +++ b/node_modules/discord.js/src/structures/Base.js @@ -0,0 +1,42 @@ +'use strict'; + +const Util = require('../util/Util'); + +/** + * Represents a data model that is identifiable by a Snowflake (i.e. Discord API data models). + */ +class Base { + constructor(client) { + /** + * The client that instantiated this + * @name Base#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + } + + _clone() { + return Object.assign(Object.create(this), this); + } + + _patch(data) { + return data; + } + + _update(data) { + const clone = this._clone(); + this._patch(data); + return clone; + } + + toJSON(...props) { + return Util.flatten(this, ...props); + } + + valueOf() { + return this.id; + } +} + +module.exports = Base; diff --git a/node_modules/discord.js/src/structures/BaseGuildEmoji.js b/node_modules/discord.js/src/structures/BaseGuildEmoji.js new file mode 100644 index 0000000..d366527 --- /dev/null +++ b/node_modules/discord.js/src/structures/BaseGuildEmoji.js @@ -0,0 +1,58 @@ +'use strict'; + +const Emoji = require('./Emoji'); + +/** + * Parent class for {@link GuildEmoji} and {@link GuildPreviewEmoji}. + * @extends {Emoji} + */ +class BaseGuildEmoji extends Emoji { + constructor(client, data, guild) { + super(client, data); + + /** + * The guild this emoji is a part of + * @type {Guild|GuildPreview} + */ + this.guild = guild; + + /** + * Array of role ids this emoji is active for + * @name BaseGuildEmoji#_roles + * @type {Snowflake[]} + * @private + */ + Object.defineProperty(this, '_roles', { value: [], writable: true }); + + this._patch(data); + } + + _patch(data) { + if (data.name) this.name = data.name; + + /** + * Whether or not this emoji requires colons surrounding it + * @type {boolean} + * @name GuildEmoji#requiresColons + */ + if (typeof data.require_colons !== 'undefined') this.requiresColons = data.require_colons; + + /** + * Whether this emoji is managed by an external service + * @type {boolean} + * @name GuildEmoji#managed + */ + if (typeof data.managed !== 'undefined') this.managed = data.managed; + + /** + * Whether this emoji is available + * @type {boolean} + * @name GuildEmoji#available + */ + if (typeof data.available !== 'undefined') this.available = data.available; + + if (data.roles) this._roles = data.roles; + } +} + +module.exports = BaseGuildEmoji; 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..4ac9fbb --- /dev/null +++ b/node_modules/discord.js/src/structures/CategoryChannel.js @@ -0,0 +1,33 @@ +'use strict'; + +const GuildChannel = require('./GuildChannel'); + +/** + * Represents a guild category channel on Discord. + * @extends {GuildChannel} + */ +class CategoryChannel extends GuildChannel { + /** + * Channels that are a part of this category + * @type {?Collection<Snowflake, GuildChannel>} + * @readonly + */ + get children() { + return this.guild.channels.cache.filter(c => c.parentID === this.id); + } + + /** + * Sets the category parent of this channel. + * <warn>It is not currently possible to set the parent of a CategoryChannel.</warn> + * @method setParent + * @memberof CategoryChannel + * @instance + * @param {?GuildChannel|Snowflake} channel Parent channel + * @param {Object} [options={}] Options to pass + * @param {boolean} [options.lockPermissions=true] Lock the permissions to what the parent's permissions are + * @param {string} [options.reason] Reason for modifying the parent of this channel + * @returns {Promise<GuildChannel>} + */ +} + +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..1dc7ee0 --- /dev/null +++ b/node_modules/discord.js/src/structures/Channel.js @@ -0,0 +1,151 @@ +'use strict'; + +const Base = require('./Base'); +const { ChannelTypes } = require('../util/Constants'); +const Snowflake = require('../util/Snowflake'); + +/** + * Represents any channel on Discord. + * @extends {Base} + */ +class Channel extends Base { + constructor(client, data) { + super(client); + + const type = Object.keys(ChannelTypes)[data.type]; + /** + * The type of the channel, either: + * * `dm` - a 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 + * * `unknown` - a generic channel of unknown type, could be Channel or GuildChannel + * @type {string} + */ + this.type = type ? type.toLowerCase() : 'unknown'; + + /** + * Whether the channel has been deleted + * @type {boolean} + */ + this.deleted = false; + + if (data) this._patch(data); + } + + _patch(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 at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * 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}!`); + */ + toString() { + return `<#${this.id}>`; + } + + /** + * Deletes this channel. + * @returns {Promise<Channel>} + * @example + * // Delete the channel + * channel.delete() + * .then(console.log) + * .catch(console.error); + */ + delete() { + return this.client.api + .channels(this.id) + .delete() + .then(() => this); + } + + /** + * Fetches this channel. + * @returns {Promise<Channel>} + */ + fetch() { + return this.client.channels.fetch(this.id, true); + } + + static create(client, data, guild) { + const Structures = require('../util/Structures'); + let channel; + if (!data.guild_id && !guild) { + if ((data.recipients && data.type !== ChannelTypes.GROUP) || data.type === ChannelTypes.DM) { + const DMChannel = Structures.get('DMChannel'); + channel = new DMChannel(client, data); + } else if (data.type === ChannelTypes.GROUP) { + const PartialGroupDMChannel = require('./PartialGroupDMChannel'); + channel = new PartialGroupDMChannel(client, data); + } + } else { + guild = guild || client.guilds.cache.get(data.guild_id); + if (guild) { + switch (data.type) { + case ChannelTypes.TEXT: { + const TextChannel = Structures.get('TextChannel'); + channel = new TextChannel(guild, data); + break; + } + case ChannelTypes.VOICE: { + const VoiceChannel = Structures.get('VoiceChannel'); + channel = new VoiceChannel(guild, data); + break; + } + case ChannelTypes.CATEGORY: { + const CategoryChannel = Structures.get('CategoryChannel'); + channel = new CategoryChannel(guild, data); + break; + } + case ChannelTypes.NEWS: { + const NewsChannel = Structures.get('NewsChannel'); + channel = new NewsChannel(guild, data); + break; + } + case ChannelTypes.STORE: { + const StoreChannel = Structures.get('StoreChannel'); + channel = new StoreChannel(guild, data); + break; + } + } + if (channel) guild.channels.cache.set(channel.id, channel); + } + } + return channel; + } + + toJSON(...props) { + return super.toJSON({ createdTimestamp: true }, ...props); + } +} + +module.exports = Channel; diff --git a/node_modules/discord.js/src/structures/ClientApplication.js b/node_modules/discord.js/src/structures/ClientApplication.js new file mode 100644 index 0000000..753d90d --- /dev/null +++ b/node_modules/discord.js/src/structures/ClientApplication.js @@ -0,0 +1,156 @@ +'use strict'; + +const Base = require('./Base'); +const Team = require('./Team'); +const { ClientApplicationAssetTypes, Endpoints } = require('../util/Constants'); +const Snowflake = require('../util/Snowflake'); + +const AssetTypes = Object.keys(ClientApplicationAssetTypes); + +/** + * Represents a Client OAuth2 Application. + * @extends {Base} + */ +class ClientApplication extends Base { + constructor(client, data) { + super(client); + this._patch(data); + } + + _patch(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 cover image + * @type {?string} + */ + this.cover = data.cover_image || null; + + /** + * The app's RPC origins, if enabled + * @type {string[]} + */ + this.rpcOrigins = data.rpc_origins || []; + + /** + * If this app's bot requires a code grant when using the OAuth2 flow + * @type {?boolean} + */ + this.botRequireCodeGrant = typeof data.bot_require_code_grant !== 'undefined' ? data.bot_require_code_grant : null; + + /** + * If this app's bot is public + * @type {?boolean} + */ + this.botPublic = typeof data.bot_public !== 'undefined' ? data.bot_public : null; + + /** + * The owner of this OAuth application + * @type {?User|Team} + */ + this.owner = data.team ? new Team(this.client, data.team) : data.owner ? this.client.users.add(data.owner) : 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 at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * A link to the application's icon. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} URL to the icon + */ + iconURL({ format, size } = {}) { + if (!this.icon) return null; + return this.client.rest.cdn.AppIcon(this.id, this.icon, { format, size }); + } + + /** + * A link to this application's cover image. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} URL to the cover image + */ + coverImage({ format, size } = {}) { + if (!this.cover) return null; + return Endpoints.CDN(this.client.options.http.cdn).AppIcon(this.id, this.cover, { format, size }); + } + + /** + * Asset data. + * @typedef {Object} ClientAsset + * @property {Snowflake} id The asset ID + * @property {string} name The asset name + * @property {string} type The asset type + */ + + /** + * Gets the clients rich presence assets. + * @returns {Promise<Array<ClientAsset>>} + */ + fetchAssets() { + return this.client.api.oauth2 + .applications(this.id) + .assets.get() + .then(assets => + assets.map(a => ({ + id: a.id, + name: a.name, + type: AssetTypes[a.type - 1], + })), + ); + } + + /** + * When concatenated with a string, this automatically returns the application's name instead of the + * ClientApplication object. + * @returns {string} + * @example + * // Logs: Application name: My App + * console.log(`Application name: ${application}`); + */ + toString() { + return this.name; + } + + toJSON() { + return super.toJSON({ createdTimestamp: true }); + } +} + +module.exports = ClientApplication; diff --git a/node_modules/discord.js/src/structures/ClientPresence.js b/node_modules/discord.js/src/structures/ClientPresence.js new file mode 100644 index 0000000..a39ba00 --- /dev/null +++ b/node_modules/discord.js/src/structures/ClientPresence.js @@ -0,0 +1,87 @@ +'use strict'; + +const { Presence } = require('./Presence'); +const { TypeError } = require('../errors'); +const Collection = require('../util/Collection'); +const { ActivityTypes, OPCodes } = require('../util/Constants'); + +class ClientPresence extends Presence { + /** + * @param {Client} client The instantiating client + * @param {Object} [data={}] The data for the client presence + */ + constructor(client, data = {}) { + super(client, Object.assign(data, { status: 'online', user: { id: null } })); + } + + async set(presence) { + const packet = await this._parse(presence); + this.patch(packet); + if (typeof presence.shardID === 'undefined') { + this.client.ws.broadcast({ op: OPCodes.STATUS_UPDATE, d: packet }); + } else if (Array.isArray(presence.shardID)) { + for (const shardID of presence.shardID) { + this.client.ws.shards.get(shardID).send({ op: OPCodes.STATUS_UPDATE, d: packet }); + } + } else { + this.client.ws.shards.get(presence.shardID).send({ op: OPCodes.STATUS_UPDATE, d: packet }); + } + return this; + } + + async _parse({ status, since, afk, activity }) { + const applicationID = activity && (activity.application ? activity.application.id || activity.application : null); + let assets = new Collection(); + if (activity) { + if (typeof activity.name !== 'string') throw new TypeError('INVALID_TYPE', 'name', 'string'); + if (!activity.type) activity.type = 0; + if (activity.assets && applicationID) { + try { + const a = await this.client.api.oauth2.applications(applicationID).assets.get(); + for (const asset of a) assets.set(asset.name, asset.id); + } catch {} // eslint-disable-line no-empty + } + } + + const packet = { + afk: afk != null ? afk : false, // eslint-disable-line eqeqeq + since: since != null ? since : null, // eslint-disable-line eqeqeq + status: status || this.status, + game: activity + ? { + type: activity.type, + name: activity.name, + url: activity.url, + details: activity.details || undefined, + state: activity.state || undefined, + assets: activity.assets + ? { + large_text: activity.assets.largeText || undefined, + small_text: activity.assets.smallText || undefined, + large_image: assets.get(activity.assets.largeImage) || activity.assets.largeImage, + small_image: assets.get(activity.assets.smallImage) || activity.assets.smallImage, + } + : undefined, + timestamps: activity.timestamps || undefined, + party: activity.party || undefined, + application_id: applicationID || undefined, + secrets: activity.secrets || undefined, + instance: activity.instance || undefined, + } + : null, + }; + + if ((status || afk || since) && !activity) { + packet.game = this.activities[0] || null; + } + + if (packet.game) { + packet.game.type = + typeof packet.game.type === 'number' ? packet.game.type : ActivityTypes.indexOf(packet.game.type); + } + + return packet; + } +} + +module.exports = ClientPresence; 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..84ada54 --- /dev/null +++ b/node_modules/discord.js/src/structures/ClientUser.js @@ -0,0 +1,180 @@ +'use strict'; + +const DataResolver = require('../util/DataResolver'); +const Structures = require('../util/Structures'); + +/** + * Represents the logged in client's Discord user. + * @extends {User} + */ +class ClientUser extends Structures.get('User') { + constructor(client, data) { + super(client, data); + this._typing = new Map(); + } + + _patch(data) { + super._patch(data); + + if ('verified' in data) { + /** + * Whether or not this account has been verified + * @type {boolean} + */ + this.verified = data.verified; + } + + if ('mfa_enabled' in data) { + /** + * If the bot's {@link ClientApplication#owner Owner} has MFA enabled on their account + * @type {?boolean} + */ + this.mfaEnabled = typeof data.mfa_enabled === 'boolean' ? data.mfa_enabled : null; + } else if (typeof this.mfaEnabled === 'undefined') { + this.mfaEnabled = null; + } + + if (data.token) this.client.token = data.token; + } + + /** + * ClientUser's presence + * @type {Presence} + * @readonly + */ + get presence() { + return this.client.presence; + } + + edit(data) { + return this.client.api + .users('@me') + .patch({ data }) + .then(newData => { + this.client.token = newData.token; + const { updated } = this.client.actions.UserUpdate.handle(newData); + if (updated) return updated; + return this; + }); + } + + /** + * Sets 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 + * @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) { + return this.edit({ username }); + } + + /** + * Sets 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); + */ + async setAvatar(avatar) { + return this.edit({ avatar: await DataResolver.resolveImage(avatar) }); + } + + /** + * Data resembling a raw Discord presence. + * @typedef {Object} PresenceData + * @property {PresenceStatusData} [status] Status of the user + * @property {boolean} [afk] Whether the user is AFK + * @property {Object} [activity] Activity the user is playing + * @property {Object|string} [activity.application] An application object or application id + * @property {string} [activity.application.id] The id of the application + * @property {string} [activity.name] Name of the activity + * @property {ActivityType|number} [activity.type] Type of the activity + * @property {string} [activity.url] Stream url + * @property {?number|number[]} [shardID] Shard Id(s) to have the activity set on + */ + + /** + * Sets the full presence of the client user. + * @param {PresenceData} data Data for the presence + * @returns {Promise<Presence>} + * @example + * // Set the client user's presence + * client.user.setPresence({ activity: { name: 'with discord.js' }, status: 'idle' }) + * .then(console.log) + * .catch(console.error); + */ + setPresence(data) { + return this.client.presence.set(data); + } + + /** + * A user's status. Must be one of: + * * `online` + * * `idle` + * * `invisible` + * * `dnd` (do not disturb) + * @typedef {string} PresenceStatusData + */ + + /** + * Sets the status of the client user. + * @param {PresenceStatusData} status Status to change to + * @param {?number|number[]} [shardID] Shard ID(s) to have the activity set on + * @returns {Promise<Presence>} + * @example + * // Set the client user's status + * client.user.setStatus('idle') + * .then(console.log) + * .catch(console.error); + */ + setStatus(status, shardID) { + return this.setPresence({ status, shardID }); + } + + /** + * Options for setting an activity + * @typedef ActivityOptions + * @type {Object} + * @property {string} [url] Twitch stream URL + * @property {ActivityType|number} [type] Type of the activity + * @property {?number|number[]} [shardID] Shard Id(s) to have the activity set on + */ + + /** + * Sets the activity the client user is playing. + * @param {string|ActivityOptions} [name] Activity being played, or options for setting the activity + * @param {ActivityOptions} [options] Options for setting the activity + * @returns {Promise<Presence>} + * @example + * // Set the client user's activity + * client.user.setActivity('discord.js', { type: 'WATCHING' }) + * .then(presence => console.log(`Activity set to ${presence.activities[0].name}`)) + * .catch(console.error); + */ + setActivity(name, options = {}) { + if (!name) return this.setPresence({ activity: null, shardID: options.shardID }); + + const activity = Object.assign({}, options, typeof name === 'object' ? name : { name }); + return this.setPresence({ activity, shardID: activity.shardID }); + } + + /** + * Sets/removes the AFK flag for the client user. + * @param {boolean} afk Whether or not the user is AFK + * @returns {Promise<Presence>} + */ + setAFK(afk) { + return this.setPresence({ afk }); + } +} + +module.exports = ClientUser; diff --git a/node_modules/discord.js/src/structures/DMChannel.js b/node_modules/discord.js/src/structures/DMChannel.js new file mode 100644 index 0000000..e661bc4 --- /dev/null +++ b/node_modules/discord.js/src/structures/DMChannel.js @@ -0,0 +1,98 @@ +'use strict'; + +const Channel = require('./Channel'); +const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const MessageManager = require('../managers/MessageManager'); + +/** + * Represents a direct message channel between two users. + * @extends {Channel} + * @implements {TextBasedChannel} + */ +class DMChannel extends Channel { + /** + * @param {Client} client The instantiating client + * @param {Object} data The data for the DM channel + */ + constructor(client, data) { + super(client, data); + // Override the channel type so partials have a known type + this.type = 'dm'; + /** + * A manager of the messages belonging to this channel + * @type {MessageManager} + */ + this.messages = new MessageManager(this); + this._typing = new Map(); + } + + _patch(data) { + super._patch(data); + + if (data.recipients) { + /** + * The recipient on the other end of the DM + * @type {User} + */ + this.recipient = this.client.users.add(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; + } + + /** + * Whether this DMChannel is a partial + * @type {boolean} + * @readonly + */ + get partial() { + return typeof this.lastMessageID === 'undefined'; + } + + /** + * Fetch this DMChannel. + * @returns {Promise<DMChannel>} + */ + fetch() { + return this.recipient.createDM(); + } + + /** + * When concatenated with a string, this automatically returns the recipient's mention instead of the + * DMChannel object. + * @returns {string} + * @example + * // Logs: Hello from <@123456789012345678>! + * console.log(`Hello from ${channel}!`); + */ + toString() { + return this.recipient.toString(); + } + + // These are here only for documentation purposes - they are implemented by TextBasedChannel + /* eslint-disable no-empty-function */ + get lastMessage() {} + get lastPinAt() {} + send() {} + startTyping() {} + stopTyping() {} + get typing() {} + get typingCount() {} + createMessageCollector() {} + awaitMessages() {} + // Doesn't work on DM channels; bulkDelete() {} +} + +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..0214ea8 --- /dev/null +++ b/node_modules/discord.js/src/structures/Emoji.js @@ -0,0 +1,104 @@ +'use strict'; + +const Base = require('./Base'); +const Snowflake = require('../util/Snowflake'); + +/** + * Represents an emoji, see {@link GuildEmoji} and {@link ReactionEmoji}. + * @extends {Base} + */ +class Emoji extends Base { + constructor(client, emoji) { + super(client); + /** + * Whether this emoji is animated + * @type {boolean} + */ + this.animated = emoji.animated; + + /** + * The name of this emoji + * @type {string} + */ + this.name = emoji.name; + + /** + * The ID of this emoji + * @type {?Snowflake} + */ + this.id = emoji.id; + + /** + * Whether this emoji has been deleted + * @type {boolean} + */ + this.deleted = false; + } + + /** + * The identifier of this emoji, used for message reactions + * @type {string} + * @readonly + */ + get identifier() { + if (this.id) return `${this.animated ? 'a:' : ''}${this.name}:${this.id}`; + return encodeURIComponent(this.name); + } + + /** + * The URL to the emoji file if its a custom emoji + * @type {?string} + * @readonly + */ + get url() { + if (!this.id) return null; + return this.client.rest.cdn.Emoji(this.id, this.animated ? 'gif' : 'png'); + } + + /** + * The timestamp the 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 emoji was created at, or null if unicode + * @type {?Date} + * @readonly + */ + get createdAt() { + if (!this.id) return null; + return new Date(this.createdTimestamp); + } + + /** + * When concatenated with a string, this automatically returns the text required to form a graphical emoji on Discord + * instead of the Emoji object. + * @returns {string} + * @example + * // Send a custom emoji from a guild: + * const emoji = guild.emojis.cache.first(); + * msg.reply(`Hello! ${emoji}`); + * @example + * // Send the emoji used in a reaction to the channel the reaction is part of + * reaction.message.channel.send(`The emoji used was: ${reaction.emoji}`); + */ + toString() { + return this.id ? `<${this.animated ? 'a' : ''}:${this.name}:${this.id}>` : this.name; + } + + toJSON() { + return super.toJSON({ + guild: 'guildID', + createdTimestamp: true, + url: true, + identifier: true, + }); + } +} + +module.exports = Emoji; 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..e2bc8c4 --- /dev/null +++ b/node_modules/discord.js/src/structures/Guild.js @@ -0,0 +1,1349 @@ +'use strict'; + +const Base = require('./Base'); +const GuildAuditLogs = require('./GuildAuditLogs'); +const GuildPreview = require('./GuildPreview'); +const Integration = require('./Integration'); +const Invite = require('./Invite'); +const VoiceRegion = require('./VoiceRegion'); +const Webhook = require('./Webhook'); +const GuildChannelManager = require('../managers/GuildChannelManager'); +const GuildEmojiManager = require('../managers/GuildEmojiManager'); +const GuildMemberManager = require('../managers/GuildMemberManager'); +const PresenceManager = require('../managers/PresenceManager'); +const RoleManager = require('../managers/RoleManager'); +const VoiceStateManager = require('../managers/VoiceStateManager'); +const Collection = require('../util/Collection'); +const { + ChannelTypes, + DefaultMessageNotifications, + PartialTypes, + VerificationLevels, + ExplicitContentFilterLevels, +} = require('../util/Constants'); +const DataResolver = require('../util/DataResolver'); +const Snowflake = require('../util/Snowflake'); +const SystemChannelFlags = require('../util/SystemChannelFlags'); +const Util = require('../util/Util'); + +/** + * 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> + * @extends {Base} + */ +class Guild extends Base { + /** + * @param {Client} client The instantiating client + * @param {Object} data The data for the guild + */ + constructor(client, data) { + super(client); + + /** + * A manager of the members belonging to this guild + * @type {GuildMemberManager} + */ + this.members = new GuildMemberManager(this); + + /** + * A manager of the channels belonging to this guild + * @type {GuildChannelManager} + */ + this.channels = new GuildChannelManager(this); + + /** + * A manager of the roles belonging to this guild + * @type {RoleManager} + */ + this.roles = new RoleManager(this); + + /** + * A manager of the presences belonging to this guild + * @type {PresenceManager} + */ + this.presences = new PresenceManager(this.client); + + /** + * A manager of the voice states of this guild + * @type {VoiceStateManager} + */ + this.voiceStates = new VoiceStateManager(this); + + /** + * 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._patch(data); + if (!data.channels) this.available = false; + } + + /** + * The id of the shard this Guild belongs to. + * @type {number} + */ + this.shardID = data.shardID; + } + + /** + * The Shard this Guild belongs to. + * @type {WebSocketShard} + * @readonly + */ + get shard() { + return this.client.ws.shards.get(this.shardID); + } + + /** + * Sets up the guild. + * @param {*} data The raw data of the guild + * @private + */ + _patch(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 enabled guild features, here are the possible values: + * * ANIMATED_ICON + * * BANNER + * * COMMERCE + * * DISCOVERABLE + * * FEATURABLE + * * INVITE_SPLASH + * * NEWS + * * PARTNERED + * * PUBLIC + * * PUBLIC_DISABLED + * * VANITY_URL + * * VERIFIED + * * VIP_REGIONS + * * WELCOME_SCREEN_ENABLED + * @typedef {string} Features + */ + + /** + * An array of guild features partnered guilds have enabled + * @type {Features[]} + */ + 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 {?Snowflake} + */ + 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 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; + } + + /** + * 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 embed channel ID, if enabled + * @type {?string} + * @name Guild#embedChannelID + */ + if (typeof data.embed_channel_id !== 'undefined') this.embedChannelID = data.embed_channel_id; + + /** + * The verification level of the guild + * @type {VerificationLevel} + */ + this.verificationLevel = VerificationLevels[data.verification_level]; + + /** + * The explicit content filter level of the guild + * @type {ExplicitContentFilterLevel} + */ + this.explicitContentFilter = ExplicitContentFilterLevels[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 the guild's default message notifications + * @type {DefaultMessageNotifications|number} + */ + this.defaultMessageNotifications = + DefaultMessageNotifications[data.default_message_notifications] || data.default_message_notifications; + + /** + * The value set for the guild's system channel flags + * @type {Readonly<SystemChannelFlags>} + */ + this.systemChannelFlags = new SystemChannelFlags(data.system_channel_flags).freeze(); + + /** + * 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 || 25000; + + /** + * The vanity URL code of the guild, if any + * @type {?string} + */ + this.vanityURLCode = data.vanity_url_code; + + /** + * The description of the guild, if any + * @type {?string} + */ + this.description = data.description; + + /** + * The hash of the guild banner + * @type {?string} + */ + this.banner = data.banner; + + 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.channels) { + this.channels.cache.clear(); + for (const rawChannel of data.channels) { + this.client.channels.add(rawChannel, this); + } + } + + if (data.roles) { + this.roles.cache.clear(); + for (const role of data.roles) this.roles.add(role); + } + + if (data.members) { + this.members.cache.clear(); + for (const guildUser of data.members) this.members.add(guildUser); + } + + if (data.owner_id) { + /** + * The user ID of this guild's owner + * @type {Snowflake} + */ + this.ownerID = data.owner_id; + } + + if (data.presences) { + for (const presence of data.presences) { + this.presences.add(Object.assign(presence, { guild: this })); + } + } + + if (data.voice_states) { + this.voiceStates.cache.clear(); + for (const voiceState of data.voice_states) { + this.voiceStates.add(voiceState); + } + } + + if (!this.emojis) { + /** + * A manager of the emojis belonging to this guild + * @type {GuildEmojiManager} + */ + this.emojis = new GuildEmojiManager(this); + if (data.emojis) for (const emoji of data.emojis) this.emojis.add(emoji); + } else if (data.emojis) { + this.client.actions.GuildEmojisUpdate.handle({ + guild_id: this.id, + emojis: data.emojis, + }); + } + } + + /** + * The URL to this guild's banner. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} + */ + bannerURL({ format, size } = {}) { + if (!this.banner) return null; + return this.client.rest.cdn.Banner(this.id, this.banner, format, size); + } + + /** + * The timestamp the guild was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * The time the guild was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The time the client user joined the guild + * @type {Date} + * @readonly + */ + get joinedAt() { + return new Date(this.joinedTimestamp); + } + + /** + * If this guild is partnered + * @type {boolean} + * @readonly + */ + get partnered() { + return this.features.includes('PARTNERED'); + } + + /** + * If this guild is verified + * @type {boolean} + * @readonly + */ + get verified() { + return this.features.includes('VERIFIED'); + } + + /** + * The URL to this guild's icon. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} + */ + iconURL({ format, size, dynamic } = {}) { + if (!this.icon) return null; + return this.client.rest.cdn.Icon(this.id, this.icon, format, size, dynamic); + } + + /** + * 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. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} + */ + splashURL({ format, size } = {}) { + if (!this.splash) return null; + return this.client.rest.cdn.Splash(this.id, this.splash, format, size); + } + + /** + * The owner of the guild + * @type {?GuildMember} + * @readonly + */ + get owner() { + return ( + this.members.cache.get(this.ownerID) || + (this.client.options.partials.includes(PartialTypes.GUILD_MEMBER) + ? this.members.add({ user: { id: this.ownerID } }, true) + : null) + ); + } + + /** + * AFK voice channel for this guild + * @type {?VoiceChannel} + * @readonly + */ + get afkChannel() { + return this.client.channels.cache.get(this.afkChannelID) || null; + } + + /** + * System channel for this guild + * @type {?TextChannel} + * @readonly + */ + get systemChannel() { + return this.client.channels.cache.get(this.systemChannelID) || null; + } + + /** + * Widget channel for this guild + * @type {?TextChannel} + * @readonly + */ + get widgetChannel() { + return this.client.channels.cache.get(this.widgetChannelID) || null; + } + + /** + * Embed channel for this guild + * @type {?TextChannel} + * @readonly + */ + get embedChannel() { + return this.client.channels.cache.get(this.embedChannelID) || null; + } + + /** + * 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.cache.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.cache.get(this.publicUpdatesChannelID) || null; + } + + /** + * The client user as a GuildMember of this guild + * @type {?GuildMember} + * @readonly + */ + get me() { + return ( + this.members.cache.get(this.client.user.id) || + (this.client.options.partials.includes(PartialTypes.GUILD_MEMBER) + ? this.members.add({ user: { id: this.client.user.id } }, true) + : null) + ); + } + + /** + * The voice state for the client user of this guild, if any + * @type {?VoiceState} + * @readonly + */ + get voice() { + return this.voiceStates.cache.get(this.client.user.id); + } + + /** + * Returns the GuildMember form of a User object, if the user is present in the guild. + * @param {UserResolvable} user The user that you want to obtain the GuildMember of + * @returns {?GuildMember} + * @example + * // Get the guild member of a user + * const member = guild.member(message.author); + */ + member(user) { + return this.members.resolve(user); + } + + /** + * Fetches this guild. + * @returns {Promise<Guild>} + */ + fetch() { + return this.client.api + .guilds(this.id) + .get() + .then(data => { + this._patch(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 + */ + + /** + * Fetches information on a banned user from this guild. + * @param {UserResolvable} user The User to fetch the ban info of + * @returns {Promise<BanInfo>} + */ + fetchBan(user) { + const id = this.client.users.resolveID(user); + if (!id) throw new Error('FETCH_BAN_RESOLVE_ID'); + return this.client.api + .guilds(this.id) + .bans(id) + .get() + .then(ban => ({ + reason: ban.reason, + user: this.client.users.add(ban.user), + })); + } + + /** + * Fetches a collection of banned users in this guild. + * @returns {Promise<Collection<Snowflake, BanInfo>>} + */ + fetchBans() { + return this.client.api + .guilds(this.id) + .bans.get() + .then(bans => + bans.reduce((collection, ban) => { + collection.set(ban.user.id, { + reason: ban.reason, + user: this.client.users.add(ban.user), + }); + return collection; + }, new Collection()), + ); + } + + /** + * 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.api + .guilds(this.id) + .integrations.get() + .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 the integration + * @param {string} reason Reason for creating the integration + * @returns {Promise<Guild>} + */ + createIntegration(data, reason) { + return this.client.api + .guilds(this.id) + .integrations.post({ data, reason }) + .then(() => this); + } + + /** + * Fetches 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.api + .guilds(this.id) + .invites.get() + .then(inviteItems => { + const invites = new Collection(); + for (const inviteItem of inviteItems) { + const invite = new Invite(this.client, inviteItem); + invites.set(invite.code, invite); + } + return invites; + }); + } + + /** + * Obtains a guild preview for this guild from Discord, only available for public guilds. + * @returns {Promise<GuildPreview>} + */ + fetchPreview() { + return this.client.api + .guilds(this.id) + .preview.get() + .then(data => new GuildPreview(this.client, data)); + } + + /** + * 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('VANITY_URL')); + } + return this.client.api + .guilds(this.id, 'vanity-url') + .get() + .then(res => res.code); + } + + /** + * Fetches 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.api + .guilds(this.id) + .webhooks.get() + .then(data => { + const hooks = new Collection(); + for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook)); + return hooks; + }); + } + + /** + * Fetches available voice regions. + * @returns {Promise<Collection<string, VoiceRegion>>} + */ + fetchVoiceRegions() { + return this.client.api + .guilds(this.id) + .regions.get() + .then(res => { + const regions = new Collection(); + for (const region of res) regions.set(region.id, new VoiceRegion(region)); + return regions; + }); + } + + /** + * The Guild Embed object + * @typedef {Object} GuildEmbedData + * @property {boolean} enabled Whether the embed is enabled + * @property {?GuildChannel} 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.api + .guilds(this.id) + .embed.get() + .then(data => ({ + enabled: data.enabled, + channel: data.channel_id ? this.channels.cache.get(data.channel_id) : null, + })); + } + + /** + * Fetches 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 {number} [options.limit] Limit number of entries + * @param {UserResolvable} [options.user] Only show entries involving this user + * @param {AuditLogAction|number} [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 = {}) { + if (options.before && options.before instanceof GuildAuditLogs.Entry) options.before = options.before.id; + if (typeof options.type === 'string') options.type = GuildAuditLogs.Actions[options.type]; + + return this.client.api + .guilds(this.id) + ['audit-logs'].get({ + query: { + before: options.before, + limit: options.limit, + user_id: this.client.users.resolveID(options.user), + action_type: options.type, + }, + }) + .then(data => GuildAuditLogs.build(this, data)); + } + + /** + * 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>|RoleResolvable[]} [options.roles] Roles to add to the member + * (requires `MANAGE_ROLES`) + * @param {boolean} [options.mute] Whether the member should be muted (requires `MUTE_MEMBERS`) + * @param {boolean} [options.deaf] Whether the member should be deafened (requires `DEAFEN_MEMBERS`) + * @returns {Promise<GuildMember>} + */ + addMember(user, options) { + user = this.client.users.resolveID(user); + if (!user) return Promise.reject(new TypeError('INVALID_TYPE', 'user', 'UserResolvable')); + if (this.members.cache.has(user)) return Promise.resolve(this.members.cache.get(user)); + options.access_token = options.accessToken; + if (options.roles) { + const roles = []; + for (let role of options.roles instanceof Collection ? options.roles.values() : options.roles) { + role = this.roles.resolve(role); + if (!role) { + return Promise.reject( + new TypeError('INVALID_TYPE', 'options.roles', 'Array or Collection of Roles or Snowflakes', true), + ); + } + roles.push(role.id); + } + options.roles = roles; + } + return this.client.api + .guilds(this.id) + .members(user) + .put({ data: options }) + .then(data => this.members.add(data)); + } + + /** + * 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 {VerificationLevel|number} [verificationLevel] The verification level of the guild + * @property {ExplicitContentFilterLevel|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 {GuildMemberResolvable} [owner] The owner of the guild + * @property {Base64Resolvable} [splash] The splash screen of the guild + * @property {Base64Resolvable} [banner] The banner of the guild + * @property {DefaultMessageNotifications|number} [defaultMessageNotifications] The default message notifications + * @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 this guild + * @returns {Promise<Guild>} + * @example + * // Set the guild name and region + * guild.edit({ + * name: 'Discord Guild', + * region: 'london', + * }) + * .then(updated => console.log(`New guild name ${updated} in region ${updated.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 = + typeof data.verificationLevel === 'number' + ? Number(data.verificationLevel) + : VerificationLevels.indexOf(data.verificationLevel); + } + if (typeof data.afkChannel !== 'undefined') { + _data.afk_channel_id = this.client.channels.resolveID(data.afkChannel); + } + if (typeof data.systemChannel !== 'undefined') { + _data.system_channel_id = this.client.channels.resolveID(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.users.resolveID(data.owner); + if (data.splash) _data.splash = data.splash; + if (data.banner) _data.banner = data.banner; + if (typeof data.explicitContentFilter !== 'undefined') { + _data.explicit_content_filter = + typeof data.explicitContentFilter === 'number' + ? data.explicitContentFilter + : ExplicitContentFilterLevels.indexOf(data.explicitContentFilter); + } + if (typeof data.defaultMessageNotifications !== 'undefined') { + _data.default_message_notifications = + typeof data.defaultMessageNotifications === 'string' + ? DefaultMessageNotifications.indexOf(data.defaultMessageNotifications) + : data.defaultMessageNotifications; + } + if (typeof data.systemChannelFlags !== 'undefined') { + _data.system_channel_flags = SystemChannelFlags.resolve(data.systemChannelFlags); + } + return this.client.api + .guilds(this.id) + .patch({ data: _data, reason }) + .then(newData => this.client.actions.GuildUpdate.handle(newData).updated); + } + + /** + * Edits the level of the explicit content filter. + * @param {ExplicitContentFilterLevel|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); + } + + /* eslint-disable max-len */ + /** + * 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); + } + /* eslint-enable max-len */ + + /** + * 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); + } + + /** + * Edits 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(updated => console.log(`Updated guild name to ${guild}`)) + * .catch(console.error); + */ + setName(name, reason) { + return this.edit({ name }, reason); + } + + /** + * Edits 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(updated => console.log(`Updated guild region to ${updated.region}`)) + * .catch(console.error); + */ + setRegion(region, reason) { + return this.edit({ region }, reason); + } + + /** + * Edits the verification level of the guild. + * @param {VerificationLevel|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(updated => console.log(`Updated guild verification level to ${guild.verificationLevel}`)) + * .catch(console.error); + */ + setVerificationLevel(verificationLevel, reason) { + return this.edit({ verificationLevel }, reason); + } + + /** + * Edits 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(updated => console.log(`Updated guild AFK channel to ${guild.afkChannel.name}`)) + * .catch(console.error); + */ + setAFKChannel(afkChannel, reason) { + return this.edit({ afkChannel }, reason); + } + + /** + * Edits 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>} + * @example + * // Edit the guild system channel + * guild.setSystemChannel(channel) + * .then(updated => console.log(`Updated guild system channel to ${guild.systemChannel.name}`)) + * .catch(console.error); + */ + setSystemChannel(systemChannel, reason) { + return this.edit({ systemChannel }, reason); + } + + /** + * Edits 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(updated => console.log(`Updated guild AFK timeout to ${guild.afkTimeout}`)) + * .catch(console.error); + */ + setAFKTimeout(afkTimeout, reason) { + return this.edit({ afkTimeout }, reason); + } + + /** + * Sets 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(updated => console.log('Updated the guild icon')) + * .catch(console.error); + */ + async setIcon(icon, reason) { + return this.edit({ icon: await DataResolver.resolveImage(icon), 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.cache.first()) + * .then(updated => console.log(`Updated the guild owner to ${updated.owner.displayName}`)) + * .catch(console.error); + */ + setOwner(owner, reason) { + return this.edit({ owner }, reason); + } + + /** + * Sets a new guild splash screen. + * @param {Base64Resolvable|BufferResolvable} 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(updated => console.log('Updated the guild splash')) + * .catch(console.error); + */ + async setSplash(splash, reason) { + return this.edit({ splash: await DataResolver.resolveImage(splash), reason }); + } + + /** + * Sets a new guild banner. + * @param {Base64Resolvable|BufferResolvable} banner The new banner of the guild + * @param {string} [reason] Reason for changing the guild's banner + * @returns {Promise<Guild>} + * @example + * guild.setBanner('./banner.png') + * .then(updated => console.log('Updated the guild banner')) + * .catch(console.error); + */ + async setBanner(banner, reason) { + return this.edit({ banner: await DataResolver.resolveImage(banner), reason }); + } + + /** + * 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.setChannelPositions([{ channel: channelID, position: newChannelIndex }]) + * .then(guild => console.log(`Updated channel positions for ${guild}`)) + * .catch(console.error); + */ + setChannelPositions(channelPositions) { + const updatedChannels = channelPositions.map(r => ({ + id: this.client.channels.resolveID(r.channel), + position: r.position, + })); + + return this.client.api + .guilds(this.id) + .channels.patch({ data: updatedChannels }) + .then( + () => + this.client.actions.GuildChannelsPositionUpdate.handle({ + guild_id: this.id, + channels: updatedChannels, + }).guild, + ); + } + + /** + * The data needed for updating a guild role's position + * @typedef {Object} GuildRolePosition + * @property {RoleResolveable} role The ID of the role + * @property {number} position The position to update + */ + + /** + * Batch-updates the guild's role positions + * @param {GuildRolePosition[]} rolePositions Role positions to update + * @returns {Promise<Guild>} + * @example + * guild.setRolePositions([{ role: roleID, position: updatedRoleIndex }]) + * .then(guild => console.log(`Role permissions updated for ${guild}`)) + * .catch(console.error); + */ + setRolePositions(rolePositions) { + // Make sure rolePositions are prepared for API + rolePositions = rolePositions.map(o => ({ + id: this.roles.resolveID(o.role), + position: o.position, + })); + + // Call the API to update role positions + return this.client.api + .guilds(this.id) + .roles.patch({ + data: rolePositions, + }) + .then( + () => + this.client.actions.GuildRolesPositionUpdate.handle({ + guild_id: this.id, + roles: rolePositions, + }).guild, + ); + } + + /** + * 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.api + .guilds(this.id) + .embed.patch({ + data: { + enabled: embed.enabled, + channel_id: this.channels.resolveID(embed.channel), + }, + reason, + }) + .then(() => this); + } + + /** + * Leaves the guild. + * @returns {Promise<Guild>} + * @example + * // Leave a guild + * guild.leave() + * .then(g => console.log(`Left the guild ${g}`)) + * .catch(console.error); + */ + leave() { + if (this.ownerID === this.client.user.id) return Promise.reject(new Error('GUILD_OWNED')); + return this.client.api + .users('@me') + .guilds(this.id) + .delete() + .then(() => this.client.actions.GuildDelete.handle({ id: this.id }).guild); + } + + /** + * Deletes 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.api + .guilds(this.id) + .delete() + .then(() => this.client.actions.GuildDelete.handle({ id: this.id }).guild); + } + + /** + * 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 && + guild instanceof this.constructor && + this.id === guild.id && + this.available === guild.available && + this.splash === guild.splash && + this.region === guild.region && + this.name === guild.name && + this.memberCount === guild.memberCount && + this.large === guild.large && + this.icon === guild.icon && + this.ownerID === guild.ownerID && + this.verificationLevel === guild.verificationLevel && + this.embedEnabled === guild.embedEnabled && + (this.features === guild.features || + (this.features.length === guild.features.length && + this.features.every((feat, i) => feat === guild.features[i]))); + + if (equal) { + if (this.embedChannel) { + if (!guild.embedChannel || this.embedChannel.id !== guild.embedChannel.id) equal = false; + } else if (guild.embedChannel) { + equal = false; + } + } + + return equal; + } + + /** + * When concatenated with a string, this automatically returns the guild's name instead of the Guild object. + * @returns {string} + * @example + * // Logs: Hello from My Guild! + * console.log(`Hello from ${guild}!`); + */ + toString() { + return this.name; + } + + toJSON() { + const json = super.toJSON({ + available: false, + createdTimestamp: true, + nameAcronym: true, + presences: false, + voiceStates: false, + }); + json.iconURL = this.iconURL(); + json.splashURL = this.splashURL(); + json.bannerURL = this.bannerURL(); + return json; + } + + /** + * Creates a collection of this guild's roles, sorted by their position and IDs. + * @returns {Collection<Role>} + * @private + */ + _sortedRoles() { + return Util.discordSort(this.roles.cache); + } + + /** + * Creates a collection of this guild's or a specific category's channels, sorted by their position and IDs. + * @param {GuildChannel} [channel] Category to get the channels of + * @returns {Collection<GuildChannel>} + * @private + */ + _sortedChannels(channel) { + const category = channel.type === ChannelTypes.CATEGORY; + return Util.discordSort( + this.channels.cache.filter( + c => + (['text', 'news', 'store'].includes(channel.type) + ? ['text', 'news', 'store'].includes(c.type) + : c.type === channel.type) && + (category || c.parent === channel.parent), + ), + ); + } +} + +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..b3522b0 --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildAuditLogs.js @@ -0,0 +1,509 @@ +'use strict'; + +const Integration = require('./Integration'); +const Webhook = require('./Webhook'); +const Collection = require('../util/Collection'); +const { PartialTypes } = require('../util/Constants'); +const Snowflake = require('../util/Snowflake'); +const Util = require('../util/Util'); + +/** + * 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.users.add(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|GuildEmoji|Invite|Webhook|Integration} AuditLogEntryTarget + */ + + /** + * Finds the target type from the entry action. + * @param {AuditLogAction} target The action target + * @returns {AuditLogTargetType} + */ + 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 Targets.UNKNOWN; + } + + /** + * 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'; + } + + toJSON() { + return Util.flatten(this); + } +} + +/** + * Audit logs entry. + */ +class GuildAuditLogsEntry { + 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 presentation + * @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.options.partials.includes(PartialTypes.USER) + ? guild.client.users.add({ id: data.user_id }) + : guild.client.users.cache.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.cache.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.cache.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.cache.get(data.options.id) || { id: data.options.id, type: 'member' }; + break; + + case 'role': + this.extra = guild.roles.cache.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.target = 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.options.partials.includes(PartialTypes.USER) + ? guild.client.users.add({ id: data.target_id }) + : guild.client.users.cache.get(data.target_id); + } else if (targetType === Targets.GUILD) { + this.target = guild.client.guilds.cache.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) { + this.target = guild.members.fetch(guild.client.user.id).then(me => { + if (me.permissions.has('MANAGE_GUILD')) { + const change = this.changes.find(c => c.key === 'code'); + return guild.fetchInvites().then(invites => { + this.target = invites.find(i => i.code === (change.new || change.old)); + }); + } else { + this.target = this.changes.reduce((o, c) => { + o[c.key] = c.new || c.old; + return o; + }, {}); + return this.target; + } + }); + } 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.cache.get(data.target_id) || { id: data.target_id } + : guild.client.users.cache.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`].cache.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 at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + toJSON() { + return Util.flatten(this, { createdTimestamp: true }); + } +} + +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..2e80eca --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildChannel.js @@ -0,0 +1,609 @@ +'use strict'; + +const Channel = require('./Channel'); +const Invite = require('./Invite'); +const PermissionOverwrites = require('./PermissionOverwrites'); +const Role = require('./Role'); +const { Error, TypeError } = require('../errors'); +const Collection = require('../util/Collection'); +const Permissions = require('../util/Permissions'); +const Util = require('../util/Util'); + +/** + * Represents a guild channel from any of the following: + * - {@link TextChannel} + * - {@link VoiceChannel} + * - {@link CategoryChannel} + * - {@link NewsChannel} + * - {@link StoreChannel} + * @extends {Channel} + */ +class GuildChannel extends Channel { + /** + * @param {Guild} guild The guild the guild channel is part of + * @param {Object} data The data for the guild channel + */ + constructor(guild, data) { + super(guild.client, data); + + /** + * The guild the channel is in + * @type {Guild} + */ + this.guild = guild; + } + + _patch(data) { + super._patch(data); + + /** + * The name of the guild channel + * @type {string} + */ + this.name = data.name; + + /** + * The raw position of the channel from discord + * @type {number} + */ + this.rawPosition = 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 category parent of this channel + * @type {?CategoryChannel} + * @readonly + */ + get parent() { + return this.guild.channels.cache.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.bitfield === value.deny.bitfield && + testVal.allow.bitfield === value.allow.bitfield + ); + }); + } + + /** + * The position of the channel + * @type {number} + * @readonly + */ + get position() { + const sorted = this.guild._sortedChannels(this); + return sorted.array().indexOf(sorted.get(this.id)); + } + + /** + * Gets the overall set of permissions for a member or role in this channel, taking into account channel overwrites. + * @param {GuildMemberResolvable|RoleResolvable} memberOrRole The member or role to obtain the overall permissions for + * @returns {?Readonly<Permissions>} + */ + permissionsFor(memberOrRole) { + const member = this.guild.members.resolve(memberOrRole); + if (member) return this.memberPermissions(member); + const role = this.guild.roles.resolve(memberOrRole); + if (role) return this.rolePermissions(role); + return null; + } + + overwritesFor(member, verified = false, roles = null) { + if (!verified) member = this.guild.members.resolve(member); + if (!member) return []; + + roles = roles || member.roles.cache; + 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, + }; + } + + /** + * Gets the overall set of permissions for a member in this channel, taking into account channel overwrites. + * @param {GuildMember} member The member to obtain the overall permissions for + * @returns {Readonly<Permissions>} + * @private + */ + memberPermissions(member) { + if (member.id === this.guild.ownerID) return new Permissions(Permissions.ALL).freeze(); + + const roles = member.roles.cache; + 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 {Role} role The role to obtain the overall permissions for + * @returns {Readonly<Permissions>} + * @private + */ + rolePermissions(role) { + if (role.permissions.has(Permissions.FLAGS.ADMINISTRATOR)) return new Permissions(Permissions.ALL).freeze(); + + const everyoneOverwrites = this.permissionOverwrites.get(this.guild.id); + const roleOverwrites = this.permissionOverwrites.get(role.id); + + return role.permissions + .remove(everyoneOverwrites ? everyoneOverwrites.deny : 0) + .add(everyoneOverwrites ? everyoneOverwrites.allow : 0) + .remove(roleOverwrites ? roleOverwrites.deny : 0) + .add(roleOverwrites ? roleOverwrites.allow : 0) + .freeze(); + } + + /** + * Replaces the permission overwrites in this channel. + * @param {OverwriteResolvable[]|Collection<Snowflake, OverwriteResolvable>} overwrites + * Permission overwrites the channel gets updated with + * @param {string} [reason] Reason for updating the channel overwrites + * @returns {Promise<GuildChannel>} + * @example + * channel.overwritePermissions([ + * { + * id: message.author.id, + * deny: ['VIEW_CHANNEL'], + * }, + * ], 'Needed to change permissions'); + */ + overwritePermissions(overwrites, reason) { + if (!Array.isArray(overwrites) && !(overwrites instanceof Collection)) { + return Promise.reject( + new TypeError('INVALID_TYPE', 'overwrites', 'Array or Collection of Permission Overwrites', true), + ); + } + return this.edit({ permissionOverwrites: overwrites, reason }).then(() => this); + } + + /** + * Updates Overwrites for a user or role in this channel. (creates if non-existent) + * @param {RoleResolvable|UserResolvable} userOrRole The user or role to update + * @param {PermissionOverwriteOptions} options The options for the update + * @param {string} [reason] Reason for creating/editing this overwrite + * @returns {Promise<GuildChannel>} + * @example + * // Update or Create permission overwrites for a message author + * message.channel.updateOverwrite(message.author, { + * SEND_MESSAGES: false + * }) + * .then(channel => console.log(channel.permissionOverwrites.get(message.author.id))) + * .catch(console.error); + */ + updateOverwrite(userOrRole, options, reason) { + userOrRole = this.guild.roles.resolve(userOrRole) || this.client.users.resolve(userOrRole); + if (!userOrRole) return Promise.reject(new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role', true)); + + const existing = this.permissionOverwrites.get(userOrRole.id); + if (existing) return existing.update(options, reason).then(() => this); + return this.createOverwrite(userOrRole, options, reason); + } + + /** + * Overwrites the permissions for a user or role in this channel. (replaces if existent) + * @param {RoleResolvable|UserResolvable} userOrRole The user or role to update + * @param {PermissionOverwriteOptions} options The options for the update + * @param {string} [reason] Reason for creating/editing this overwrite + * @returns {Promise<GuildChannel>} + * @example + * // Create or Replace permissions overwrites for a message author + * message.channel.createOverwrite(message.author, { + * SEND_MESSAGES: false + * }) + * .then(channel => console.log(channel.permissionOverwrites.get(message.author.id))) + * .catch(console.error); + */ + createOverwrite(userOrRole, options, reason) { + userOrRole = this.guild.roles.resolve(userOrRole) || this.client.users.resolve(userOrRole); + if (!userOrRole) return Promise.reject(new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role', true)); + + const type = userOrRole instanceof Role ? 'role' : 'member'; + const { allow, deny } = PermissionOverwrites.resolveOverwriteOptions(options); + + return this.client.api + .channels(this.id) + .permissions[userOrRole.id].put({ + data: { id: userOrRole.id, type, allow: allow.bitfield, deny: deny.bitfield }, + reason, + }) + .then(() => this); + } + + /** + * Locks in the permission overwrites from the parent channel. + * @returns {Promise<GuildChannel>} + */ + lockPermissions() { + if (!this.parent) return Promise.reject(new Error('GUILD_CHANNEL_ORPHAN')); + const permissionOverwrites = this.parent.permissionOverwrites.map(overwrite => overwrite.toJSON()); + return this.edit({ permissionOverwrites }); + } + + /** + * 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.cache.values()) { + if (this.permissionsFor(member).has('VIEW_CHANNEL', false)) { + members.set(member.id, member); + } + } + return members; + } + + /** + * The data for a guild channel. + * @typedef {Object} ChannelData + * @property {string} [name] The name of the channel + * @property {number} [position] The position of the channel + * @property {string} [topic] The topic of the text channel + * @property {boolean} [nsfw] Whether the channel is NSFW + * @property {number} [bitrate] The bitrate of the voice channel + * @property {number} [userLimit] The user limit of the voice channel + * @property {Snowflake} [parentID] The parent ID of the channel + * @property {boolean} [lockPermissions] + * Lock the permissions of the channel to what the parent's permissions are + * @property {OverwriteResolvable[]|Collection<Snowflake, OverwriteResolvable>} [permissionOverwrites] + * Permission overwrites for the channel + * @property {number} [rateLimitPerUser] The ratelimit per user for the channel in seconds + */ + + /** + * 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); + */ + async edit(data, reason) { + if (typeof data.position !== 'undefined') { + await Util.setPosition( + this, + data.position, + false, + this.guild._sortedChannels(this), + this.client.api.guilds(this.guild.id).channels, + reason, + ).then(updatedChannels => { + this.client.actions.GuildChannelsPositionUpdate.handle({ + guild_id: this.guild.id, + channels: updatedChannels, + }); + }); + } + + const permission_overwrites = + data.permissionOverwrites && data.permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild)); + + const newData = await this.client.api.channels(this.id).patch({ + data: { + name: (data.name || this.name).trim(), + topic: data.topic, + nsfw: data.nsfw, + bitrate: data.bitrate || this.bitrate, + user_limit: typeof data.userLimit !== 'undefined' ? data.userLimit : this.userLimit, + parent_id: data.parentID, + lock_permissions: data.lockPermissions, + rate_limit_per_user: data.rateLimitPerUser, + permission_overwrites, + }, + reason, + }); + + const clone = this._clone(); + clone._patch(newData); + return clone; + } + + /** + * Sets 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); + } + + /** + * Sets the category parent of this channel. + * @param {?CategoryChannel|Snowflake} channel Parent channel + * @param {Object} [options={}] Options to pass + * @param {boolean} [options.lockPermissions=true] Lock the permissions to what the parent's permissions are + * @param {string} [options.reason] Reason for modifying the parent of this channel + * @returns {Promise<GuildChannel>} + * @example + * // Add a parent to a channel + * message.channel.setParent('355908108431917066', { lockPermissions: false }) + * .then(channel => console.log(`New parent of ${message.channel.name}: ${channel.name}`)) + * .catch(console.error); + */ + setParent(channel, { lockPermissions = true, reason } = {}) { + return this.edit( + { + // eslint-disable-next-line no-prototype-builtins + parentID: channel !== null ? (channel.hasOwnProperty('id') ? channel.id : channel) : null, + lockPermissions, + }, + reason, + ); + } + + /** + * Sets 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(newChannel => console.log(`Channel's new topic is ${newChannel.topic}`)) + * .catch(console.error); + */ + setTopic(topic, reason) { + return this.edit({ topic }, reason); + } + + /** + * Sets a new position for the guild channel. + * @param {number} position The new position for the guild channel + * @param {Object} [options] Options for setting position + * @param {boolean} [options.relative=false] Change the position relative to its current value + * @param {string} [options.reason] Reason for changing the position + * @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, reason } = {}) { + return Util.setPosition( + this, + position, + relative, + this.guild._sortedChannels(this), + this.client.api.guilds(this.guild.id).channels, + reason, + ).then(updatedChannels => { + this.client.actions.GuildChannelsPositionUpdate.handle({ + guild_id: this.guild.id, + channels: updatedChannels, + }); + return this; + }); + } + + /** + * Creates an invite to this guild channel. + * @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} [options.reason] Reason for creating this + * @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({ temporary = false, maxAge = 86400, maxUses = 0, unique, reason } = {}) { + return this.client.api + .channels(this.id) + .invites.post({ + data: { + temporary, + max_age: maxAge, + max_uses: maxUses, + unique, + }, + reason, + }) + .then(invite => new Invite(this.client, invite)); + } + + /** + * Fetches a collection of invites to this guild channel. + * Resolves with a collection mapping invites by their codes. + * @returns {Promise<Collection<string, Invite>>} + */ + async fetchInvites() { + const inviteItems = await this.client.api.channels(this.id).invites.get(); + const invites = new Collection(); + for (const inviteItem of inviteItems) { + const invite = new Invite(this.client, inviteItem); + invites.set(invite.code, invite); + } + return invites; + } + + /* eslint-disable max-len */ + /** + * Clones this channel. + * @param {Object} [options] The options + * @param {string} [options.name=this.name] Name of the new channel + * @param {OverwriteResolvable[]|Collection<Snowflake, OverwriteResolvable>} [options.permissionOverwrites=this.permissionOverwrites] + * Permission overwrites of the new channel + * @param {string} [options.type=this.type] Type of the new channel + * @param {string} [options.topic=this.topic] Topic of the new channel (only text) + * @param {boolean} [options.nsfw=this.nsfw] Whether the new channel is nsfw (only text) + * @param {number} [options.bitrate=this.bitrate] Bitrate of the new channel in bits (only voice) + * @param {number} [options.userLimit=this.userLimit] Maximum amount of users allowed in the new channel (only voice) + * @param {number} [options.rateLimitPerUser=ThisType.rateLimitPerUser] Ratelimit per user for the new channel (only text) + * @param {ChannelResolvable} [options.parent=this.parent] Parent of the new channel + * @param {string} [options.reason] Reason for cloning this channel + * @returns {Promise<GuildChannel>} + */ + clone(options = {}) { + 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, + }, + options, + ); + return this.guild.channels.create(options.name, options); + } + /* eslint-enable max-len */ + + /** + * 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.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_CHANNELS, false); + } + + /** + * Whether the channel is manageable by the client user + * @type {boolean} + * @readonly + */ + get manageable() { + if (this.client.user.id === this.guild.ownerID) return true; + if (this.type === 'voice') { + if (!this.permissionsFor(this.client.user).has(Permissions.FLAGS.CONNECT, false)) { + return false; + } + } else if (!this.viewable) { + return false; + } + return this.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_CHANNELS, false); + } + + /** + * Whether the channel is viewable by the client user + * @type {boolean} + * @readonly + */ + get viewable() { + 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.VIEW_CHANNEL, false); + } + + /** + * 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(console.log) + * .catch(console.error); + */ + delete(reason) { + return this.client.api + .channels(this.id) + .delete({ reason }) + .then(() => this); + } +} + +module.exports = GuildChannel; diff --git a/node_modules/discord.js/src/structures/GuildEmoji.js b/node_modules/discord.js/src/structures/GuildEmoji.js new file mode 100644 index 0000000..d4189e9 --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildEmoji.js @@ -0,0 +1,159 @@ +'use strict'; + +const BaseGuildEmoji = require('./BaseGuildEmoji'); +const { Error } = require('../errors'); +const GuildEmojiRoleManager = require('../managers/GuildEmojiRoleManager'); +const Permissions = require('../util/Permissions'); + +/** + * Represents a custom emoji. + * @extends {BaseGuildEmoji} + */ +class GuildEmoji extends BaseGuildEmoji { + /** + * @name GuildEmoji + * @kind constructor + * @memberof GuildEmoji + * @param {Client} client The instantiating client + * @param {Object} data The data for the guild emoji + * @param {Guild} guild The guild the guild emoji is part of + */ + + /** + * The guild this emoji is part of + * @type {Guild} + * @name GuildEmoji#guild + */ + + _clone() { + const clone = super._clone(); + clone._roles = this._roles.slice(); + return clone; + } + + /** + * Whether the emoji is deletable by the client user + * @type {boolean} + * @readonly + */ + get deletable() { + if (!this.guild.me) throw new Error('GUILD_UNCACHED_ME'); + return !this.managed && this.guild.me.hasPermission(Permissions.FLAGS.MANAGE_EMOJIS); + } + + /** + * A manager for roles this emoji is active for. + * @type {GuildEmojiRoleManager} + * @readonly + */ + get roles() { + return new GuildEmojiRoleManager(this); + } + + /** + * Fetches the author for this emoji + * @returns {Promise<User>} + */ + fetchAuthor() { + if (this.managed) { + return Promise.reject(new Error('EMOJI_MANAGED')); + } else { + if (!this.guild.me) return Promise.reject(new Error('GUILD_UNCACHED_ME')); + if (!this.guild.me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS)) { + return Promise.reject(new Error('MISSING_MANAGE_EMOJIS_PERMISSION', this.guild)); + } + } + return this.client.api + .guilds(this.guild.id) + .emojis(this.id) + .get() + .then(emoji => this.client.users.add(emoji.user)); + } + + /** + * Data for editing an emoji. + * @typedef {Object} GuildEmojiEditData + * @property {string} [name] The name of the emoji + * @property {Collection<Snowflake, Role>|RoleResolvable[]} [roles] Roles to restrict emoji to + */ + + /** + * Edits the emoji. + * @param {GuildEmojiEditData} data The new data for the emoji + * @param {string} [reason] Reason for editing this emoji + * @returns {Promise<GuildEmoji>} + * @example + * // Edit an emoji + * emoji.edit({ name: 'newemoji' }) + * .then(e => console.log(`Edited emoji ${e}`)) + * .catch(console.error); + */ + edit(data, reason) { + const roles = data.roles ? data.roles.map(r => r.id || r) : undefined; + return this.client.api + .guilds(this.guild.id) + .emojis(this.id) + .patch({ + data: { + name: data.name, + roles, + }, + reason, + }) + .then(newData => { + const clone = this._clone(); + clone._patch(newData); + return clone; + }); + } + + /** + * Sets the name of the emoji. + * @param {string} name The new name for the emoji + * @param {string} [reason] Reason for changing the emoji's name + * @returns {Promise<GuildEmoji>} + */ + setName(name, reason) { + return this.edit({ name }, reason); + } + + /** + * Deletes the emoji. + * @param {string} [reason] Reason for deleting the emoji + * @returns {Promise<GuildEmoji>} + */ + delete(reason) { + return this.client.api + .guilds(this.guild.id) + .emojis(this.id) + .delete({ reason }) + .then(() => this); + } + + /** + * Whether this emoji is the same as another one. + * @param {GuildEmoji|Object} other The emoji to compare it to + * @returns {boolean} Whether the emoji is equal to the given emoji or not + */ + equals(other) { + if (other instanceof GuildEmoji) { + return ( + other.id === this.id && + other.name === this.name && + other.managed === this.managed && + other.requiresColons === this.requiresColons && + other.roles.cache.size === this.roles.cache.size && + other.roles.cache.every(role => this.roles.cache.has(role.id)) + ); + } else { + return ( + other.id === this.id && + other.name === this.name && + other.roles.length === this.roles.cache.size && + other.roles.every(role => this.roles.cache.has(role)) + ); + } + } +} + +module.exports = GuildEmoji; 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..f949517 --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildMember.js @@ -0,0 +1,410 @@ +'use strict'; + +const Base = require('./Base'); +const { Presence } = require('./Presence'); +const Role = require('./Role'); +const VoiceState = require('./VoiceState'); +const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const { Error } = require('../errors'); +const GuildMemberRoleManager = require('../managers/GuildMemberRoleManager'); +const Permissions = require('../util/Permissions'); + +/** + * Represents a member of a guild on Discord. + * @implements {TextBasedChannel} + * @extends {Base} + */ +class GuildMember extends Base { + /** + * @param {Client} client The instantiating client + * @param {Object} data The data for the guild member + * @param {Guild} guild The guild the member is part of + */ + constructor(client, data, guild) { + super(client); + + /** + * The guild that this member is part of + * @type {Guild} + */ + this.guild = guild; + + /** + * The user that this guild member instance represents + * @type {User} + * @name GuildMember#user + */ + if (data.user) this.user = client.users.add(data.user, true); + + /** + * The timestamp the member joined the guild at + * @type {?number} + */ + this.joinedTimestamp = null; + + /** + * The ID of the last message sent by the member in their guild, if one was sent + * @type {?Snowflake} + */ + this.lastMessageID = null; + + /** + * The ID of the channel for the last message sent by the member in their guild, if one was sent + * @type {?Snowflake} + */ + this.lastMessageChannelID = null; + + /** + * The timestamp of when the member used their Nitro boost on the guild, if it was used + * @type {?number} + */ + this.premiumSinceTimestamp = null; + + /** + * Whether the member has been removed from the guild + * @type {boolean} + */ + this.deleted = false; + + this._roles = []; + if (data) this._patch(data); + } + + _patch(data) { + /** + * The nickname of this member, if they have one + * @type {?string} + * @name GuildMember#nickname + */ + if (typeof data.nick !== 'undefined') this.nickname = data.nick; + + if (data.joined_at) this.joinedTimestamp = new Date(data.joined_at).getTime(); + if (data.premium_since) this.premiumSinceTimestamp = new Date(data.premium_since).getTime(); + + if (data.user) this.user = this.guild.client.users.add(data.user); + if (data.roles) this._roles = data.roles; + } + + _clone() { + const clone = super._clone(); + clone._roles = this._roles.slice(); + return clone; + } + + /** + * Whether this GuildMember is a partial + * @type {boolean} + * @readonly + */ + get partial() { + return !this.joinedTimestamp; + } + + /** + * A manager for the roles belonging to this member + * @type {GuildMemberRoleManager} + * @readonly + */ + get roles() { + return new GuildMemberRoleManager(this); + } + + /** + * The Message object of the last message sent by the member in their guild, if one was sent + * @type {?Message} + * @readonly + */ + get lastMessage() { + const channel = this.guild.channels.cache.get(this.lastMessageChannelID); + return (channel && channel.messages.cache.get(this.lastMessageID)) || null; + } + + /** + * The voice state of this member + * @type {VoiceState} + * @readonly + */ + get voice() { + return this.guild.voiceStates.cache.get(this.id) || new VoiceState(this.guild, { user_id: this.id }); + } + + /** + * 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 guild member + * @type {Presence} + * @readonly + */ + get presence() { + return ( + this.guild.presences.cache.get(this.id) || + new Presence(this.client, { + user: { + id: this.id, + }, + guild: this.guild, + }) + ); + } + + /** + * The displayed color of this member in base 10 + * @type {number} + * @readonly + */ + get displayColor() { + const role = this.roles.color; + return (role && role.color) || 0; + } + + /** + * The displayed color of this member in hexadecimal + * @type {string} + * @readonly + */ + get displayHexColor() { + const role = this.roles.color; + return (role && role.hexColor) || '#000000'; + } + + /** + * The ID of this member + * @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 {Readonly<Permissions>} + * @readonly + */ + get permissions() { + if (this.user.id === this.guild.ownerID) return new Permissions(Permissions.ALL).freeze(); + return new Permissions(this.roles.cache.map(role => role.permissions)).freeze(); + } + + /** + * Whether the client user is above this user in the hierarchy, according to role position and guild ownership. + * This is a prerequisite for many moderative actions. + * @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; + if (!this.guild.me) throw new Error('GUILD_UNCACHED_ME'); + return this.guild.me.roles.highest.comparePositionTo(this.roles.highest) > 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 a member in a guild channel, + * taking into account roles and permission overwrites. + * @param {ChannelResolvable} channel The guild channel to use as context + * @returns {Readonly<Permissions>} + */ + permissionsIn(channel) { + channel = this.guild.channels.resolve(channel); + if (!channel) throw new Error('GUILD_CHANNEL_RESOLVE'); + return channel.memberPermissions(this); + } + + /** + * Checks if any of this member's roles have a permission. + * @param {PermissionResolvable} permission Permission(s) to check for + * @param {Object} [options] Options + * @param {boolean} [options.checkAdmin=true] Whether to allow the administrator permission to override + * @param {boolean} [options.checkOwner=true] Whether to allow being the guild's owner to override + * @returns {boolean} + */ + hasPermission(permission, { checkAdmin = true, checkOwner = true } = {}) { + if (checkOwner && this.user.id === this.guild.ownerID) return true; + return this.roles.cache.some(r => r.permissions.has(permission, checkAdmin)); + } + + /** + * The data for editing a guild 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>} + */ + async edit(data, reason) { + if (data.channel) { + data.channel = this.guild.channels.resolve(data.channel); + if (!data.channel || data.channel.type !== 'voice') { + throw new Error('GUILD_VOICE_CHANNEL_RESOLVE'); + } + data.channel_id = data.channel.id; + data.channel = undefined; + } else if (data.channel === null) { + data.channel_id = null; + data.channel = undefined; + } + if (data.roles) data.roles = data.roles.map(role => (role instanceof Role ? role.id : role)); + let endpoint = this.client.api.guilds(this.guild.id); + if (this.user.id === this.client.user.id) { + const keys = Object.keys(data); + if (keys.length === 1 && keys[0] === 'nick') endpoint = endpoint.members('@me').nick; + else endpoint = endpoint.members(this.id); + } else { + endpoint = endpoint.members(this.id); + } + await endpoint.patch({ data, reason }); + + const clone = this._clone(); + data.user = this.user; + clone._patch(data); + return clone; + } + + /** + * Sets 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>} + */ + 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 member. + * @returns {Promise<DMChannel>} + */ + deleteDM() { + return this.user.deleteDM(); + } + + /** + * Kicks this member from the guild. + * @param {string} [reason] Reason for kicking user + * @returns {Promise<GuildMember>} + */ + kick(reason) { + return this.client.api + .guilds(this.guild.id) + .members(this.user.id) + .delete({ reason }) + .then(() => this); + } + + /** + * Bans this guild member. + * @param {Object} [options] Options for the ban + * @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 + * guildMember.ban({ days: 7, reason: 'They deserved it' }) + * .then(console.log) + * .catch(console.error); + */ + ban(options) { + return this.guild.members.ban(this, options); + } + + /** + * Fetches this GuildMember. + * @returns {Promise<GuildMember>} + */ + fetch() { + return this.guild.members.fetch(this.id, true); + } + + /** + * When concatenated with a string, this automatically returns the user's mention instead of the GuildMember object. + * @returns {string} + * @example + * // Logs: Hello from <@123456789012345678>! + * console.log(`Hello from ${member}!`); + */ + toString() { + return `<@${this.nickname ? '!' : ''}${this.user.id}>`; + } + + toJSON() { + return super.toJSON({ + guild: 'guildID', + user: 'userID', + displayName: true, + speaking: false, + lastMessage: false, + lastMessageID: false, + roles: true, + }); + } + + // These are here only for documentation purposes - they are implemented by TextBasedChannel + /* eslint-disable no-empty-function */ + send() {} +} + +TextBasedChannel.applyToClass(GuildMember); + +module.exports = GuildMember; diff --git a/node_modules/discord.js/src/structures/GuildPreview.js b/node_modules/discord.js/src/structures/GuildPreview.js new file mode 100644 index 0000000..681ff60 --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildPreview.js @@ -0,0 +1,157 @@ +'use strict'; + +const Base = require('./Base'); +const GuildPreviewEmoji = require('./GuildPreviewEmoji'); +const Collection = require('../util/Collection'); + +/** + * Represents the data about the guild any bot can preview, connected to the specified public guild. + * @extends {Base} + */ +class GuildPreview extends Base { + constructor(client, data) { + super(client); + + if (!data) return; + + this._patch(data); + } + + /** + * Builds the public guild with the provided data. + * @param {*} data The raw data of the public guild + * @private + */ + _patch(data) { + /** + * The id of this public guild + * @type {string} + */ + this.id = data.id; + + /** + * The name of this public guild + * @type {string} + */ + this.name = data.name; + + /** + * The icon of this public guild + * @type {?string} + */ + this.icon = data.icon; + + /** + * The splash icon of this public guild + * @type {?string} + */ + this.splash = data.splash; + + /** + * The discovery splash icon of this public guild + * @type {?string} + */ + this.discoverySplash = data.discovery_splash; + + /** + * An array of enabled guild features + * @type {Features[]} + */ + this.features = data.features; + + /** + * The approximate count of members in this public guild + * @type {number} + */ + this.approximateMemberCount = data.approximate_member_count; + + /** + * The approximate count of online members in this public guild + * @type {number} + */ + this.approximatePresenceCount = data.approximate_presence_count; + + /** + * The description for this public guild + * @type {?string} + */ + this.description = data.description; + + if (!this.emojis) { + /** + * Collection of emojis belonging to this public guild + * @type {Collection<Snowflake, GuildPreviewEmoji>} + */ + this.emojis = new Collection(); + } else { + this.emojis.clear(); + } + for (const emoji of data.emojis) { + this.emojis.set(emoji.id, new GuildPreviewEmoji(this.client, emoji, this)); + } + } + + /** + * The URL to this public guild's splash. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} + */ + splashURL({ format, size } = {}) { + if (!this.splash) return null; + return this.client.rest.cdn.Splash(this.id, this.splash, format, size); + } + + /** + * The URL to this public guild's discovery splash. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} + */ + discoverySplashURL({ format, size } = {}) { + if (!this.discoverySplash) return null; + return this.client.rest.cdn.DiscoverySplash(this.id, this.discoverySplash, format, size); + } + + /** + * The URL to this public guild's icon. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} + */ + iconURL({ format, size, dynamic } = {}) { + if (!this.icon) return null; + return this.client.rest.cdn.Icon(this.id, this.icon, format, size, dynamic); + } + + /** + * Fetches this public guild. + * @returns {Promise<GuildPreview>} + */ + fetch() { + return this.client.api + .guilds(this.id) + .preview.get() + .then(data => { + this._patch(data); + return this; + }); + } + + /** + * When concatenated with a string, this automatically returns the guild's name instead of the Guild object. + * @returns {string} + * @example + * // Logs: Hello from My Guild! + * console.log(`Hello from ${previewGuild}!`); + */ + toString() { + return this.name; + } + + toJSON() { + const json = super.toJSON(); + json.iconURL = this.iconURL(); + json.splashURL = this.splashURL(); + return json; + } +} + +module.exports = GuildPreview; diff --git a/node_modules/discord.js/src/structures/GuildPreviewEmoji.js b/node_modules/discord.js/src/structures/GuildPreviewEmoji.js new file mode 100644 index 0000000..4c70903 --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildPreviewEmoji.js @@ -0,0 +1,26 @@ +'use strict'; + +const BaseGuildEmoji = require('./BaseGuildEmoji'); + +/** + * Represents an instance of an emoji belonging to a public guild obtained through Discord's preview endpoint. + * @extends {BaseGuildEmoji} + */ +class GuildPreviewEmoji extends BaseGuildEmoji { + /** + * The public guild this emoji is part of + * @type {GuildPreview} + * @name GuildPreviewEmoji#guild + */ + + /** + * Set of roles this emoji is active for + * @type {Set<Snowflake>} + * @readonly + */ + get roles() { + return new Set(this._roles); + } +} + +module.exports = GuildPreviewEmoji; 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..49a0227 --- /dev/null +++ b/node_modules/discord.js/src/structures/Integration.js @@ -0,0 +1,167 @@ +'use strict'; + +const Base = require('./Base'); + +/** + * 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 extends Base { + constructor(client, data, guild) { + super(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.cache.get(data.role_id); + + /** + * The user for this integration + * @type {User} + */ + this.user = this.client.users.add(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; + } + + /** + * Sync this integration + * @returns {Promise<Integration>} + */ + sync() { + this.syncing = true; + return this.client.api + .guilds(this.guild.id) + .integrations(this.id) + .post() + .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 = null; + } + if ('expireGracePeriod' in data) { + data.expire_grace_period = data.expireGracePeriod; + data.expireGracePeriod = null; + } + // The option enable_emoticons is only available for Twitch at this moment + return this.client.api + .guilds(this.guild.id) + .integrations(this.id) + .patch({ 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.api + .guilds(this.guild.id) + .integrations(this.id) + .delete({ reason }) + .then(() => this); + } + + toJSON() { + return super.toJSON({ + role: 'roleID', + guild: 'guildID', + user: 'userID', + }); + } +} + +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..6833266 --- /dev/null +++ b/node_modules/discord.js/src/structures/Invite.js @@ -0,0 +1,194 @@ +'use strict'; + +const Base = require('./Base'); +const { Endpoints } = require('../util/Constants'); +const Permissions = require('../util/Permissions'); + +/** + * Represents an invitation to a guild channel. + * <warn>The only guaranteed properties are `code`, `channel`, and `url`. Other properties can be missing.</warn> + * @extends {Base} + */ +class Invite extends Base { + constructor(client, data) { + super(client); + this._patch(data); + } + + _patch(data) { + /** + * The guild the invite is for + * @type {?Guild} + */ + this.guild = data.guild ? this.client.guilds.add(data.guild, false) : null; + + /** + * 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 = 'approximate_presence_count' in data ? data.approximate_presence_count : null; + + /** + * The approximate total number of members of the guild this invite is for + * @type {?number} + */ + this.memberCount = 'approximate_member_count' in data ? data.approximate_member_count : null; + + /** + * Whether or not this invite is temporary + * @type {?boolean} + */ + this.temporary = 'temporary' in data ? data.temporary : null; + + /** + * The maximum age of the invite, in seconds, 0 if never expires + * @type {?number} + */ + this.maxAge = 'max_age' in data ? data.max_age : null; + + /** + * How many times this invite has been used + * @type {?number} + */ + this.uses = 'uses' in data ? data.uses : null; + + /** + * The maximum uses of this invite + * @type {?number} + */ + this.maxUses = 'max_uses' in data ? data.max_uses : null; + + /** + * The user who created this invite + * @type {?User} + */ + this.inviter = data.inviter ? this.client.users.add(data.inviter) : null; + + /** + * The target user for this invite + * @type {?User} + */ + this.targetUser = data.target_user ? this.client.users.add(data.target_user) : null; + + /** + * The type of the target user: + * * 1: STREAM + * @typedef {number} TargetUser + */ + + /** + * The target user type + * @type {?TargetUser} + */ + this.targetUserType = typeof data.target_user_type === 'number' ? data.target_user_type : null; + + /** + * The channel the invite is for + * @type {Channel} + */ + this.channel = this.client.channels.add(data.channel, this.guild, false); + + /** + * The timestamp the invite was created at + * @type {?number} + */ + this.createdTimestamp = 'created_at' in data ? new Date(data.created_at).getTime() : null; + } + + /** + * The time the invite was created at + * @type {?Date} + * @readonly + */ + get createdAt() { + return this.createdTimestamp ? new Date(this.createdTimestamp) : null; + } + + /** + * Whether the invite is deletable by the client user + * @type {boolean} + * @readonly + */ + get deletable() { + const guild = this.guild; + if (!guild || !this.client.guilds.cache.has(guild.id)) return false; + if (!guild.me) throw new Error('GUILD_UNCACHED_ME'); + return ( + this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_CHANNELS, false) || + guild.me.permissions.has(Permissions.FLAGS.MANAGE_GUILD) + ); + } + + /** + * The timestamp the invite will expire at + * @type {?number} + * @readonly + */ + get expiresTimestamp() { + return this.createdTimestamp && this.maxAge ? this.createdTimestamp + this.maxAge * 1000 : null; + } + + /** + * The time the invite will expire at + * @type {?Date} + * @readonly + */ + get expiresAt() { + const { expiresTimestamp } = this; + return expiresTimestamp ? new Date(expiresTimestamp) : null; + } + + /** + * The URL to the invite + * @type {string} + * @readonly + */ + get url() { + return Endpoints.invite(this.client.options.http.invite, this.code); + } + + /** + * Deletes this invite. + * @param {string} [reason] Reason for deleting this invite + * @returns {Promise<Invite>} + */ + delete(reason) { + return this.client.api.invites[this.code].delete({ reason }).then(() => this); + } + + /** + * When concatenated with a string, this automatically concatenates the invite's URL instead of the object. + * @returns {string} + * @example + * // Logs: Invite: https://discord.gg/A1b2C3 + * console.log(`Invite: ${invite}`); + */ + toString() { + return this.url; + } + + toJSON() { + return super.toJSON({ + url: true, + expiresTimestamp: true, + presenceCount: false, + memberCount: false, + uses: false, + channel: 'channelID', + inviter: 'inviterID', + guild: 'guildID', + }); + } + + valueOf() { + return this.code; + } +} + +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..06ed12a --- /dev/null +++ b/node_modules/discord.js/src/structures/Message.js @@ -0,0 +1,623 @@ +'use strict'; + +const APIMessage = require('./APIMessage'); +const Base = require('./Base'); +const ClientApplication = require('./ClientApplication'); +const MessageAttachment = require('./MessageAttachment'); +const Embed = require('./MessageEmbed'); +const Mentions = require('./MessageMentions'); +const ReactionCollector = require('./ReactionCollector'); +const { Error, TypeError } = require('../errors'); +const ReactionManager = require('../managers/ReactionManager'); +const Collection = require('../util/Collection'); +const { MessageTypes } = require('../util/Constants'); +const MessageFlags = require('../util/MessageFlags'); +const Permissions = require('../util/Permissions'); +const Util = require('../util/Util'); + +/** + * Represents a message on Discord. + * @extends {Base} + */ +class Message extends Base { + /** + * @param {Client} client The instantiating client + * @param {Object} data The data for the message + * @param {TextChannel|DMChannel} channel The channel the message was sent in + */ + constructor(client, data, channel) { + super(client); + + /** + * The channel that the message was sent in + * @type {TextChannel|DMChannel} + */ + this.channel = channel; + + /** + * Whether this message has been deleted + * @type {boolean} + */ + this.deleted = false; + + if (data) this._patch(data); + } + + _patch(data) { + /** + * The ID of the message + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The type of the message + * @type {MessageType} + */ + this.type = MessageTypes[data.type]; + + /** + * The content of the message + * @type {string} + */ + this.content = data.content; + + /** + * The author of the message + * @type {?User} + */ + this.author = data.author ? this.client.users.add(data.author, !data.webhook_id) : null; + + /** + * Whether or not this message is pinned + * @type {boolean} + */ + this.pinned = data.pinned; + + /** + * Whether or not the message was Text-To-Speech + * @type {boolean} + */ + this.tts = data.tts; + + /** + * A random number 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(e, true)); + + /** + * A collection of attachments in the message - e.g. Pictures - mapped by their ID + * @type {Collection<Snowflake, MessageAttachment>} + */ + this.attachments = new Collection(); + if (data.attachments) { + for (const attachment of data.attachments) { + this.attachments.set(attachment.id, new MessageAttachment(attachment.url, attachment.filename, 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 manager of the reactions belonging to this message + * @type {ReactionManager} + */ + this.reactions = new ReactionManager(this); + if (data.reactions && data.reactions.length > 0) { + for (const reaction of data.reactions) { + this.reactions.add(reaction); + } + } + + /** + * 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; + + /** + * Supplemental application information for group activities + * @type {?ClientApplication} + */ + this.application = data.application ? new ClientApplication(this.client, data.application) : null; + + /** + * Group activity + * @type {?MessageActivity} + */ + this.activity = data.activity + ? { + partyID: data.activity.party_id, + type: data.activity.type, + } + : null; + + /** + * The previous versions of the message, sorted with the most recent first + * @type {Message[]} + * @private + */ + this._edits = []; + + if (this.member && data.member) { + this.member._patch(data.member); + } else if (data.member && this.guild && this.author) { + this.guild.members.add(Object.assign(data.member, { user: this.author })); + } + + /** + * 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; + } + + /** + * Whether or not this message is a partial + * @type {boolean} + * @readonly + */ + get partial() { + return typeof this.content !== 'string' || !this.author; + } + + /** + * Updates the message. + * @param {Object} data Raw Discord message update data + * @private + */ + patch(data) { + const clone = this._clone(); + 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(e, true)); + 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 MessageAttachment(attachment.url, attachment.filename, attachment)); + } + } else { + this.attachments = new Collection(this.attachments); + } + + this.mentions = new Mentions( + this, + 'mentions' in data ? data.mentions : this.mentions.users, + 'mention_roles' in data ? data.mention_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(); + } + + /** + * 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} + * @readonly + */ + get member() { + return this.guild ? this.guild.member(this.author) || null : null; + } + + /** + * The time the message was sent at + * @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 this 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() { + // eslint-disable-next-line eqeqeq + return this.content != null ? Util.cleanContent(this.content, this) : null; + } + + /** + * 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 createReactionCollector 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, false))) + ); + } + + /** + * 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, false)) + ); + } + + /** + * Options that can be passed into editMessage. + * @typedef {Object} MessageEditOptions + * @property {string} [content] Content to be edited + * @property {Object} [embed] An embed to be added/edited + * @property {string|boolean} [code] Language for optional codeblock formatting to apply + * @property {MessageMentionOptions} [allowedMentions] Which mentions should be parsed from the message content + */ + + /** + * Edits the content of the message. + * @param {StringResolvable|APIMessage} [content] The new content for the message + * @param {MessageEditOptions|MessageEmbed} [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(`Updated the content of a message to ${msg.content}`)) + * .catch(console.error); + */ + edit(content, options) { + const { data } = + content instanceof APIMessage ? content.resolveData() : APIMessage.create(this, content, options).resolveData(); + return this.client.api.channels[this.channel.id].messages[this.id].patch({ data }).then(d => { + const clone = this._clone(); + clone._patch(d); + return clone; + }); + } + + /** + * Pins this message to the channel's pinned messages. + * @returns {Promise<Message>} + */ + pin() { + return this.client.api + .channels(this.channel.id) + .pins(this.id) + .put() + .then(() => this); + } + + /** + * Unpins this message from the channel's pinned messages. + * @returns {Promise<Message>} + */ + unpin() { + return this.client.api + .channels(this.channel.id) + .pins(this.id) + .delete() + .then(() => this); + } + + /** + * Adds a reaction to the message. + * @param {EmojiIdentifierResolvable} 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.cache.get('123456789012345678')) + * .then(console.log) + * .catch(console.error); + */ + react(emoji) { + emoji = this.client.emojis.resolveIdentifier(emoji); + if (!emoji) throw new TypeError('EMOJI_TYPE'); + + return this.client.api + .channels(this.channel.id) + .messages(this.id) + .reactions(emoji, '@me') + .put() + .then( + () => + this.client.actions.MessageReactionAdd.handle({ + user: this.client.user, + channel: this.channel, + message: this, + emoji: Util.parseEmoji(emoji), + }).reaction, + ); + } + + /** + * Deletes the message. + * @param {Object} [options] Options + * @param {number} [options.timeout=0] How long to wait to delete the message in milliseconds + * @param {string} [options.reason] Reason for deleting this message, if it does not belong to the client user + * @returns {Promise<Message>} + * @example + * // Delete a message + * message.delete() + * .then(msg => console.log(`Deleted message from ${msg.author.username}`)) + * .catch(console.error); + */ + delete(options = {}) { + if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true); + const { timeout = 0, reason } = options; + if (timeout <= 0) { + return this.channel.messages.delete(this.id, reason).then(() => this); + } else { + return new Promise(resolve => { + this.client.setTimeout(() => { + resolve(this.delete({ reason })); + }, timeout); + }); + } + } + + /** + * Replies to the message. + * @param {StringResolvable|APIMessage} [content=''] The content for the message + * @param {MessageOptions|MessageAdditions} [options={}] The options to provide + * @returns {Promise<Message|Message[]>} + * @example + * // Reply to a message + * message.reply('Hey, I\'m a reply!') + * .then(() => console.log(`Sent a reply to ${message.author.username}`)) + * .catch(console.error); + */ + reply(content, options) { + return this.channel.send( + content instanceof APIMessage + ? content + : APIMessage.transformOptions(content, options, { reply: this.member || this.author }), + ); + } + + /** + * Fetch this message. + * @returns {Promise<Message>} + */ + fetch() { + return this.channel.messages.fetch(this.id, true); + } + + /** + * Fetches the webhook used to create this message. + * @returns {Promise<?Webhook>} + */ + fetchWebhook() { + if (!this.webhookID) return Promise.reject(new Error('WEBHOOK_MESSAGE')); + 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({ 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; + } + + toJSON() { + return super.toJSON({ + channel: 'channelID', + author: 'authorID', + application: 'applicationID', + guild: 'guildID', + cleanContent: true, + member: false, + reactions: false, + }); + } +} + +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..f5fb723 --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageAttachment.js @@ -0,0 +1,98 @@ +'use strict'; + +const Util = require('../util/Util'); + +/** + * Represents an attachment in a message. + */ +class MessageAttachment { + /** + * @param {BufferResolvable|Stream} attachment The file + * @param {string} [name=null] The name of the file, if any + * @param {Object} [data] Extra data + */ + constructor(attachment, name = null, data) { + this.attachment = attachment; + /** + * The name of this attachment + * @type {?string} + */ + this.name = name; + if (data) this._patch(data); + } + + /** + * Sets the file of this attachment. + * @param {BufferResolvable|Stream} attachment The file + * @param {string} [name=null] The name of the file, if any + * @returns {MessageAttachment} This attachment + */ + setFile(attachment, name = null) { + this.attachment = attachment; + this.name = name; + return this; + } + + /** + * Sets the name of this attachment. + * @param {string} name The name of the file + * @returns {MessageAttachment} This attachment + */ + setName(name) { + this.name = name; + return this; + } + + _patch(data) { + /** + * The ID of this attachment + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The size of this attachment in bytes + * @type {number} + */ + this.size = 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 or video) + * @type {?number} + */ + this.height = typeof data.height !== 'undefined' ? data.height : null; + + /** + * The width of this attachment (if an image or video) + * @type {?number} + */ + this.width = typeof data.width !== 'undefined' ? data.width : null; + } + + /** + * Whether or not this attachment has been marked as a spoiler + * @type {boolean} + * @readonly + */ + get spoiler() { + return Util.basename(this.url).startsWith('SPOILER_'); + } + + toJSON() { + return Util.flatten(this); + } +} + +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..f8f3d5a --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageCollector.js @@ -0,0 +1,129 @@ +'use strict'; + +const Collector = require('./interfaces/Collector'); +const { Events } = require('../util/Constants'); + +/** + * @typedef {CollectorOptions} MessageCollectorOptions + * @property {number} max The maximum amount of messages to collect + * @property {number} maxProcessed The maximum amount of messages to process + */ + +/** + * Collects messages on a channel. + * Will automatically stop if the channel (`'channelDelete'`) or guild (`'guildDelete'`) are deleted. + * @extends {Collector} + */ +class MessageCollector extends Collector { + /** + * @param {TextChannel|DMChannel} 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; + + const bulkDeleteListener = messages => { + for (const message of messages.values()) this.handleDispose(message); + }; + this._handleChannelDeletion = this._handleChannelDeletion.bind(this); + this._handleGuildDeletion = this._handleGuildDeletion.bind(this); + + if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() + 1); + this.client.on(Events.MESSAGE_CREATE, this.handleCollect); + this.client.on(Events.MESSAGE_DELETE, this.handleDispose); + this.client.on(Events.MESSAGE_BULK_DELETE, bulkDeleteListener); + this.client.on(Events.CHANNEL_DELETE, this._handleChannelDeletion); + this.client.on(Events.GUILD_DELETE, this._handleGuildDeletion); + + this.once('end', () => { + this.client.removeListener(Events.MESSAGE_CREATE, this.handleCollect); + this.client.removeListener(Events.MESSAGE_DELETE, this.handleDispose); + this.client.removeListener(Events.MESSAGE_BULK_DELETE, bulkDeleteListener); + this.client.removeListener(Events.CHANNEL_DELETE, this._handleChannelDeletion); + this.client.removeListener(Events.GUILD_DELETE, this._handleGuildDeletion); + if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() - 1); + }); + } + + /** + * Handles a message for possible collection. + * @param {Message} message The message that could be collected + * @returns {?Snowflake} + * @private + */ + collect(message) { + /** + * Emitted whenever a message is collected. + * @event MessageCollector#collect + * @param {Message} message The message that was collected + */ + if (message.channel.id !== this.channel.id) return null; + this.received++; + return message.id; + } + + /** + * Handles a message for possible disposal. + * @param {Message} message The message that could be disposed of + * @returns {?Snowflake} + */ + dispose(message) { + /** + * Emitted whenever a message is disposed of. + * @event MessageCollector#dispose + * @param {Message} message The message that was disposed of + */ + return message.channel.id === this.channel.id ? message.id : null; + } + + /** + * Checks after un/collection to see if the collector is done. + * @returns {?string} + * @private + */ + endReason() { + if (this.options.max && this.collected.size >= this.options.max) return 'limit'; + if (this.options.maxProcessed && this.received === this.options.maxProcessed) return 'processedLimit'; + return null; + } + + /** + * Handles checking if the channel has been deleted, and if so, stops the collector with the reason 'channelDelete'. + * @private + * @param {GuildChannel} channel The channel that was deleted + * @returns {void} + */ + _handleChannelDeletion(channel) { + if (channel.id === this.channel.id) { + this.stop('channelDelete'); + } + } + + /** + * Handles checking if the guild has been deleted, and if so, stops the collector with the reason 'guildDelete'. + * @private + * @param {Guild} guild The guild that was deleted + * @returns {void} + */ + _handleGuildDeletion(guild) { + if (this.channel.guild && guild.id === this.channel.guild.id) { + this.stop('guildDelete'); + } + } +} + +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..2b993a3 --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageEmbed.js @@ -0,0 +1,454 @@ +'use strict'; + +const { RangeError } = require('../errors'); +const Util = require('../util/Util'); + +/** + * Represents an embed in a message (image/video preview, rich embed, etc.) + */ +class MessageEmbed { + /** + * @name MessageEmbed + * @kind constructor + * @memberof MessageEmbed + * @param {MessageEmbed|Object} [data={}] MessageEmbed to clone or raw embed data + */ + + constructor(data = {}, skipValidation = false) { + this.setup(data, skipValidation); + } + + setup(data, skipValidation) { + /** + * The type of this embed, either: + * * `rich` - a rich embed + * * `image` - an image embed + * * `video` - a video embed + * * `gifv` - a gifv embed + * * `article` - an article embed + * * `link` - a link 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 this embed + * @type {?number} + */ + this.color = Util.resolveColor(data.color); + + /** + * The timestamp of this embed + * @type {?number} + */ + this.timestamp = data.timestamp ? new Date(data.timestamp).getTime() : null; + + /** + * @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 + */ + + /** + * The fields of this embed + * @type {EmbedField[]} + */ + this.fields = []; + if (data.fields) { + this.fields = skipValidation ? data.fields.map(Util.cloneObject) : this.constructor.normalizeFields(data.fields); + } + + /** + * @typedef {Object} MessageEmbedThumbnail + * @property {string} url URL for this thumbnail + * @property {string} proxyURL ProxyURL for this thumbnail + * @property {number} height Height of this thumbnail + * @property {number} width Width of this thumbnail + */ + + /** + * The thumbnail of this embed (if there is one) + * @type {?MessageEmbedThumbnail} + */ + this.thumbnail = data.thumbnail + ? { + url: data.thumbnail.url, + proxyURL: data.thumbnail.proxyURL || data.thumbnail.proxy_url, + height: data.thumbnail.height, + width: data.thumbnail.width, + } + : null; + + /** + * @typedef {Object} MessageEmbedImage + * @property {string} url URL for this image + * @property {string} proxyURL ProxyURL for this image + * @property {number} height Height of this image + * @property {number} width Width of this image + */ + + /** + * The image of this embed, if there is one + * @type {?MessageEmbedImage} + */ + this.image = data.image + ? { + url: data.image.url, + proxyURL: data.image.proxyURL || data.image.proxy_url, + height: data.image.height, + width: data.image.width, + } + : null; + + /** + * @typedef {Object} MessageEmbedVideo + * @property {string} url URL of this video + * @property {string} proxyURL ProxyURL for this video + * @property {number} height Height of this video + * @property {number} width Width of this video + */ + + /** + * The video of this embed (if there is one) + * @type {?MessageEmbedVideo} + * @readonly + */ + this.video = data.video + ? { + url: data.video.url, + proxyURL: data.video.proxyURL || data.video.proxy_url, + height: data.video.height, + width: data.video.width, + } + : null; + + /** + * @typedef {Object} MessageEmbedAuthor + * @property {string} name The name of this author + * @property {string} url URL of this author + * @property {string} iconURL URL of the icon for this author + * @property {string} proxyIconURL Proxied URL of the icon for this author + */ + + /** + * The author of this embed (if there is one) + * @type {?MessageEmbedAuthor} + */ + this.author = data.author + ? { + name: data.author.name, + url: data.author.url, + iconURL: data.author.iconURL || data.author.icon_url, + proxyIconURL: data.author.proxyIconURL || data.author.proxy_icon_url, + } + : null; + + /** + * @typedef {Object} MessageEmbedProvider + * @property {string} name The name of this provider + * @property {string} url URL of this provider + */ + + /** + * The provider of this embed (if there is one) + * @type {?MessageEmbedProvider} + */ + this.provider = data.provider + ? { + name: data.provider.name, + url: data.provider.name, + } + : null; + + /** + * @typedef {Object} MessageEmbedFooter + * @property {string} text The text of this footer + * @property {string} iconURL URL of the icon for this footer + * @property {string} proxyIconURL Proxied URL of the icon for this footer + */ + + /** + * The footer of this embed + * @type {?MessageEmbedFooter} + */ + this.footer = data.footer + ? { + text: data.footer.text, + iconURL: data.footer.iconURL || data.footer.icon_url, + proxyIconURL: data.footer.proxyIconURL || data.footer.proxy_icon_url, + } + : null; + + /** + * The files of this embed + * @type {Array<FileOptions|string|MessageAttachment>} + */ + this.files = data.files || []; + } + + /** + * The date displayed on this embed + * @type {?Date} + * @readonly + */ + get createdAt() { + return this.timestamp ? new Date(this.timestamp) : null; + } + + /** + * The hexadecimal version of the embed color, with a leading hash + * @type {?string} + * @readonly + */ + get hexColor() { + return this.color ? `#${this.color.toString(16).padStart(6, '0')}` : null; + } + + /** + * The accumulated length for the embed title, description, fields 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) + ); + } + + /** + * Adds a field to the embed (max 25). + * @param {StringResolvable} name The name of this field + * @param {StringResolvable} value The value of this field + * @param {boolean} [inline=false] If this field will be displayed inline + * @returns {MessageEmbed} + */ + addField(name, value, inline) { + return this.addFields({ name, value, inline }); + } + + /** + * Adds fields to the embed (max 25). + * @param {...EmbedFieldData|EmbedFieldData[]} fields The fields to add + * @returns {MessageEmbed} + */ + addFields(...fields) { + this.fields.push(...this.constructor.normalizeFields(fields)); + return this; + } + + /** + * 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|EmbedFieldData[]} [fields] The replacing field objects + * @returns {MessageEmbed} + */ + spliceFields(index, deleteCount, ...fields) { + this.fields.splice(index, deleteCount, ...this.constructor.normalizeFields(...fields)); + 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. Multiple files can be attached. + * @param {Array<FileOptions|string|MessageAttachment>} files Files to attach + * @returns {MessageEmbed} + */ + attachFiles(files) { + this.files = this.files.concat(files); + return this; + } + + /** + * Sets the author of this embed. + * @param {StringResolvable} name The name of the author + * @param {string} [iconURL] The icon URL of the author + * @param {string} [url] The URL of the author + * @returns {MessageEmbed} + */ + setAuthor(name, iconURL, url) { + this.author = { name: Util.resolveString(name), iconURL, url }; + return this; + } + + /** + * Sets the color of this embed. + * @param {ColorResolvable} color The color of the embed + * @returns {MessageEmbed} + */ + setColor(color) { + this.color = Util.resolveColor(color); + return this; + } + + /** + * Sets the description of this embed. + * @param {StringResolvable} description The description + * @returns {MessageEmbed} + */ + setDescription(description) { + description = Util.resolveString(description); + this.description = description; + return this; + } + + /** + * Sets the footer of this embed. + * @param {StringResolvable} text The text of the footer + * @param {string} [iconURL] The icon URL of the footer + * @returns {MessageEmbed} + */ + setFooter(text, iconURL) { + text = Util.resolveString(text); + this.footer = { text, iconURL }; + return this; + } + + /** + * Sets the image of this embed. + * @param {string} url The URL of the image + * @returns {MessageEmbed} + */ + setImage(url) { + this.image = { url }; + return this; + } + + /** + * Sets the thumbnail of this embed. + * @param {string} url The URL of the thumbnail + * @returns {MessageEmbed} + */ + setThumbnail(url) { + this.thumbnail = { url }; + return this; + } + + /** + * Sets the timestamp of this embed. + * @param {Date|number} [timestamp=Date.now()] The timestamp or date + * @returns {MessageEmbed} + */ + setTimestamp(timestamp = Date.now()) { + if (timestamp instanceof Date) timestamp = timestamp.getTime(); + this.timestamp = timestamp; + return this; + } + + /** + * Sets the title of this embed. + * @param {StringResolvable} title The title + * @returns {MessageEmbed} + */ + setTitle(title) { + title = Util.resolveString(title); + this.title = title; + return this; + } + + /** + * Sets the URL of this embed. + * @param {string} url The URL + * @returns {MessageEmbed} + */ + setURL(url) { + this.url = url; + return this; + } + + /** + * 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, + thumbnail: this.thumbnail, + image: this.image, + author: this.author + ? { + name: this.author.name, + url: this.author.url, + icon_url: this.author.iconURL, + } + : null, + footer: this.footer + ? { + text: this.footer.text, + icon_url: this.footer.iconURL, + } + : 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) throw new RangeError('EMBED_FIELD_NAME'); + value = Util.resolveString(value); + if (!value) throw new RangeError('EMBED_FIELD_VALUE'); + return { name, value, inline }; + } + + /** + * @typedef {Object} EmbedFieldData + * @property {StringResolvable} name The name of this field + * @property {StringResolvable} value The value of this field + * @property {boolean} [inline] If this field will be displayed inline + */ + + /** + * Normalizes field input and resolves strings. + * @param {...EmbedFieldData|EmbedFieldData[]} fields Fields to normalize + * @returns {EmbedField[]} + */ + static normalizeFields(...fields) { + return fields + .flat(2) + .map(field => + this.normalizeField( + field && field.name, + field && field.value, + field && typeof field.inline === 'boolean' ? field.inline : false, + ), + ); + } +} + +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..cbdd1c7 --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageMentions.js @@ -0,0 +1,221 @@ +'use strict'; + +const Collection = require('../util/Collection'); +const { ChannelTypes } = require('../util/Constants'); +const Util = require('../util/Util'); + +/** + * Keeps track of mentions in a {@link Message}. + */ +class MessageMentions { + constructor(message, users, roles, everyone, crosspostedChannels) { + /** + * The client the message is from + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: message.client }); + + /** + * The guild the message is in + * @type {?Guild} + * @readonly + */ + Object.defineProperty(this, 'guild', { value: message.guild }); + + /** + * The initial message content + * @type {string} + * @readonly + * @private + */ + Object.defineProperty(this, '_content', { value: message.content }); + + /** + * 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) { + if (mention.member && message.guild) { + message.guild.members.add(Object.assign(mention.member, { user: mention })); + } + const user = message.client.users.add(mention); + this.users.set(user.id, user); + } + } + } 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</info> + * @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.cache.get(mention); + if (role) this.roles.set(role.id, role); + } + } + } else { + this.roles = new Collection(); + } + + /** + * Cached members for {@link MessageMention#members} + * @type {?Collection<Snowflake, GuildMember>} + * @private + */ + this._members = null; + + /** + * Cached channels for {@link MessageMention#channels} + * @type {?Collection<Snowflake, GuildChannel>} + * @private + */ + this._channels = null; + + /** + * Crossposted channel data. + * @typedef {Object} CrosspostedChannel + * @property {string} channelID ID of the mentioned channel + * @property {string} guildID ID of the guild that has the channel + * @property {string} type Type of the channel + * @property {string} name The name of the channel + */ + + if (crosspostedChannels) { + if (crosspostedChannels instanceof Collection) { + /** + * A collection of crossposted channels + * <info>Order as received from the API, not as they appear in the message content</info> + * @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</info> + * @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.cache.get(matches[1]); + if (chan) this._channels.set(chan.id, chan); + } + return this._channels; + } + + /** + * Checks if a user, guild member, role, or channel is mentioned. + * Takes into account user mentions, role mentions, and @everyone/@here mentions. + * @param {UserResolvable|GuildMember|Role|GuildChannel} data User/GuildMember/Role/Channel to check + * @param {Object} [options] Options + * @param {boolean} [options.ignoreDirect=false] - Whether to ignore direct mentions to the item + * @param {boolean} [options.ignoreRoles=false] - Whether to ignore role mentions to a guild member + * @param {boolean} [options.ignoreEveryone=false] - Whether to ignore everyone/here mentions + * @returns {boolean} + */ + has(data, { ignoreDirect = false, ignoreRoles = false, ignoreEveryone = false } = {}) { + if (!ignoreEveryone && this.everyone) return true; + const GuildMember = require('./GuildMember'); + if (!ignoreRoles && data instanceof GuildMember) { + for (const role of this.roles.values()) if (data.roles.cache.has(role.id)) return true; + } + + if (!ignoreDirect) { + const id = data.id || data; + return this.users.has(id) || this.channels.has(id) || this.roles.has(id); + } + + return false; + } + + toJSON() { + return Util.flatten(this, { + members: true, + channels: true, + }); + } +} + +/** + * 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 = /<@!?(\d{17,19})>/g; + +/** + * Regular expression that globally matches role mentions like `<@&297577916114403338>` + * @type {RegExp} + */ +MessageMentions.ROLES_PATTERN = /<@&(\d{17,19})>/g; + +/** + * Regular expression that globally matches channel mentions like `<#222079895583457280>` + * @type {RegExp} + */ +MessageMentions.CHANNELS_PATTERN = /<#(\d{17,19})>/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..771626e --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageReaction.js @@ -0,0 +1,135 @@ +'use strict'; + +const GuildEmoji = require('./GuildEmoji'); +const ReactionEmoji = require('./ReactionEmoji'); +const ReactionUserManager = require('../managers/ReactionUserManager'); +const Util = require('../util/Util'); + +/** + * Represents a reaction to a message. + */ +class MessageReaction { + /** + * @param {Client} client The instantiating client + * @param {Object} data The data for the message reaction + * @param {Message} message The message the reaction refers to + */ + constructor(client, data, message) { + /** + * The client that instantiated this message reaction + * @name MessageReaction#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + /** + * The message that this reaction refers to + * @type {Message} + */ + this.message = message; + + /** + * Whether the client has given this reaction + * @type {boolean} + */ + this.me = data.me; + + /** + * A manager of the users that have given this reaction + * @type {ReactionUserManager} + */ + this.users = new ReactionUserManager(client, undefined, this); + + this._emoji = new ReactionEmoji(this, data.emoji); + + this._patch(data); + } + + _patch(data) { + /** + * The number of people that have given the same reaction + * @type {?number} + * @name MessageReaction#count + */ + // eslint-disable-next-line eqeqeq + if (this.count == undefined) this.count = data.count; + } + + /** + * Removes all users from this reaction. + * @returns {Promise<MessageReaction>} + */ + async remove() { + await this.client.api + .channels(this.message.channel.id) + .messages(this.message.id) + .reactions(this._emoji.identifier) + .delete(); + return this; + } + + /** + * The emoji of this reaction, either an GuildEmoji object for known custom emojis, or a ReactionEmoji + * object which has fewer properties. Whatever the prototype of the emoji, it will still have + * `name`, `id`, `identifier` and `toString()` + * @type {GuildEmoji|ReactionEmoji} + * @readonly + */ + get emoji() { + if (this._emoji instanceof GuildEmoji) return this._emoji; + // Check to see if the emoji has become known to the client + if (this._emoji.id) { + const emojis = this.message.client.emojis.cache; + if (emojis.has(this._emoji.id)) { + const emoji = emojis.get(this._emoji.id); + this._emoji = emoji; + return emoji; + } + } + return this._emoji; + } + + /** + * Whether or not this reaction is a partial + * @type {boolean} + * @readonly + */ + get partial() { + return this.count === null; + } + + /** + * Fetch this reaction. + * @returns {Promise<MessageReaction>} + */ + async fetch() { + const message = await this.message.fetch(); + const existing = message.reactions.cache.get(this.emoji.id || this.emoji.name); + // The reaction won't get set when it has been completely removed + this._patch(existing || { count: 0 }); + return this; + } + + toJSON() { + return Util.flatten(this, { emoji: 'emojiID', message: 'messageID' }); + } + + _add(user) { + if (this.partial) return; + this.users.cache.set(user.id, user); + if (!this.me || user.id !== this.message.client.user.id || this.count === 0) this.count++; + if (!this.me) this.me = user.id === this.message.client.user.id; + } + + _remove(user) { + if (this.partial) return; + this.users.cache.delete(user.id); + if (!this.me || user.id !== this.message.client.user.id) this.count--; + if (user.id === this.message.client.user.id) this.me = false; + if (this.count <= 0 && this.users.cache.size === 0) { + this.message.reactions.cache.delete(this.emoji.id || this.emoji.name); + } + } +} + +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..76727fc --- /dev/null +++ b/node_modules/discord.js/src/structures/NewsChannel.js @@ -0,0 +1,18 @@ +'use strict'; + +const TextChannel = require('./TextChannel'); + +/** + * Represents a guild news channel on Discord. + * @extends {TextChannel} + */ +class NewsChannel extends TextChannel { + _patch(data) { + super._patch(data); + + // News channels don't have a rate limit per user, remove it + this.rateLimitPerUser = undefined; + } +} + +module.exports = NewsChannel; diff --git a/node_modules/discord.js/src/structures/PartialGroupDMChannel.js b/node_modules/discord.js/src/structures/PartialGroupDMChannel.js new file mode 100644 index 0000000..e398f23 --- /dev/null +++ b/node_modules/discord.js/src/structures/PartialGroupDMChannel.js @@ -0,0 +1,46 @@ +'use strict'; + +const Channel = require('./Channel'); +const { Error } = require('../errors'); + +/** + * Represents a Partial Group DM Channel on Discord. + * @extends {Channel} + */ +class PartialGroupDMChannel extends Channel { + constructor(client, data) { + super(client, data); + + /** + * The name of this Group DM Channel + * @type {string} + */ + this.name = data.name; + + /** + * The hash of the channel icon + * @type {?string} + */ + this.icon = data.icon; + } + + /** + * The URL to this channel's icon. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} + */ + iconURL({ format, size } = {}) { + if (!this.icon) return null; + return this.client.rest.cdn.GDMIcon(this.id, this.icon, format, size); + } + + delete() { + return Promise.reject(new Error('DELETE_GROUP_DM_CHANNEL')); + } + + fetch() { + return Promise.reject(new Error('FETCH_GROUP_DM_CHANNEL')); + } +} + +module.exports = PartialGroupDMChannel; 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..ea7a45e --- /dev/null +++ b/node_modules/discord.js/src/structures/PermissionOverwrites.js @@ -0,0 +1,189 @@ +'use strict'; + +const Role = require('./Role'); +const { TypeError } = require('../errors'); +const Permissions = require('../util/Permissions'); +const Util = require('../util/Util'); + +/** + * 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._patch(data); + } + + _patch(data) { + /** + * The ID of this overwrite, either a user ID or a role ID + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The type of a permission overwrite. It can be one of: + * * member + * * role + * @typedef {string} OverwriteType + */ + + /** + * The type of this overwrite + * @type {OverwriteType} + */ + this.type = data.type; + + /** + * The permissions that are denied for the user or role. + * @type {Readonly<Permissions>} + */ + this.deny = new Permissions(data.deny).freeze(); + + /** + * The permissions that are allowed for the user or role. + * @type {Readonly<Permissions>} + */ + this.allow = new Permissions(data.allow).freeze(); + } + + /** + * Updates this permissionOverwrites. + * @param {PermissionOverwriteOptions} options The options for the update + * @param {string} [reason] Reason for creating/editing this overwrite + * @returns {Promise<PermissionOverwrites>} + * @example + * // Update permission overwrites + * permissionOverwrites.update({ + * SEND_MESSAGES: false + * }) + * .then(channel => console.log(channel.permissionOverwrites.get(message.author.id))) + * .catch(console.error); + */ + update(options, reason) { + const { allow, deny } = this.constructor.resolveOverwriteOptions(options, this); + + return this.channel.client.api + .channels(this.channel.id) + .permissions[this.id].put({ + data: { id: this.id, type: this.type, allow: allow.bitfield, deny: deny.bitfield }, + reason, + }) + .then(() => this); + } + + /** + * Deletes this Permission Overwrite. + * @param {string} [reason] Reason for deleting this overwrite + * @returns {Promise<PermissionOverwrites>} + */ + delete(reason) { + return this.channel.client.api.channels[this.channel.id].permissions[this.id].delete({ reason }).then(() => this); + } + + toJSON() { + return Util.flatten(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 + */ + + /** + * @typedef {object} ResolvedOverwriteOptions + * @property {Permissions} allow The allowed permissions + * @property {Permissions} deny The denied permissions + */ + + /** + * Resolves bitfield permissions overwrites from an object. + * @param {PermissionOverwriteOptions} options The options for the update + * @param {Object} initialPermissions The initial permissions + * @param {PermissionResolvable} initialPermissions.allow Initial allowed permissions + * @param {PermissionResolvable} initialPermissions.deny Initial denied permissions + * @returns {ResolvedOverwriteOptions} + */ + static resolveOverwriteOptions(options, { allow, deny } = {}) { + allow = new Permissions(allow); + deny = new Permissions(deny); + + for (const [perm, value] of Object.entries(options)) { + if (value === true) { + allow.add(Permissions.FLAGS[perm]); + deny.remove(Permissions.FLAGS[perm]); + } else if (value === false) { + allow.remove(Permissions.FLAGS[perm]); + deny.add(Permissions.FLAGS[perm]); + } else if (value === null) { + allow.remove(Permissions.FLAGS[perm]); + deny.remove(Permissions.FLAGS[perm]); + } + } + + return { allow, deny }; + } + + /** + * The raw data for a permission overwrite + * @typedef {Object} RawOverwriteData + * @property {Snowflake} id The id of the overwrite + * @property {number} allow The permissions to allow + * @property {number} deny The permissions to deny + * @property {OverwriteType} type The type of this OverwriteData + */ + + /** + * Data that can be resolved into {@link RawOverwriteData} + * @typedef {PermissionOverwrites|OverwriteData} OverwriteResolvable + */ + + /** + * Data that can be used for a permission overwrite + * @typedef {Object} OverwriteData + * @property {GuildMemberResolvable|RoleResolvable} id Member or role this overwrite is for + * @property {PermissionResolvable} [allow] The permissions to allow + * @property {PermissionResolvable} [deny] The permissions to deny + * @property {OverwriteType} [type] The type of this OverwriteData + */ + + /** + * Resolves an overwrite into {@link RawOverwriteData}. + * @param {OverwriteResolvable} overwrite The overwrite-like data to resolve + * @param {Guild} guild The guild to resolve from + * @returns {RawOverwriteData} + */ + static resolve(overwrite, guild) { + if (overwrite instanceof this) return overwrite.toJSON(); + if (typeof overwrite.id === 'string' && ['role', 'member'].includes(overwrite.type)) { + return { ...overwrite, allow: Permissions.resolve(overwrite.allow), deny: Permissions.resolve(overwrite.deny) }; + } + + const userOrRole = guild.roles.resolve(overwrite.id) || guild.client.users.resolve(overwrite.id); + if (!userOrRole) throw new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role', true); + const type = userOrRole instanceof Role ? 'role' : 'member'; + + return { + id: userOrRole.id, + type, + allow: Permissions.resolve(overwrite.allow), + deny: Permissions.resolve(overwrite.deny), + }; + } +} + +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..ac07a54 --- /dev/null +++ b/node_modules/discord.js/src/structures/Presence.js @@ -0,0 +1,336 @@ +'use strict'; + +const Emoji = require('./Emoji'); +const ActivityFlags = require('../util/ActivityFlags'); +const { ActivityTypes } = require('../util/Constants'); +const Util = require('../util/Util'); + +/** + * Activity sent in a message. + * @typedef {Object} MessageActivity + * @property {string} [partyID] Id of the party represented in activity + * @property {number} [type] Type of activity sent + */ + +/** + * 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 { + /** + * @param {Client} client The instantiating client + * @param {Object} [data={}] The data for the presence + */ + constructor(client, data = {}) { + /** + * The client that instantiated this + * @name Presence#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + /** + * The user ID of this presence + * @type {Snowflake} + */ + this.userID = data.user.id; + + /** + * The guild of this presence + * @type {?Guild} + */ + this.guild = data.guild || null; + + this.patch(data); + } + + /** + * The user of this presence + * @type {?User} + * @readonly + */ + get user() { + return this.client.users.cache.get(this.userID) || null; + } + + /** + * The member of this presence + * @type {?GuildMember} + * @readonly + */ + get member() { + return this.guild.members.cache.get(this.userID) || null; + } + + patch(data) { + /** + * The status of this presence + * @type {PresenceStatus} + */ + this.status = data.status || this.status || 'offline'; + + if (data.activities) { + /** + * The activities of this presence + * @type {Activity[]} + */ + this.activities = data.activities.map(activity => new Activity(this, activity)); + } else if (data.activity || data.game) { + this.activities = [new Activity(this, data.game || data.activity)]; + } 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; + + return this; + } + + _clone() { + const clone = Object.assign(Object.create(this), this); + if (this.activities) clone.activities = this.activities.map(activity => activity._clone()); + return clone; + } + + /** + * 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) + ); + } + + toJSON() { + return Util.flatten(this); + } +} + +/** + * Represents an activity that is part of a user's presence. + */ +class Activity { + constructor(presence, data) { + Object.defineProperty(this, 'presence', { value: presence }); + + /** + * The name of the activity being played + * @type {string} + */ + this.name = data.name; + + /** + * The type of the activity status + * @type {ActivityType} + */ + this.type = ActivityTypes[data.type]; + + /** + * If the activity 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; + + this.syncID = data.sync_id; + + /** + * Flags that describe the activity + * @type {Readonly<ActivityFlags>} + */ + this.flags = new ActivityFlags(data.flags).freeze(); + + /** + * Emoji for a custom activity + * @type {?Emoji} + */ + this.emoji = data.emoji ? new Emoji(presence.client, data.emoji) : null; + + /** + * Creation date of the activity + * @type {number} + */ + this.createdTimestamp = new Date(data.created_at).getTime(); + } + + /** + * Whether this activity is equal to another activity. + * @param {Activity} activity The activity to compare with + * @returns {boolean} + */ + equals(activity) { + return ( + this === activity || + (activity && this.name === activity.name && this.type === activity.type && this.url === activity.url) + ); + } + + /** + * The time the activity was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * When concatenated with a string, this automatically returns the activities' name instead of the Activity object. + * @returns {string} + */ + toString() { + return this.name; + } + + _clone() { + return Object.assign(Object.create(this), this); + } +} + +/** + * Assets for a rich presence + */ +class RichPresenceAssets { + constructor(activity, assets) { + Object.defineProperty(this, 'activity', { value: activity }); + + /** + * 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; + } + + /** + * Gets the URL of the small image asset + * @param {Object} [options] Options for the image url + * @param {string} [options.format] Format of the image + * @param {number} [options.size] Size of the image + * @returns {?string} The small image URL + */ + smallImageURL({ format, size } = {}) { + if (!this.smallImage) return null; + return this.activity.presence.client.rest.cdn.AppAsset(this.activity.applicationID, this.smallImage, { + format, + size, + }); + } + + /** + * Gets the URL of the large image asset + * @param {Object} [options] Options for the image url + * @param {string} [options.format] Format of the image + * @param {number} [options.size] Size of the image + * @returns {?string} The large image URL + */ + largeImageURL({ format, size } = {}) { + 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 this.activity.presence.client.rest.cdn.AppAsset(this.activity.applicationID, this.largeImage, { + format, + size, + }); + } +} + +exports.Presence = Presence; +exports.Activity = Activity; +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..6da9d17 --- /dev/null +++ b/node_modules/discord.js/src/structures/ReactionCollector.js @@ -0,0 +1,190 @@ +'use strict'; + +const Collector = require('./interfaces/Collector'); +const Collection = require('../util/Collection'); +const { Events } = require('../util/Constants'); + +/** + * @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. + * Will automatically stop if the message (`'messageDelete'`), + * channel (`'channelDelete'`), or guild (`'guildDelete'`) are deleted. + * @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 upon which to collect reactions + * @type {Message} + */ + this.message = message; + + /** + * The users which have reacted to this message + * @type {Collection} + */ + this.users = new Collection(); + + /** + * The total number of reactions collected + * @type {number} + */ + this.total = 0; + + this.empty = this.empty.bind(this); + this._handleChannelDeletion = this._handleChannelDeletion.bind(this); + this._handleGuildDeletion = this._handleGuildDeletion.bind(this); + this._handleMessageDeletion = this._handleMessageDeletion.bind(this); + + if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() + 1); + this.client.on(Events.MESSAGE_REACTION_ADD, this.handleCollect); + this.client.on(Events.MESSAGE_REACTION_REMOVE, this.handleDispose); + this.client.on(Events.MESSAGE_REACTION_REMOVE_ALL, this.empty); + this.client.on(Events.MESSAGE_DELETE, this._handleMessageDeletion); + this.client.on(Events.CHANNEL_DELETE, this._handleChannelDeletion); + this.client.on(Events.GUILD_DELETE, this._handleGuildDeletion); + + this.once('end', () => { + this.client.removeListener(Events.MESSAGE_REACTION_ADD, this.handleCollect); + this.client.removeListener(Events.MESSAGE_REACTION_REMOVE, this.handleDispose); + this.client.removeListener(Events.MESSAGE_REACTION_REMOVE_ALL, this.empty); + this.client.removeListener(Events.MESSAGE_DELETE, this._handleMessageDeletion); + this.client.removeListener(Events.CHANNEL_DELETE, this._handleChannelDeletion); + this.client.removeListener(Events.GUILD_DELETE, this._handleGuildDeletion); + if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() - 1); + }); + + this.on('collect', (reaction, user) => { + this.total++; + this.users.set(user.id, user); + }); + + this.on('remove', (reaction, user) => { + this.total--; + if (!this.collected.some(r => r.users.cache.has(user.id))) this.users.delete(user.id); + }); + } + + /** + * Handles an incoming reaction for possible collection. + * @param {MessageReaction} reaction The reaction to possibly collect + * @returns {?Snowflake|string} + * @private + */ + collect(reaction) { + /** + * Emitted whenever a reaction is collected. + * @event ReactionCollector#collect + * @param {MessageReaction} reaction The reaction that was collected + * @param {User} user The user that added the reaction + */ + if (reaction.message.id !== this.message.id) return null; + return ReactionCollector.key(reaction); + } + + /** + * Handles a reaction deletion for possible disposal. + * @param {MessageReaction} reaction The reaction to possibly dispose of + * @param {User} user The user that removed the reaction + * @returns {?Snowflake|string} + */ + dispose(reaction, user) { + /** + * Emitted whenever a reaction is disposed of. + * @event ReactionCollector#dispose + * @param {MessageReaction} reaction The reaction that was disposed of + * @param {User} user The user that removed the reaction + */ + if (reaction.message.id !== this.message.id) return null; + + /** + * Emitted whenever a reaction is removed from a message. Will emit on all reaction removals, + * as opposed to {@link Collector#dispose} which will only be emitted when the entire reaction + * is removed. + * @event ReactionCollector#remove + * @param {MessageReaction} reaction The reaction that was removed + * @param {User} user The user that removed the reaction + */ + if (this.collected.has(ReactionCollector.key(reaction)) && this.users.has(user.id)) { + this.emit('remove', reaction, user); + } + return reaction.count ? null : ReactionCollector.key(reaction); + } + + /** + * Empties this reaction collector. + */ + empty() { + this.total = 0; + this.collected.clear(); + this.users.clear(); + this.checkEnd(); + } + + endReason() { + 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; + } + + /** + * Handles checking if the message has been deleted, and if so, stops the collector with the reason 'messageDelete'. + * @private + * @param {Message} message The message that was deleted + * @returns {void} + */ + _handleMessageDeletion(message) { + if (message.id === this.message.id) { + this.stop('messageDelete'); + } + } + + /** + * Handles checking if the channel has been deleted, and if so, stops the collector with the reason 'channelDelete'. + * @private + * @param {GuildChannel} channel The channel that was deleted + * @returns {void} + */ + _handleChannelDeletion(channel) { + if (channel.id === this.message.channel.id) { + this.stop('channelDelete'); + } + } + + /** + * Handles checking if the guild has been deleted, and if so, stops the collector with the reason 'guildDelete'. + * @private + * @param {Guild} guild The guild that was deleted + * @returns {void} + */ + _handleGuildDeletion(guild) { + if (this.message.guild && guild.id === this.message.guild.id) { + this.stop('guildDelete'); + } + } + + /** + * Gets the collector key for a reaction. + * @param {MessageReaction} reaction The message reaction to get the key for + * @returns {Snowflake|string} + */ + static key(reaction) { + return reaction.emoji.id || reaction.emoji.name; + } +} + +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..5c4bc13 --- /dev/null +++ b/node_modules/discord.js/src/structures/ReactionEmoji.js @@ -0,0 +1,31 @@ +'use strict'; + +const Emoji = require('./Emoji'); +const Util = require('../util/Util'); + +/** + * Represents a limited emoji set used for both custom and unicode emojis. Custom emojis + * will use this class opposed to the Emoji class when the client doesn't know enough + * information about them. + * @extends {Emoji} + */ +class ReactionEmoji extends Emoji { + constructor(reaction, emoji) { + super(reaction.message.client, emoji); + /** + * The message reaction this emoji refers to + * @type {MessageReaction} + */ + this.reaction = reaction; + } + + toJSON() { + return Util.flatten(this, { identifier: true }); + } + + valueOf() { + return this.id; + } +} + +module.exports = ReactionEmoji; 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..bba198e --- /dev/null +++ b/node_modules/discord.js/src/structures/Role.js @@ -0,0 +1,403 @@ +'use strict'; + +const Base = require('./Base'); +const { Error, TypeError } = require('../errors'); +const Permissions = require('../util/Permissions'); +const Snowflake = require('../util/Snowflake'); +const Util = require('../util/Util'); + +/** + * Represents a role on Discord. + * @extends {Base} + */ +class Role extends Base { + /** + * @param {Client} client The instantiating client + * @param {Object} data The data for the role + * @param {Guild} guild The guild the role is part of + */ + constructor(client, data, guild) { + super(client); + + /** + * The guild that the role belongs to + * @type {Guild} + */ + this.guild = guild; + + if (data) this._patch(data); + } + + _patch(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 raw position of the role from the API + * @type {number} + */ + this.rawPosition = data.position; + + /** + * The permissions of the role + * @type {Readonly<Permissions>} + */ + this.permissions = new Permissions(data.permissions).freeze(); + + /** + * 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; + + /** + * Whether the role has been deleted + * @type {boolean} + */ + this.deleted = false; + } + + /** + * The timestamp the role was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * The time the role was created at + * @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() { + return `#${this.color.toString(16).padStart(6, '0')}`; + } + + /** + * The cached guild members that have this role + * @type {Collection<Snowflake, GuildMember>} + * @readonly + */ + get members() { + return this.guild.members.cache.filter(m => m.roles.cache.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)) return false; + return clientMember.roles.highest.comparePositionTo(this) > 0; + } + + /** + * The position of the role in the role manager + * @type {number} + * @readonly + */ + get position() { + const sorted = this.guild._sortedRoles(); + return sorted.array().indexOf(sorted.get(this.id)); + } + + /** + * Compares this role's position to another role's. + * @param {RoleResolvable} 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) { + role = this.guild.roles.resolve(role); + if (!role) throw new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake'); + 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} [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] Reason for editing this role + * @returns {Promise<Role>} + * @example + * // Edit a role + * role.edit({ name: 'new role' }) + * .then(updated => console.log(`Edited role ${updated.name} name to ${updated.name}`)) + * .catch(console.error); + */ + async edit(data, reason) { + if (typeof data.permissions !== 'undefined') data.permissions = Permissions.resolve(data.permissions); + else data.permissions = this.permissions.bitfield; + if (typeof data.position !== 'undefined') { + await Util.setPosition( + this, + data.position, + false, + this.guild._sortedRoles(), + this.client.api.guilds(this.guild.id).roles, + reason, + ).then(updatedRoles => { + this.client.actions.GuildRolesPositionUpdate.handle({ + guild_id: this.guild.id, + roles: updatedRoles, + }); + }); + } + return this.client.api.guilds[this.guild.id].roles[this.id] + .patch({ + data: { + name: data.name || this.name, + color: data.color !== null ? Util.resolveColor(data.color || this.color) : null, + hoist: typeof data.hoist !== 'undefined' ? data.hoist : this.hoist, + permissions: data.permissions, + mentionable: typeof data.mentionable !== 'undefined' ? data.mentionable : this.mentionable, + }, + reason, + }) + .then(role => { + const clone = this._clone(); + clone._patch(role); + return clone; + }); + } + + /** + * Returns `channel.permissionsFor(role)`. Returns permissions for a role in a guild channel, + * taking into account permission overwrites. + * @param {ChannelResolvable} channel The guild channel to use as context + * @returns {Readonly<Permissions>} + */ + permissionsIn(channel) { + channel = this.guild.channels.resolve(channel); + if (!channel) throw new Error('GUILD_CHANNEL_RESOLVE'); + return channel.rolePermissions(this); + } + + /** + * Sets a new name for the role. + * @param {string} name The new name of the role + * @param {string} [reason] Reason for changing the role's name + * @returns {Promise<Role>} + * @example + * // Set the name of the role + * role.setName('new role') + * .then(updated => console.log(`Edited name of role ${role.name} to ${updated.name}`)) + * .catch(console.error); + */ + setName(name, reason) { + return this.edit({ name }, reason); + } + + /** + * Sets 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 ${updated.color}`)) + * .catch(console.error); + */ + setColor(color, reason) { + return this.edit({ color }, reason); + } + + /** + * Sets 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(r => console.log(`Role hoisted: ${r.hoist}`)) + * .catch(console.error); + */ + setHoist(hoist, reason) { + return this.edit({ hoist }, reason); + } + + /** + * Sets 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); + } + + /** + * Sets 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) + * .then(updated => console.log(`Role updated ${updated.name}`)) + * .catch(console.error); + */ + setMentionable(mentionable, reason) { + return this.edit({ mentionable }, reason); + } + + /** + * Sets the position of the role. + * @param {number} position The position of the role + * @param {Object} [options] Options for setting position + * @param {boolean} [options.relative=false] Change the position relative to its current value + * @param {string} [options.reason] Reason for changing the position + * @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, reason } = {}) { + return Util.setPosition( + this, + position, + relative, + this.guild._sortedRoles(), + this.client.api.guilds(this.guild.id).roles, + reason, + ).then(updatedRoles => { + this.client.actions.GuildRolesPositionUpdate.handle({ + guild_id: this.guild.id, + roles: updatedRoles, + }); + return this; + }); + } + + /** + * Deletes the role. + * @param {string} [reason] Reason for deleting this 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.api.guilds[this.guild.id].roles[this.id].delete({ reason }).then(() => { + this.client.actions.GuildRoleDelete.handle({ guild_id: this.guild.id, role_id: this.id }); + return this; + }); + } + + /** + * Whether this role equals another role. It compares all properties, so for most operations + * it is advisable to just compare `role.id === role2.id` as it is much faster and is often + * what most users need. + * @param {Role} role Role to compare with + * @returns {boolean} + */ + equals(role) { + return ( + role && + this.id === role.id && + this.name === role.name && + this.color === role.color && + this.hoist === role.hoist && + this.position === role.position && + this.permissions.bitfield === role.permissions.bitfield && + this.managed === role.managed + ); + } + + /** + * When concatenated with a string, this automatically returns the role's mention instead of the Role object. + * @returns {string} + * @example + * // Logs: Role: <@&123456789012345678> + * console.log(`Role: ${role}`); + */ + toString() { + if (this.id === this.guild.id) return '@everyone'; + return `<@&${this.id}>`; + } + + toJSON() { + return super.toJSON({ createdTimestamp: true }); + } + + /** + * Compares the positions of two roles. + * @param {Role} role1 First role to compare + * @param {Role} role2 Second role to compare + * @returns {number} Negative number if the first role's position is lower (second role's is higher), + * positive number if the first's is higher (second's is lower), 0 if equal + */ + static comparePositions(role1, role2) { + if (role1.position === role2.position) return role2.id - role1.id; + return role1.position - role2.position; + } +} + +module.exports = Role; diff --git a/node_modules/discord.js/src/structures/StoreChannel.js b/node_modules/discord.js/src/structures/StoreChannel.js new file mode 100644 index 0000000..87cb040 --- /dev/null +++ b/node_modules/discord.js/src/structures/StoreChannel.js @@ -0,0 +1,22 @@ +'use strict'; + +const GuildChannel = require('./GuildChannel'); + +/** + * Represents a guild store channel on Discord. + * @extends {GuildChannel} + */ +class StoreChannel extends GuildChannel { + _patch(data) { + super._patch(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..a28c5a2 --- /dev/null +++ b/node_modules/discord.js/src/structures/Team.js @@ -0,0 +1,109 @@ +'use strict'; + +const Base = require('./Base'); +const TeamMember = require('./TeamMember'); +const Collection = require('../util/Collection'); +const Snowflake = require('../util/Snowflake'); + +/** + * Represents a Client OAuth2 Application Team. + * @extends {Base} + */ +class Team extends Base { + constructor(client, data) { + super(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, memberData); + this.members.set(member.id, member); + } + } + + /** + * The owner of this 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. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} URL to the icon + */ + iconURL({ format, size } = {}) { + if (!this.icon) return null; + return this.client.rest.cdn.TeamIcon(this.id, this.icon, { format, size }); + } + + /** + * 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; + } + + toJSON() { + return super.toJSON({ createdTimestamp: true }); + } +} + +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..ba7ecd2 --- /dev/null +++ b/node_modules/discord.js/src/structures/TeamMember.js @@ -0,0 +1,65 @@ +'use strict'; + +const Base = require('./Base'); +const { MembershipStates } = require('../util/Constants'); + +/** + * Represents a Client OAuth2 Application Team Member. + * @extends {Base} + */ +class TeamMember extends Base { + constructor(team, data) { + super(team.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 permissions 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.users.add(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: <@123456789012345678> + * 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..65ec13d --- /dev/null +++ b/node_modules/discord.js/src/structures/TextChannel.js @@ -0,0 +1,151 @@ +'use strict'; + +const GuildChannel = require('./GuildChannel'); +const Webhook = require('./Webhook'); +const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const MessageManager = require('../managers/MessageManager'); +const Collection = require('../util/Collection'); +const DataResolver = require('../util/DataResolver'); + +/** + * Represents a guild text channel on Discord. + * @extends {GuildChannel} + * @implements {TextBasedChannel} + */ +class TextChannel extends GuildChannel { + /** + * @param {Guild} guild The guild the text channel is part of + * @param {Object} data The data for the text channel + */ + constructor(guild, data) { + super(guild, data); + /** + * A manager of the messages sent to this channel + * @type {MessageManager} + */ + this.messages = new MessageManager(this); + this._typing = new Map(); + } + + _patch(data) { + super._patch(data); + + /** + * The topic of the text channel + * @type {?string} + */ + this.topic = data.topic; + + /** + * If the guild considers this channel NSFW + * @type {boolean} + * @readonly + */ + this.nsfw = 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 ratelimit per user for this channel in seconds + * @type {number} + */ + this.rateLimitPerUser = data.rate_limit_per_user || 0; + + /** + * 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; + + if (data.messages) for (const message of data.messages) this.messages.add(message); + } + + /** + * 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); + } + + /** + * 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); + } + + /** + * Fetches 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.api.channels[this.id].webhooks.get().then(data => { + const hooks = new Collection(); + for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook)); + return hooks; + }); + } + + /** + * Creates a webhook for the channel. + * @param {string} name The name of the webhook + * @param {Object} [options] Options for creating the webhook + * @param {BufferResolvable|Base64Resolvable} [options.avatar] Avatar for the webhook + * @param {string} [options.reason] Reason for creating the webhook + * @returns {Promise<Webhook>} webhook The created webhook + * @example + * // Create a webhook for the current channel + * channel.createWebhook('Snek', { + * avatar: 'https://i.imgur.com/mI8XcpG.jpg', + * reason: 'Needed a cool new Webhook' + * }) + * .then(console.log) + * .catch(console.error) + */ + async createWebhook(name, { avatar, reason } = {}) { + if (typeof avatar === 'string' && !avatar.startsWith('data:')) { + avatar = await DataResolver.resolveImage(avatar); + } + return this.client.api.channels[this.id].webhooks + .post({ + data: { + name, + avatar, + }, + reason, + }) + .then(data => new Webhook(this.client, data)); + } + + // These are here only for documentation purposes - they are implemented by TextBasedChannel + /* eslint-disable no-empty-function */ + get lastMessage() {} + get lastPinAt() {} + send() {} + startTyping() {} + stopTyping() {} + get typing() {} + get typingCount() {} + createMessageCollector() {} + awaitMessages() {} + bulkDelete() {} +} + +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..b2a6500 --- /dev/null +++ b/node_modules/discord.js/src/structures/User.js @@ -0,0 +1,318 @@ +'use strict'; + +const Base = require('./Base'); +const { Presence } = require('./Presence'); +const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const { Error } = require('../errors'); +const Snowflake = require('../util/Snowflake'); +const UserFlags = require('../util/UserFlags'); + +/** + * Represents a user on Discord. + * @implements {TextBasedChannel} + * @extends {Base} + */ +class User extends Base { + /** + * @param {Client} client The instantiating client + * @param {Object} data The data for the user + */ + constructor(client, data) { + super(client); + + /** + * The ID of the user + * @type {Snowflake} + */ + this.id = data.id; + + /** + * Whether or not the user is a bot + * @type {boolean} + * @name User#bot + */ + this.bot = Boolean(data.bot); + + this._patch(data); + } + + _patch(data) { + /** + * The username of the user + * @type {?string} + * @name User#username + */ + if (data.username) this.username = data.username; + + /** + * A discriminator based on username for the user + * @type {?string} + * @name User#discriminator + */ + if (data.discriminator) this.discriminator = data.discriminator; + + /** + * The ID of the user's avatar + * @type {?string} + * @name User#avatar + */ + if (typeof data.avatar !== 'undefined') this.avatar = data.avatar; + + if (typeof data.bot !== 'undefined') this.bot = Boolean(data.bot); + + /** + * Whether the user 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 locale of the user's client (ISO 639-1) + * @type {?string} + * @name User#locale + */ + if (data.locale) this.locale = data.locale; + + /** + * The flags for this user + * @type {?UserFlags} + */ + if (typeof data.public_flags !== 'undefined') this.flags = new UserFlags(data.public_flags); + + /** + * The ID of the last message sent by the user, if one was sent + * @type {?Snowflake} + */ + this.lastMessageID = null; + + /** + * The ID of the channel for the last message sent by the user, if one was sent + * @type {?Snowflake} + */ + this.lastMessageChannelID = null; + } + + /** + * Whether this User is a partial + * @type {boolean} + * @readonly + */ + get partial() { + return typeof this.username !== 'string'; + } + + /** + * The timestamp the user was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * The time the user was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The Message object of the last message sent by the user, if one was sent + * @type {?Message} + * @readonly + */ + get lastMessage() { + const channel = this.client.channels.cache.get(this.lastMessageChannelID); + return (channel && channel.messages.cache.get(this.lastMessageID)) || null; + } + + /** + * The presence of this user + * @type {Presence} + * @readonly + */ + get presence() { + for (const guild of this.client.guilds.cache.values()) { + if (guild.presences.cache.has(this.id)) return guild.presences.cache.get(this.id); + } + return new Presence(this.client, { user: { id: this.id } }); + } + + /** + * A link to the user's avatar. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} + */ + avatarURL({ format, size, dynamic } = {}) { + if (!this.avatar) return null; + return this.client.rest.cdn.Avatar(this.id, this.avatar, format, size, dynamic); + } + + /** + * A link to the user's default avatar + * @type {string} + * @readonly + */ + get defaultAvatarURL() { + return this.client.rest.cdn.DefaultAvatar(this.discriminator % 5); + } + + /** + * A link to the user's avatar if they have one. + * Otherwise a link to their default avatar will be returned. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {string} + */ + displayAvatarURL(options) { + return this.avatarURL(options) || this.defaultAvatarURL; + } + + /** + * The Discord "tag" (e.g. `hydrabolt#0001`) for this user + * @type {?string} + * @readonly + */ + get tag() { + return typeof this.username === 'string' ? `${this.username}#${this.discriminator}` : null; + } + + /** + * Checks whether the user is typing in a channel. + * @param {ChannelResolvable} channel The channel to check in + * @returns {boolean} + */ + typingIn(channel) { + channel = this.client.channels.resolve(channel); + return channel._typing.has(this.id); + } + + /** + * Gets the time that the user started typing. + * @param {ChannelResolvable} channel The channel to get the time in + * @returns {?Date} + */ + typingSinceIn(channel) { + channel = this.client.channels.resolve(channel); + return channel._typing.has(this.id) ? new Date(channel._typing.get(this.id).since) : null; + } + + /** + * Gets 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.channels.resolve(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.cache.find(c => c.type === 'dm' && c.recipient.id === this.id) || null; + } + + /** + * Creates a DM channel between the client and the user. + * @returns {Promise<DMChannel>} + */ + async createDM() { + const { dmChannel } = this; + if (dmChannel && !dmChannel.partial) return dmChannel; + const data = await this.client.api.users(this.client.user.id).channels.post({ + data: { + recipient_id: this.id, + }, + }); + return this.client.actions.ChannelCreate.handle(data).channel; + } + + /** + * Deletes a DM channel (if one exists) between the client and the user. Resolves with the channel if successful. + * @returns {Promise<DMChannel>} + */ + async deleteDM() { + const { dmChannel } = this; + if (!dmChannel) throw new Error('USER_NO_DMCHANNEL'); + const data = await this.client.api.channels(dmChannel.id).delete(); + return this.client.actions.ChannelDelete.handle(data).channel; + } + + /** + * 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; + + return equal; + } + + /** + * Fetches this user's flags. + * @returns {Promise<UserFlags>} + */ + async fetchFlags() { + if (this.flags) return this.flags; + const data = await this.client.api.users(this.id).get(); + this._patch(data); + return this.flags; + } + + /** + * Fetches this user. + * @returns {Promise<User>} + */ + fetch() { + return this.client.users.fetch(this.id, true); + } + + /** + * When concatenated with a string, this automatically returns the user's mention instead of the User object. + * @returns {string} + * @example + * // Logs: Hello from <@123456789012345678>! + * console.log(`Hello from ${user}!`); + */ + toString() { + return `<@${this.id}>`; + } + + toJSON(...props) { + const json = super.toJSON( + { + createdTimestamp: true, + defaultAvatarURL: true, + tag: true, + lastMessage: false, + lastMessageID: false, + }, + ...props, + ); + json.avatarURL = this.avatarURL(); + json.displayAvatarURL = this.displayAvatarURL(); + return json; + } + + // These are here only for documentation purposes - they are implemented by TextBasedChannel + /* eslint-disable no-empty-function */ + send() {} +} + +TextBasedChannel.applyToClass(User); + +module.exports = User; 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..6fb5ab7 --- /dev/null +++ b/node_modules/discord.js/src/structures/VoiceChannel.js @@ -0,0 +1,151 @@ +'use strict'; + +const GuildChannel = require('./GuildChannel'); +const { Error } = require('../errors'); +const Collection = require('../util/Collection'); +const { browser } = require('../util/Constants'); +const Permissions = require('../util/Permissions'); + +/** + * Represents a guild voice channel on Discord. + * @extends {GuildChannel} + */ +class VoiceChannel extends GuildChannel { + _patch(data) { + super._patch(data); + /** + * The bitrate of this voice channel + * @type {number} + */ + this.bitrate = data.bitrate; + + /** + * The maximum amount of users allowed in this channel - 0 means unlimited. + * @type {number} + */ + this.userLimit = data.user_limit; + } + + /** + * The members in this voice channel + * @type {Collection<Snowflake, GuildMember>} + * @name VoiceChannel#members + * @readonly + */ + get members() { + const coll = new Collection(); + for (const state of this.guild.voiceStates.cache.values()) { + if (state.channelID === this.id && state.member) { + coll.set(state.id, state.member); + } + } + return coll; + } + + /** + * 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, false); + } + + /** + * Whether the channel is editable by the client user + * @type {boolean} + * @readonly + */ + get editable() { + return this.manageable && this.permissionsFor(this.client.user).has(Permissions.FLAGS.CONNECT, false); + } + + /** + * Whether the channel is joinable by the client user + * @type {boolean} + * @readonly + */ + get joinable() { + if (browser) return false; + if (!this.viewable) return false; + if (!this.permissionsFor(this.client.user).has(Permissions.FLAGS.CONNECT, false)) return false; + if (this.full && !this.permissionsFor(this.client.user).has(Permissions.FLAGS.MOVE_MEMBERS, false)) 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(Permissions.FLAGS.SPEAK, false); + } + + /** + * Sets the bitrate of the channel. + * @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(48000) + * .then(vc => console.log(`Set bitrate to ${vc.bitrate}bps for ${vc.name}`)) + * .catch(console.error); + */ + setBitrate(bitrate, reason) { + 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 (browser) return Promise.reject(new Error('VOICE_NO_BROWSER')); + return this.client.voice.joinChannel(this); + } + + /** + * Leaves this voice channel. + * @example + * // Leave a voice channel + * voiceChannel.leave(); + */ + leave() { + if (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..9626195 --- /dev/null +++ b/node_modules/discord.js/src/structures/VoiceRegion.js @@ -0,0 +1,52 @@ +'use strict'; + +const Util = require('../util/Util'); + +/** + * 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; + } + + toJSON() { + return Util.flatten(this); + } +} + +module.exports = VoiceRegion; diff --git a/node_modules/discord.js/src/structures/VoiceState.js b/node_modules/discord.js/src/structures/VoiceState.js new file mode 100644 index 0000000..cc9bcde --- /dev/null +++ b/node_modules/discord.js/src/structures/VoiceState.js @@ -0,0 +1,208 @@ +'use strict'; + +const Base = require('./Base'); +const { Error, TypeError } = require('../errors'); +const { browser } = require('../util/Constants'); + +/** + * Represents the voice state for a Guild Member. + */ +class VoiceState extends Base { + /** + * @param {Guild} guild The guild the voice state is part of + * @param {Object} data The data for the voice state + */ + constructor(guild, data) { + super(guild.client); + /** + * The guild of this voice state + * @type {Guild} + */ + this.guild = guild; + /** + * The ID of the member of this voice state + * @type {Snowflake} + */ + this.id = data.user_id; + this._patch(data); + } + + _patch(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-deafened + * @type {?boolean} + */ + this.selfDeaf = data.self_deaf; + /** + * Whether this member is self-muted + * @type {?boolean} + */ + this.selfMute = data.self_mute; + /** + * The session ID of this member's connection + * @type {?string} + */ + this.sessionID = data.session_id; + /** + * Whether this member is streaming using "Go Live" + * @type {boolean} + */ + this.streaming = data.self_stream || false; + /** + * The ID of the voice channel that this member is in + * @type {?Snowflake} + */ + this.channelID = data.channel_id; + return this; + } + + /** + * The member that this voice state belongs to + * @type {?GuildMember} + * @readonly + */ + get member() { + return this.guild.members.cache.get(this.id) || null; + } + + /** + * The channel that the member is connected to + * @type {?VoiceChannel} + * @readonly + */ + get channel() { + return this.guild.channels.cache.get(this.channelID) || null; + } + + /** + * If this is a voice state of the client user, then this will refer to the active VoiceConnection for this guild + * @type {?VoiceConnection} + * @readonly + */ + get connection() { + if (browser || this.id !== this.client.user.id) return null; + return this.client.voice.connections.get(this.guild.id) || null; + } + + /** + * Whether this member is either self-deafened or server-deafened + * @type {?boolean} + * @readonly + */ + get deaf() { + return this.serverDeaf || this.selfDeaf; + } + + /** + * Whether this member is either self-muted or server-muted + * @type {?boolean} + * @readonly + */ + get mute() { + return this.serverMute || this.selfMute; + } + + /** + * Whether this member is currently speaking. A boolean if the information is available (aka + * the bot is connected to any voice channel in the guild), otherwise this is null + * @type {?boolean} + * @readonly + */ + get speaking() { + return this.channel && this.channel.connection ? Boolean(this.channel.connection._speaking.get(this.id)) : null; + } + + /** + * Mutes/unmutes the member of this voice state. + * @param {boolean} mute Whether or not the member should be muted + * @param {string} [reason] Reason for muting or unmuting + * @returns {Promise<GuildMember>} + */ + setMute(mute, reason) { + return this.member ? this.member.edit({ mute }, reason) : Promise.reject(new Error('VOICE_STATE_UNCACHED_MEMBER')); + } + + /** + * Deafens/undeafens the member of this voice state. + * @param {boolean} deaf Whether or not the member should be deafened + * @param {string} [reason] Reason for deafening or undeafening + * @returns {Promise<GuildMember>} + */ + setDeaf(deaf, reason) { + return this.member ? this.member.edit({ deaf }, reason) : Promise.reject(new Error('VOICE_STATE_UNCACHED_MEMBER')); + } + + /** + * Kicks the member from the voice channel. + * @param {string} [reason] Reason for kicking member from the channel + * @returns {Promise<GuildMember>} + */ + kick(reason) { + return this.setChannel(null, reason); + } + + /** + * Moves the member to a different channel, or disconnects them from the one they're in. + * @param {ChannelResolvable|null} [channel] Channel to move the member to, or `null` if you want to disconnect them + * from voice. Requires the `MOVE_MEMBERS` permission. + * @param {string} [reason] Reason for moving member to another channel or disconnecting + * @returns {Promise<GuildMember>} + */ + setChannel(channel, reason) { + return this.member + ? this.member.edit({ channel }, reason) + : Promise.reject(new Error('VOICE_STATE_UNCACHED_MEMBER')); + } + + /** + * Self-mutes/unmutes the bot for this voice state. + * @param {boolean} mute Whether or not the bot should be self-muted + * @returns {Promise<boolean>} true if the voice state was successfully updated, otherwise false + */ + async setSelfMute(mute) { + if (this.id !== this.client.user.id) throw new Error('VOICE_STATE_NOT_OWN'); + if (typeof mute !== 'boolean') throw new TypeError('VOICE_STATE_INVALID_TYPE', 'mute'); + if (!this.connection) return false; + this.selfMute = mute; + await this.connection.sendVoiceStateUpdate(); + return true; + } + + /** + * Self-deafens/undeafens the bot for this voice state. + * @param {boolean} deaf Whether or not the bot should be self-deafened + * @returns {Promise<boolean>} true if the voice state was successfully updated, otherwise false + */ + async setSelfDeaf(deaf) { + if (this.id !== this.client.user.id) return new Error('VOICE_STATE_NOT_OWN'); + if (typeof deaf !== 'boolean') return new TypeError('VOICE_STATE_INVALID_TYPE', 'deaf'); + if (!this.connection) return false; + this.selfDeaf = deaf; + await this.connection.sendVoiceStateUpdate(); + return true; + } + + toJSON() { + return super.toJSON({ + id: true, + serverDeaf: true, + serverMute: true, + selfDeaf: true, + selfMute: true, + sessionID: true, + channelID: 'channel', + }); + } +} + +module.exports = VoiceState; 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..b9aed9d --- /dev/null +++ b/node_modules/discord.js/src/structures/Webhook.js @@ -0,0 +1,273 @@ +'use strict'; + +const APIMessage = require('./APIMessage'); +const Channel = require('./Channel'); +const { WebhookTypes } = require('../util/Constants'); +const DataResolver = require('../util/DataResolver'); +const Snowflake = require('../util/Snowflake'); + +/** + * Represents a webhook. + */ +class Webhook { + constructor(client, data) { + /** + * The client that instantiated the webhook + * @name Webhook#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + if (data) this._patch(data); + } + + _patch(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 = 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.cache.get(data.user.id) : data.user; + } else { + this.owner = null; + } + } + + /** + * Options that can be passed into send. + * @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 {Object[]} [embeds] An array of embeds for the message + * @property {MessageMentionOptions} [allowedMentions] Which mentions should be parsed from the message content + * (see [here](https://discordapp.com/developers/docs/resources/channel#embed-object) for more details) + * @property {DisableMentionType} [disableMentions=this.client.options.disableMentions] Whether or not all mentions or + * everyone/here mentions should be sanitized to prevent unexpected mentions + * @property {FileOptions[]|string[]} [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. + */ + + /** + * Sends a message with this webhook. + * @param {StringResolvable|APIMessage} [content=''] The content to send + * @param {WebhookMessageOptions|MessageAdditions} [options={}] The options to provide + * @returns {Promise<Message|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); + */ + async send(content, options) { + let apiMessage; + + if (content instanceof APIMessage) { + apiMessage = content.resolveData(); + } else { + apiMessage = APIMessage.create(this, content, options).resolveData(); + if (Array.isArray(apiMessage.data.content)) { + return Promise.all(apiMessage.split().map(this.send.bind(this))); + } + } + + const { data, files } = await apiMessage.resolveFiles(); + return this.client.api + .webhooks(this.id, this.token) + .post({ + data, + files, + query: { wait: true }, + auth: false, + }) + .then(d => { + const channel = this.client.channels ? this.client.channels.cache.get(d.channel_id) : undefined; + if (!channel) return d; + return channel.messages.add(d, false); + }); + } + + /** + * Sends a raw slack message with this webhook. + * @param {Object} body The raw body to send + * @returns {Promise<boolean>} + * @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.api + .webhooks(this.id, this.token) + .slack.post({ + query: { wait: true }, + auth: false, + data: body, + }) + .then(data => data.toString() === 'ok'); + } + + /** + * Edits the webhook. + * @param {Object} options Options + * @param {string} [options.name=this.name] New name for this webhook + * @param {BufferResolvable} [options.avatar] New avatar for this webhook + * @param {ChannelResolvable} [options.channel] New channel for this webhook + * @param {string} [reason] Reason for editing this webhook + * @returns {Promise<Webhook>} + */ + async edit({ name = this.name, avatar, channel }, reason) { + if (avatar && typeof avatar === 'string' && !avatar.startsWith('data:')) { + avatar = await DataResolver.resolveImage(avatar); + } + if (channel) channel = channel instanceof Channel ? channel.id : channel; + const data = await this.client.api.webhooks(this.id, channel ? undefined : this.token).patch({ + data: { name, avatar, channel_id: channel }, + reason, + }); + + this.name = data.name; + this.avatar = data.avatar; + this.channelID = data.channel_id; + return this; + } + + /** + * Deletes the webhook. + * @param {string} [reason] Reason for deleting this webhook + * @returns {Promise} + */ + delete(reason) { + return this.client.api.webhooks(this.id, this.token).delete({ reason }); + } + /** + * 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); + } + + /** + * The url of this webhook + * @type {string} + * @readonly + */ + get url() { + return this.client.options.http.api + this.client.api.webhooks(this.id, this.token); + } + + /** + * A link to the webhook's avatar. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} + */ + avatarURL({ format, size } = {}) { + if (!this.avatar) return null; + return this.client.rest.cdn.Avatar(this.id, this.avatar, format, size); + } + + static applyToClass(structure) { + for (const prop of ['send', 'sendSlackMessage', 'edit', 'delete', 'createdTimestamp', 'createdAt', 'url']) { + Object.defineProperty(structure.prototype, prop, Object.getOwnPropertyDescriptor(Webhook.prototype, prop)); + } + } +} + +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..21d01ca --- /dev/null +++ b/node_modules/discord.js/src/structures/interfaces/Collector.js @@ -0,0 +1,281 @@ +'use strict'; + +const EventEmitter = require('events'); +const Collection = require('../../util/Collection'); +const Util = require('../../util/Util'); + +/** + * 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 in milliseconds + * @property {number} [idle] How long to stop the collector after inactivity in milliseconds + * @property {boolean} [dispose=false] Whether to dispose data when it's deleted + */ + +/** + * Abstract class for defining a new Collector. + * @abstract + */ +class Collector extends EventEmitter { + constructor(client, filter, options = {}) { + super(); + + /** + * The client that instantiated this Collector + * @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; + + this.handleCollect = this.handleCollect.bind(this); + this.handleDispose = this.handleDispose.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); + } + + /** + * Call this to handle an event as a collectable element. Accepts any event data as parameters. + * @param {...*} args The arguments emitted by the listener + * @emits Collector#collect + */ + handleCollect(...args) { + const collect = this.collect(...args); + + if (collect && this.filter(...args, this.collected)) { + this.collected.set(collect, args[0]); + + /** + * Emitted whenever an element is collected. + * @event Collector#collect + * @param {...*} args The arguments emitted by the listener + */ + this.emit('collect', ...args); + + if (this._idletimeout) { + this.client.clearTimeout(this._idletimeout); + this._idletimeout = this.client.setTimeout(() => this.stop('idle'), this.options.idle); + } + } + this.checkEnd(); + } + + /** + * Call this to remove an element from the collection. Accepts any event data as parameters. + * @param {...*} args The arguments emitted by the listener + * @emits Collector#dispose + */ + handleDispose(...args) { + if (!this.options.dispose) return; + + const dispose = this.dispose(...args); + if (!dispose || !this.filter(...args) || !this.collected.has(dispose)) return; + this.collected.delete(dispose); + + /** + * Emitted whenever an element is disposed of. + * @event Collector#dispose + * @param {...*} args The arguments emitted by the listener + */ + this.emit('dispose', ...args); + this.checkEnd(); + } + + /** + * Returns 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); + }); + } + + /** + * Stops this collector and emits 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; + + /** + * 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); + } + + /** + * Resets the collectors timeout and idle timer. + * @param {Object} [options] Options + * @param {number} [options.time] How long to run the collector for in milliseconds + * @param {number} [options.idle] How long to stop the collector after inactivity in milliseconds + */ + resetTimer({ time, idle } = {}) { + if (this._timeout) { + this.client.clearTimeout(this._timeout); + this._timeout = this.client.setTimeout(() => this.stop('time'), time || this.options.time); + } + if (this._idletimeout) { + this.client.clearTimeout(this._idletimeout); + this._idletimeout = this.client.setTimeout(() => this.stop('idle'), idle || this.options.idle); + } + } + + /** + * Checks whether the collector should end, and if so, ends it. + */ + checkEnd() { + const reason = this.endReason(); + if (reason) this.stop(reason); + } + + /** + * Allows collectors to be consumed with for-await-of loops + * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of} + */ + async *[Symbol.asyncIterator]() { + const queue = []; + const onCollect = item => queue.push(item); + this.on('collect', onCollect); + + try { + while (queue.length || !this.ended) { + if (queue.length) { + yield queue.shift(); + } else { + // eslint-disable-next-line no-await-in-loop + await new Promise(resolve => { + const tick = () => { + this.removeListener('collect', tick); + this.removeListener('end', tick); + return resolve(); + }; + this.on('collect', tick); + this.on('end', tick); + }); + } + } + } finally { + this.removeListener('collect', onCollect); + } + } + + toJSON() { + return Util.flatten(this); + } + + /* eslint-disable no-empty-function, valid-jsdoc */ + /** + * Handles incoming events from the `handleCollect` function. Returns null if the event should not + * be collected, or returns an object describing the data that should be stored. + * @see Collector#handleCollect + * @param {...*} args Any args the event listener emits + * @returns {?{key, value}} Data to insert into collection, if any + * @abstract + */ + collect() {} + + /** + * Handles incoming events from the `handleDispose`. Returns null if the event should not + * be disposed, or returns the key that should be removed. + * @see Collector#handleDispose + * @param {...*} args Any args the event listener emits + * @returns {?*} Key to remove from the collection, if any + * @abstract + */ + dispose() {} + + /** + * The reason this collector has ended or will end with. + * @returns {?string} Reason to end the collector, if any + * @abstract + */ + endReason() {} + /* 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..b63c92a --- /dev/null +++ b/node_modules/discord.js/src/structures/interfaces/TextBasedChannel.js @@ -0,0 +1,396 @@ +'use strict'; + +/* eslint-disable import/order */ +const MessageCollector = require('../MessageCollector'); +const APIMessage = require('../APIMessage'); +const Snowflake = require('../../util/Snowflake'); +const Collection = require('../../util/Collection'); +const { RangeError, TypeError } = require('../../errors'); + +/** + * Interface for classes that have text-channel-like features. + * @interface + */ +class TextBasedChannel { + constructor() { + /** + * A manager of the messages sent to this channel + * @type {MessageManager} + */ + this.messages = new MessageManager(this); + + /** + * The ID of the last message in the channel, if one was sent + * @type {?Snowflake} + */ + this.lastMessageID = null; + + /** + * The timestamp when the last pinned message was pinned, if there was one + * @type {?number} + */ + this.lastPinTimestamp = null; + } + + /** + * The Message object of the last message in the channel, if one was sent + * @type {?Message} + * @readonly + */ + get lastMessage() { + return this.messages.cache.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; + } + + /** + * 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 {string} [content=''] The content for the message + * @property {MessageEmbed|Object} [embed] An embed for the message + * (see [here](https://discordapp.com/developers/docs/resources/channel#embed-object) for more details) + * @property {MessageMentionOptions} [allowedMentions] Which mentions should be parsed from the message content + * @property {DisableMentionType} [disableMentions=this.client.options.disableMentions] Whether or not all mentions or + * everyone/here mentions should be sanitized to prevent unexpected mentions + * @property {FileOptions[]|BufferResolvable[]} [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) + */ + + /** + * Options provided to control parsing of mentions by Discord + * @typedef {Object} MessageMentionOptions + * @property {MessageMentionTypes[]} [parse] Types of mentions to be parsed + * @property {Snowflake[]} [users] Snowflakes of Users to be parsed as mentions + * @property {Snowflake[]} [roles] Snowflakes of Roles to be parsed as mentions + */ + + /** + * Types of mentions to enable in MessageMentionOptions. + * - `roles` + * - `users` + * - `everyone` + * @typedef {string} MessageMentionTypes + */ + + /** + * The type of mentions to disable. + * - `none` + * - `all` + * - `everyone` + * @typedef {string} DisableMentionType + */ + + /** + * @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=2000] 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 + */ + + /** + * Sends a message to this channel. + * @param {StringResolvable|APIMessage} [content=''] The content to send + * @param {MessageOptions|MessageAdditions} [options={}] The options to provide + * @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); + */ + async send(content, options) { + const User = require('../User'); + const GuildMember = require('../GuildMember'); + + if (this instanceof User || this instanceof GuildMember) { + return this.createDM().then(dm => dm.send(content, options)); + } + + let apiMessage; + + if (content instanceof APIMessage) { + apiMessage = content.resolveData(); + } else { + apiMessage = APIMessage.create(this, content, options).resolveData(); + if (Array.isArray(apiMessage.data.content)) { + return Promise.all(apiMessage.split().map(this.send.bind(this))); + } + } + + const { data, files } = await apiMessage.resolveFiles(); + return this.client.api.channels[this.id].messages + .post({ data, files }) + .then(d => this.client.actions.MessageCreate.handle(d).message); + } + + /** + * Starts a typing indicator in the channel. + * @param {number} [count=1] The number of times startTyping should be considered to have been called + * @returns {Promise} Resolves once the bot stops typing gracefully, or rejects when an error occurs + * @example + * // Start typing in a channel, or increase the typing count by one + * channel.startTyping(); + * @example + * // Start typing in a channel with a typing count of five, or set it to five + * channel.startTyping(5); + */ + startTyping(count) { + if (typeof count !== 'undefined' && count < 1) throw new RangeError('TYPING_COUNT'); + if (this.client.user._typing.has(this.id)) { + const entry = this.client.user._typing.get(this.id); + entry.count = count || entry.count + 1; + return entry.promise; + } + + const entry = {}; + entry.promise = new Promise((resolve, reject) => { + const endpoint = this.client.api.channels[this.id].typing; + Object.assign(entry, { + count: count || 1, + interval: this.client.setInterval(() => { + endpoint.post().catch(error => { + this.client.clearInterval(entry.interval); + this.client.user._typing.delete(this.id); + reject(error); + }); + }, 9000), + resolve, + }); + endpoint.post().catch(error => { + this.client.clearInterval(entry.interval); + this.client.user._typing.delete(this.id); + reject(error); + }); + this.client.user._typing.set(this.id, entry); + }); + return entry.promise; + } + + /** + * 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 regardless of typing count + * 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); + entry.resolve(); + } + } + } + + /** + * Whether or not the typing indicator is being shown in the channel + * @type {boolean} + * @readonly + */ + get typing() { + return this.client.user._typing.has(this.id); + } + + /** + * Number of times `startTyping` has been called + * @type {number} + * @readonly + */ + get typingCount() { + if (this.client.user._typing.has(this.id)) return this.client.user._typing.get(this.id).count; + return 0; + } + + /** + * Creates a Message Collector. + * @param {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 createMessageCollector 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.createMessageCollector(filter, options); + collector.once('end', (collection, reason) => { + if (options.errors && options.errors.includes(reason)) { + reject(collection); + } else { + resolve(collection); + } + }); + }); + } + + /** + * Bulk deletes given messages that are newer than two weeks. + * @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); + */ + async bulkDelete(messages, filterOld = false) { + if (Array.isArray(messages) || 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 new Collection(); + if (messageIDs.length === 1) { + await this.client.api + .channels(this.id) + .messages(messageIDs[0]) + .delete(); + const message = this.client.actions.MessageDelete.getMessage( + { + message_id: messageIDs[0], + }, + this, + ); + return message ? new Collection([[message.id, message]]) : new Collection(); + } + await this.client.api.channels[this.id].messages['bulk-delete'].post({ data: { messages: messageIDs } }); + return messageIDs.reduce( + (col, id) => + col.set( + id, + this.client.actions.MessageDeleteBulk.getMessage( + { + message_id: id, + }, + this, + ), + ), + new Collection(), + ); + } + if (!isNaN(messages)) { + const msgs = await this.messages.fetch({ limit: messages }); + return this.bulkDelete(msgs, filterOld); + } + throw new TypeError('MESSAGE_BULK_DELETE_TYPE'); + } + + static applyToClass(structure, full = false, ignore = []) { + const props = ['send']; + if (full) { + props.push( + 'lastMessage', + 'lastPinAt', + 'bulkDelete', + 'startTyping', + 'stopTyping', + 'typing', + 'typingCount', + 'createMessageCollector', + 'awaitMessages', + ); + } + for (const prop of props) { + if (ignore.includes(prop)) continue; + Object.defineProperty( + structure.prototype, + prop, + Object.getOwnPropertyDescriptor(TextBasedChannel.prototype, prop), + ); + } + } +} + +module.exports = TextBasedChannel; + +// Fixes Circular +const MessageManager = require('../../managers/MessageManager'); |