SB-6.3-BETA 2

Added Moderation
Added Node Modules
This commit is contained in:
ItzEmerald committed 2021-03-27 16:22:19 +00:00
1 parent 60daab82d2
commit 09aad61cfe
385 files changed
+78314 -20

No files matched your search

+396
View File
@@ -0,0 +1,396 @@
'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);
}
if (typeof content !== 'string') return 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;
}
let allowedMentions =
typeof this.options.allowedMentions === 'undefined'
? this.target.client.options.allowedMentions
: this.options.allowedMentions;
if (this.options.reply) {
const id = this.target.client.users.resolveID(this.options.reply);
if (allowedMentions) {
// Clone the object as not to alter the ClientOptions object
allowedMentions = Util.cloneObject(allowedMentions);
const parsed = allowedMentions.parse && allowedMentions.parse.includes('users');
// Check if the mention won't be parsed, and isn't supplied in `users`
if (!parsed && !(allowedMentions.users && allowedMentions.users.includes(id))) {
if (!allowedMentions.users) allowedMentions.users = [];
allowedMentions.users.push(id);
}
} else {
allowedMentions = { users: [id] };
}
}
this.data = {
content,
tts,
nonce,
embed: this.options.embed === null ? null : embeds[0],
embeds,
username,
avatar_url: avatarURL,
allowed_mentions: typeof content === 'undefined' ? undefined : 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, allowed_mentions: this.options.allowedMentions };
opt = { content: this.data.content[i], tts: this.data.tts, allowedMentions: this.options.allowedMentions };
}
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
*/
+43
View File
@@ -0,0 +1,43 @@
'use strict';
const Util = require('../util/Util');
/**
* Represents a data model that is identifiable by a Snowflake (i.e. Discord API data models).
* @abstract
*/
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;
+66
View File
@@ -0,0 +1,66 @@
'use strict';
const Emoji = require('./Emoji');
/**
* Parent class for {@link GuildEmoji} and {@link GuildPreviewEmoji}.
* @extends {Emoji}
* @abstract
*/
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;
this.requireColons = null;
this.managed = null;
this.available = null;
/**
* 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;
if (typeof data.require_colons !== 'undefined') {
/**
* Whether or not this emoji requires colons surrounding it
* @type {?boolean}
*/
this.requiresColons = data.require_colons;
}
if (typeof data.managed !== 'undefined') {
/**
* Whether this emoji is managed by an external service
* @type {?boolean}
*/
this.managed = data.managed;
}
if (typeof data.available !== 'undefined') {
/**
* Whether this emoji is available
* @type {?boolean}
*/
this.available = data.available;
}
if (data.roles) this._roles = data.roles;
}
}
module.exports = BaseGuildEmoji;
+33
View File
@@ -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;
+161
View File
@@ -0,0 +1,161 @@
'use strict';
const Base = require('./Base');
const { ChannelTypes } = require('../util/Constants');
const Snowflake = require('../util/Snowflake');
/**
* Represents any channel on Discord.
* @extends {Base}
* @abstract
*/
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.
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<Channel>}
*/
fetch(force = false) {
return this.client.channels.fetch(this.id, true, force);
}
/**
* Indicates whether this channel is text-based.
* @returns {boolean}
*/
isText() {
return 'messages' in this;
}
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;
+46
View File
@@ -0,0 +1,46 @@
'use strict';
const Team = require('./Team');
const Application = require('./interfaces/Application');
/**
* Represents a Client OAuth2 Application.
* @extends {Application}
*/
class ClientApplication extends Application {
_patch(data) {
super._patch(data);
/**
* 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;
}
}
module.exports = ClientApplication;
+87
View File
@@ -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;
+178
View File
@@ -0,0 +1,178 @@
'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 {string} [activity.name] Name of the activity
* @property {ActivityType|number} [activity.type] Type of the activity
* @property {string} [activity.url] Twitch / YouTube 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 / YouTube 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;
+99
View File
@@ -0,0 +1,99 @@
'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.
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<DMChannel>}
*/
fetch(force = false) {
return this.recipient.createDM(force);
}
/**
* 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;
+104
View File
@@ -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;
+1583
View File
File diff suppressed because it is too large. Load diff
+509
View File
@@ -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 {Object<string, string>}
*/
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 {Object<string, number>}
*/
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;
+624
View File
@@ -0,0 +1,624 @@
'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}
* @abstract
*/
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 || null;
/**
* 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'));
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'));
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,
});
});
}
let permission_overwrites;
if (data.permissionOverwrites) {
permission_overwrites = data.permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild));
}
if (data.lockPermissions) {
if (data.parentID) {
const newParent = this.guild.channels.resolve(data.parentID);
if (newParent && newParent.type === 'category') {
permission_overwrites = newParent.permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild));
}
} else if (this.parent) {
permission_overwrites = this.parent.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=this.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;
+168
View File
@@ -0,0 +1,168 @@
'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 {
/**
* @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
*/
constructor(client, data, guild) {
super(client, data, guild);
/**
* The user who created this emoji
* @type {?User}
*/
this.author = null;
}
/**
* The guild this emoji is part of
* @type {Guild}
* @name GuildEmoji#guild
*/
_clone() {
const clone = super._clone();
clone._roles = this._roles.slice();
return clone;
}
_patch(data) {
super._patch(data);
if (typeof data.user !== 'undefined') this.author = this.client.users.add(data.user);
}
/**
* 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>}
*/
async fetchAuthor() {
if (this.managed) {
throw new Error('EMOJI_MANAGED');
} else {
if (!this.guild.me) throw new Error('GUILD_UNCACHED_ME');
if (!this.guild.me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS)) {
throw new Error('MISSING_MANAGE_EMOJIS_PERMISSION', this.guild);
}
}
const data = await this.client.api.guilds(this.guild.id).emojis(this.id).get();
this._patch(data);
return this.author;
}
/**
* 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;
+414
View File
@@ -0,0 +1,414 @@
'use strict';
const Base = require('./Base');
const Role = require('./Role');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const { Error } = require('../errors');
const GuildMemberRoleManager = require('../managers/GuildMemberRoleManager');
const Permissions = require('../util/Permissions');
let Structures;
/**
* 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 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;
/**
* The nickname of this member, if they have one
* @type {?string}
*/
this.nickname = null;
this._roles = [];
if (data) this._patch(data);
}
_patch(data) {
if ('user' in data) {
/**
* The user that this guild member instance represents
* @type {User}
*/
this.user = this.client.users.add(data.user, true);
}
if ('nick' in data) this.nickname = data.nick;
if ('joined_at' in data) this.joinedTimestamp = new Date(data.joined_at).getTime();
if ('premium_since' in data) this.premiumSinceTimestamp = new Date(data.premium_since).getTime();
if ('roles' in data) 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() {
if (!Structures) Structures = require('../util/Structures');
const VoiceState = Structures.get('VoiceState');
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() {
if (!Structures) Structures = require('../util/Structures');
const Presence = Structures.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;
const permissions = new Permissions(this.roles.cache.map(role => role.permissions));
return 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, must be between 0 and 7
* @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.
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<GuildMember>}
*/
fetch(force = false) {
return this.guild.members.fetch({ user: this.id, cache: true, force });
}
/**
* 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;
+157
View File
@@ -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 guild.
* @extends {Base}
*/
class GuildPreview extends Base {
constructor(client, data) {
super(client);
if (!data) return;
this._patch(data);
}
/**
* Builds the guild with the provided data.
* @param {*} data The raw data of the guild
* @private
*/
_patch(data) {
/**
* The id of this guild
* @type {string}
*/
this.id = data.id;
/**
* The name of this guild
* @type {string}
*/
this.name = data.name;
/**
* The icon of this guild
* @type {?string}
*/
this.icon = data.icon;
/**
* The splash icon of this guild
* @type {?string}
*/
this.splash = data.splash;
/**
* The discovery splash icon of this 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 guild
* @type {number}
*/
this.approximateMemberCount = data.approximate_member_count;
/**
* The approximate count of online members in this guild
* @type {number}
*/
this.approximatePresenceCount = data.approximate_presence_count;
/**
* The description for this guild
* @type {?string}
*/
this.description = data.description || null;
if (!this.emojis) {
/**
* Collection of emojis belonging to this 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 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 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 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 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;
+26
View File
@@ -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;
+225
View File
@@ -0,0 +1,225 @@
'use strict';
const Base = require('./Base');
const { Events } = require('../util/Constants');
const DataResolver = require('../util/DataResolver');
/**
* Represents the template for a guild.
* @extends {Base}
*/
class GuildTemplate extends Base {
/**
* @param {Client} client The instantiating client
* @param {Object} data The raw data for the template
*/
constructor(client, data) {
super(client);
this._patch(data);
}
/**
* Builds or updates the template with the provided data.
* @param {Object} data The raw data for the template
* @returns {GuildTemplate}
* @private
*/
_patch(data) {
/**
* The unique code of this template
* @type {string}
*/
this.code = data.code;
/**
* The name of this template
* @type {string}
*/
this.name = data.name;
/**
* The description of this template
* @type {?string}
*/
this.description = data.description;
/**
* The amount of times this template has been used
* @type {number}
*/
this.usageCount = data.usage_count;
/**
* The ID of the user that created this template
* @type {Snowflake}
*/
this.creatorID = data.creator_id;
/**
* The user that created this template
* @type {User}
*/
this.creator = this.client.users.add(data.creator);
/**
* The time of when this template was created at
* @type {Date}
*/
this.createdAt = new Date(data.created_at);
/**
* The time of when this template was last synced to the guild
* @type {Date}
*/
this.updatedAt = new Date(data.updated_at);
/**
* The ID of the guild that this template belongs to
* @type {Snowflake}
*/
this.guildID = data.source_guild_id;
/**
* The data of the guild that this template would create
* @type {Object}
* @see {@link https://discord.com/developers/docs/resources/guild#guild-resource}
*/
this.serializedGuild = data.serialized_source_guild;
/**
* Whether this template has unsynced changes
* @type {?boolean}
*/
this.unSynced = 'is_dirty' in data ? Boolean(data.is_dirty) : null;
return this;
}
/**
* Creates a guild based from this template.
* <warn>This is only available to bots in fewer than 10 guilds.</warn>
* @param {string} name The name of the guild
* @param {BufferResolvable|Base64Resolvable} [icon] The icon for the guild
* @returns {Promise<Guild>}
*/
async createGuild(name, icon) {
const { client } = this;
const data = await client.api.guilds.templates(this.code).post({
data: {
name,
icon: await DataResolver.resolveImage(icon),
},
});
// eslint-disable-next-line consistent-return
return new Promise(resolve => {
const createdGuild = client.guilds.cache.get(data.id);
if (createdGuild) return resolve(createdGuild);
const resolveGuild = guild => {
client.off(Events.GUILD_CREATE, handleGuild);
client.decrementMaxListeners();
resolve(guild);
};
const handleGuild = guild => {
if (guild.id === data.id) {
client.clearTimeout(timeout);
resolveGuild(guild);
}
};
client.incrementMaxListeners();
client.on(Events.GUILD_CREATE, handleGuild);
const timeout = client.setTimeout(() => resolveGuild(client.guilds.add(data)), 10000);
});
}
/**
* Updates the metadata on this template.
* @param {Object} options Options for the template
* @param {string} [options.name] The name of this template
* @param {string} [options.description] The description of this template
* @returns {Promise<GuildTemplate>}
*/
edit({ name, description } = {}) {
return this.client.api
.guilds(this.guildID)
.templates(this.code)
.patch({ data: { name, description } })
.then(data => this._patch(data));
}
/**
* Deletes this template.
* @returns {Promise<GuildTemplate>}
*/
delete() {
return this.client.api
.guilds(this.guildID)
.templates(this.code)
.delete()
.then(() => this);
}
/**
* Syncs this template to the current state of the guild.
* @returns {Promise<GuildTemplate>}
*/
sync() {
return this.client.api
.guilds(this.guildID)
.templates(this.code)
.put()
.then(data => this._patch(data));
}
/**
* The timestamp of when this template was created at
* @type {number}
* @readonly
*/
get createdTimestamp() {
return this.createdAt.getTime();
}
/**
* The timestamp of when this template was last synced to the guild
* @type {number}
* @readonly
*/
get updatedTimestamp() {
return this.updatedAt.getTime();
}
/**
* The guild that this template belongs to
* @type {?Guild}
* @readonly
*/
get guild() {
return this.client.guilds.cache.get(this.guildID) || null;
}
/**
* The URL of this template
* @type {string}
* @readonly
*/
get url() {
return `${this.client.options.http.template}/${this.code}`;
}
/**
* When concatenated with a string, this automatically returns the templates's code instead of the template object.
* @returns {string}
* @example
* // Logs: Template: FKvmczH2HyUf
* console.log(`Template: ${guildTemplate}!`);
*/
toString() {
return this.code;
}
}
module.exports = GuildTemplate;
+186
View File
@@ -0,0 +1,186 @@
'use strict';
const Base = require('./Base');
const IntegrationApplication = require('./IntegrationApplication');
/**
* 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);
if (data.user) {
/**
* The user for this integration
* @type {?User}
*/
this.user = this.client.users.add(data.user);
} else {
this.user = null;
}
/**
* 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;
if ('application' in data) {
if (this.application) {
this.application._patch(data.application);
} else {
/**
* The application for this integration
* @type {?IntegrationApplication}
*/
this.application = new IntegrationApplication(this.client, data.application);
}
} else if (!this.application) {
this.application = null;
}
}
/**
* 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;
+25
View File
@@ -0,0 +1,25 @@
'use strict';
const Application = require('./interfaces/Application');
/**
* Represents an Integration's OAuth2 Application.
* @extends {Application}
*/
class IntegrationApplication extends Application {
_patch(data) {
super._patch(data);
if (typeof data.bot !== 'undefined') {
/**
* The bot {@link User user} for this application
* @type {?User}
*/
this.bot = this.client.users.add(data.bot);
} else if (!this.bot) {
this.bot = null;
}
}
}
module.exports = IntegrationApplication;
+194
View File
@@ -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;
+700
View File
@@ -0,0 +1,700 @@
'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 SnowflakeUtil = require('../util/Snowflake');
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|NewsChannel} 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|NewsChannel}
*/
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;
if ('type' in data) {
/**
* The type of the message
* @type {?MessageType}
*/
this.type = MessageTypes[data.type];
/**
* Whether or not this message was sent by Discord, not actually a user (e.g. pin notifications)
* @type {?boolean}
*/
this.system = data.type !== 0;
} else if (typeof this.type !== 'string') {
this.system = null;
this.type = null;
}
if ('content' in data) {
/**
* The content of the message
* @type {?string}
*/
this.content = data.content;
} else if (typeof this.content !== 'string') {
this.content = null;
}
if ('author' in data) {
/**
* The author of the message
* @type {?User}
*/
this.author = this.client.users.add(data.author, !data.webhook_id);
} else if (!this.author) {
this.author = null;
}
if ('pinned' in data) {
/**
* Whether or not this message is pinned
* @type {?boolean}
*/
this.pinned = Boolean(data.pinned);
} else if (typeof this.pinned !== 'boolean') {
this.pinned = null;
}
if ('tts' in data) {
/**
* Whether or not the message was Text-To-Speech
* @type {?boolean}
*/
this.tts = data.tts;
} else if (typeof this.tts !== 'boolean') {
this.tts = null;
}
/**
* 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 = 'nonce' in data ? data.nonce : null;
/**
* 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 = SnowflakeUtil.deconstruct(this.id).timestamp;
/**
* The timestamp the message was last edited at (if applicable)
* @type {?number}
*/
this.editedTimestamp = 'edited_timestamp' in data ? 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 and returns the old message.
* @param {Object} data Raw Discord message update data
* @returns {Message}
* @private
*/
patch(data) {
const clone = this._clone();
const { messageEditHistoryMaxSize } = this.client.options;
if (messageEditHistoryMaxSize !== 0) {
const editsLimit = messageEditHistoryMaxSize === -1 ? Infinity : messageEditHistoryMaxSize;
if (this._edits.unshift(clone) > editsLimit) this._edits.pop();
}
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();
return clone;
}
/**
* 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://discord.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))
);
}
/**
* Whether the message is crosspostable by the client user
* @type {boolean}
* @readonly
*/
get crosspostable() {
return (
this.channel.type === 'news' &&
!this.flags.has(MessageFlags.FLAGS.CROSSPOSTED) &&
this.type === 'DEFAULT' &&
this.channel.viewable &&
this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.SEND_MESSAGES) &&
(this.author.id === this.client.user.id ||
this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_MESSAGES))
);
}
/**
* Options that can be passed into editMessage.
* @typedef {Object} MessageEditOptions
* @property {string} [content] Content to be edited
* @property {MessageEmbed|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;
});
}
/**
* Publishes a message in an announcement channel to all channels following it.
* @returns {Promise<Message>}
* @example
* // Crosspost a message
* if (message.channel.type === 'news') {
* message.crosspost()
* .then(() => console.log('Crossposted message'))
* .catch(console.error);
* }
*/
async crosspost() {
await this.client.api.channels(this.channel.id).messages(this.id).crosspost.post();
return this;
}
/**
* Pins this message to the channel's pinned messages.
* @param {Object} [options] Options for pinning
* @param {string} [options.reason] Reason for pinning
* @returns {Promise<Message>}
* @example
* // Pin a message with a reason
* message.pin({ reason: 'important' })
* .then(console.log)
* .catch(console.error)
*/
pin(options) {
return this.client.api
.channels(this.channel.id)
.pins(this.id)
.put(options)
.then(() => this);
}
/**
* Unpins this message from the channel's pinned messages.
* @param {Object} [options] Options for unpinning
* @param {string} [options.reason] Reason for unpinning
* @returns {Promise<Message>}
* @example
* // Unpin a message with a reason
* message.unpin({ reason: 'no longer relevant' })
* .then(console.log)
* .catch(console.error)
*/
unpin(options) {
return this.client.api
.channels(this.channel.id)
.pins(this.id)
.delete(options)
.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({ timeout: 5000 })
* .then(msg => console.log(`Deleted message from ${msg.author.username} after 5 seconds`))
* .catch(console.error);
*/
delete(options = {}) {
if (typeof options !== 'object') return Promise.reject(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.
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<Message>}
*/
fetch(force = false) {
return this.channel.messages.fetch(this.id, true, force);
}
/**
* 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;
+98
View File
@@ -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;
+129
View File
@@ -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);
this.client.incrementMaxListeners();
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);
this.client.decrementMaxListeners();
});
}
/**
* 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;
+461
View File
@@ -0,0 +1,461 @@
'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 || 'rich';
/**
* The title of this embed
* @type {?string}
*/
this.title = 'title' in data ? data.title : null;
/**
* The description of this embed
* @type {?string}
*/
this.description = 'description' in data ? data.description : null;
/**
* The URL of this embed
* @type {?string}
*/
this.url = 'url' in data ? data.url : null;
/**
* The color of this embed
* @type {?number}
*/
this.color = 'color' in data ? Util.resolveColor(data.color) : null;
/**
* The timestamp of this embed
* @type {?number}
*/
this.timestamp = 'timestamp' in data ? new Date(data.timestamp).getTime() : null;
/**
* Represents a field of a MessageEmbed
* @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);
}
/**
* Represents the thumbnail of a MessageEmbed
* @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;
/**
* Represents the image of a MessageEmbed
* @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;
/**
* Represents the video of a MessageEmbed
* @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;
/**
* Represents the author field of a MessageEmbed
* @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;
/**
* Represents the provider of a MessageEmbed
* @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;
/**
* Represents the footer field of a MessageEmbed
* @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;
+225
View File
@@ -0,0 +1,225 @@
'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 MessageMentions#members}
* @type {?Collection<Snowflake, GuildMember>}
* @private
*/
this._members = null;
/**
* Cached channels for {@link MessageMentions#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|RoleResolvable|GuildChannelResolvable} data User/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 =
this.client.users.resolveID(data) ||
(this.guild && this.guild.roles.resolveID(data)) ||
this.client.channels.resolveID(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;
+136
View File
@@ -0,0 +1,136 @@
'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;
/**
* 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) {
// eslint-disable-next-line eqeqeq
if (this.count == undefined) {
/**
* The number of people that have given the same reaction
* @type {?number}
*/
this.count = data.count;
}
/**
* Whether the client has given this reaction
* @type {boolean}
*/
this.me = data.me;
}
/**
* 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;
+38
View File
@@ -0,0 +1,38 @@
'use strict';
const TextChannel = require('./TextChannel');
const { Error } = require('../errors');
/**
* 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;
}
/**
* Adds the target to this channel's followers.
* @param {GuildChannelResolvable} channel The channel where the webhook should be created
* @param {string} [reason] Reason for creating the webhook
* @returns {Promise<NewsChannel>}
* @example
* if (channel.type === 'news') {
* channel.addFollower('222197033908436994', 'Important announcements')
* .then(() => console.log('Added follower'))
* .catch(console.error);
* }
*/
async addFollower(channel, reason) {
const channelID = this.guild.channels.resolveID(channel);
if (!channelID) throw new Error('GUILD_CHANNEL_RESOLVE');
await this.client.api.channels(this.id).followers.post({ data: { webhook_channel_id: channelID }, reason });
return this;
}
}
module.exports = NewsChannel;
+46
View File
@@ -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;
+189
View File
@@ -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');
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;
+336
View File
@@ -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}
* @property {?Date} start When the activity started
* @property {?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}
* @property {?string} id ID of the party
* @property {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;
+188
View File
@@ -0,0 +1,188 @@
'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);
this.client.incrementMaxListeners();
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);
this.client.decrementMaxListeners();
});
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 when the reaction had all the users removed and the `dispose` option is set to true.
* @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 when the reaction had one user removed and the `dispose` option is set to true.
* @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;
+31
View File
@@ -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;
+403
View File
@@ -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 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(`Updated 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(updated => console.log(`Role hoisted: ${updated.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;
+32
View File
@@ -0,0 +1,32 @@
'use strict';
const GuildChannel = require('./GuildChannel');
/**
* Represents a guild store channel on Discord.
* @extends {GuildChannel}
*/
class StoreChannel extends GuildChannel {
/**
* @param {*} guild The guild the store channel is part of
* @param {*} data The data for the store channel
*/
constructor(guild, data) {
super(guild, data);
/**
* If the guild considers this channel NSFW
* @type {boolean}
* @readonly
*/
this.nsfw = Boolean(data.nsfw);
}
_patch(data) {
super._patch(data);
if (typeof data.nsfw !== 'undefined') this.nsfw = Boolean(data.nsfw);
}
}
module.exports = StoreChannel;
+109
View File
@@ -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;
+65
View File
@@ -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;
+153
View File
@@ -0,0 +1,153 @@
'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);
/**
* If the guild considers this channel NSFW
* @type {boolean}
* @readonly
*/
this.nsfw = Boolean(data.nsfw);
this._typing = new Map();
}
_patch(data) {
super._patch(data);
/**
* The topic of the text channel
* @type {?string}
*/
this.topic = data.topic;
if (typeof data.nsfw !== 'undefined') this.nsfw = Boolean(data.nsfw);
/**
* The ID of the last message sent in this channel, if one was sent
* @type {?Snowflake}
*/
this.lastMessageID = data.last_message_id;
/**
* The 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;
+343
View File
@@ -0,0 +1,343 @@
'use strict';
const Base = require('./Base');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const { Error } = require('../errors');
const Snowflake = require('../util/Snowflake');
const UserFlags = require('../util/UserFlags');
let Structures;
/**
* 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;
this.system = null;
this.locale = null;
this.flags = null;
this._patch(data);
}
_patch(data) {
if ('username' in data) {
/**
* The username of the user
* @type {?string}
*/
this.username = data.username;
} else if (typeof this.username !== 'string') {
this.username = null;
}
if ('bot' in data || typeof this.bot !== 'boolean') {
/**
* Whether or not the user is a bot
* @type {boolean}
*/
this.bot = Boolean(data.bot);
}
if ('discriminator' in data) {
/**
* A discriminator based on username for the user
* @type {?string}
*/
this.discriminator = data.discriminator;
} else if (typeof this.discriminator !== 'string') {
this.discriminator = null;
}
if ('avatar' in data) {
/**
* The ID of the user's avatar
* @type {?string}
*/
this.avatar = data.avatar;
} else if (typeof this.avatar !== 'string') {
this.avatar = null;
}
if ('system' in data) {
/**
* Whether the user is an Official Discord System user (part of the urgent message system)
* @type {?boolean}
*/
this.system = Boolean(data.system);
}
if ('locale' in data) {
/**
* The locale of the user's client (ISO 639-1)
* @type {?string}
*/
this.locale = data.locale;
}
if ('public_flags' in data) {
/**
* The flags for this user
* @type {?UserFlags}
*/
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);
}
if (!Structures) Structures = require('../util/Structures');
const Presence = Structures.get('Presence');
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.
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<DMChannel>}
*/
async createDM(force = false) {
if (!force) {
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.
* @param {boolean} [force=false] Whether to skip the cache check and request the AP
* @returns {Promise<UserFlags>}
*/
async fetchFlags(force = false) {
if (this.flags && !force) return this.flags;
const data = await this.client.api.users(this.id).get();
this._patch(data);
return this.flags;
}
/**
* Fetches this user.
* @param {boolean} [force=false] Whether to skip the cache check and request the AP
* @returns {Promise<User>}
*/
fetch(force = false) {
return this.client.users.fetch(this.id, true, force);
}
/**
* 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;
+150
View File
@@ -0,0 +1,150 @@
'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>}
* @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;
+52
View File
@@ -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;
+213
View File
@@ -0,0 +1,213 @@
'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 = 'deaf' in data ? data.deaf : null;
/**
* Whether this member is muted server-wide
* @type {?boolean}
*/
this.serverMute = 'mute' in data ? data.mute : null;
/**
* Whether this member is self-deafened
* @type {?boolean}
*/
this.selfDeaf = 'self_deaf' in data ? data.self_deaf : null;
/**
* Whether this member is self-muted
* @type {?boolean}
*/
this.selfMute = 'self_mute' in data ? data.self_mute : null;
/**
* Whether this member's camera is enabled
* @type {?boolean}
*/
this.selfVideo = 'self_video' in data ? data.self_video : null;
/**
* The session ID of this member's connection
* @type {?string}
*/
this.sessionID = 'session_id' in data ? data.session_id : null;
/**
* 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 || null;
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.
* @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;
+273
View File
@@ -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://discord.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;
+125
View File
@@ -0,0 +1,125 @@
'use strict';
const { ClientApplicationAssetTypes, Endpoints } = require('../../util/Constants');
const Snowflake = require('../../util/Snowflake');
const Base = require('../Base');
const AssetTypes = Object.keys(ClientApplicationAssetTypes);
/**
* Represents an OAuth2 Application.
* @abstract
*/
class Application 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 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} ApplicationAsset
* @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<ApplicationAsset>>}
*/
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
* Oauth2Application 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 = Application;
+281
View File
@@ -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|Promise<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
*/
async handleCollect(...args) {
const collect = this.collect(...args);
if (collect && (await 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;
+393
View File
@@ -0,0 +1,393 @@
'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://discord.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>|MessageResolvable[]|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');