SupportBot 7.3
This commit is contained in:
1 parent
19a12f6c8f
commit
09f0873f27
4762 files changed
+504608
-52
No files matched your search
+79
@@ -0,0 +1,79 @@
|
||||
'use strict';
|
||||
|
||||
const BaseGuild = require('./BaseGuild');
|
||||
const { VerificationLevels, NSFWLevels } = require('../util/Constants');
|
||||
|
||||
/**
|
||||
* Bundles common attributes and methods between {@link Guild} and {@link InviteGuild}
|
||||
* @extends {BaseGuild}
|
||||
* @abstract
|
||||
*/
|
||||
class AnonymousGuild extends BaseGuild {
|
||||
constructor(client, data, immediatePatch = true) {
|
||||
super(client, data);
|
||||
if (immediatePatch) this._patch(data);
|
||||
}
|
||||
|
||||
_patch(data) {
|
||||
this.features = data.features;
|
||||
/**
|
||||
* The hash of the guild invite splash image
|
||||
* @type {?string}
|
||||
*/
|
||||
this.splash = data.splash;
|
||||
|
||||
/**
|
||||
* The hash of the guild banner
|
||||
* @type {?string}
|
||||
*/
|
||||
this.banner = data.banner;
|
||||
|
||||
/**
|
||||
* The description of the guild, if any
|
||||
* @type {?string}
|
||||
*/
|
||||
this.description = data.description;
|
||||
|
||||
/**
|
||||
* The verification level of the guild
|
||||
* @type {VerificationLevel}
|
||||
*/
|
||||
this.verificationLevel = VerificationLevels[data.verification_level];
|
||||
|
||||
/**
|
||||
* The vanity invite code of the guild, if any
|
||||
* @type {?string}
|
||||
*/
|
||||
this.vanityURLCode = data.vanity_url_code;
|
||||
|
||||
if ('nsfw_level' in data) {
|
||||
/**
|
||||
* The NSFW level of this guild
|
||||
* @type {NSFWLevel}
|
||||
*/
|
||||
this.nsfwLevel = NSFWLevels[data.nsfw_level];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The URL to this guild's banner.
|
||||
* @param {ImageURLOptions} [options={}] Options for the Image URL
|
||||
* @returns {?string}
|
||||
*/
|
||||
bannerURL({ format, size } = {}) {
|
||||
if (!this.banner) return null;
|
||||
return this.client.rest.cdn.Banner(this.id, this.banner, format, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* The URL to this guild's invite splash image.
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AnonymousGuild;
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
'use strict';
|
||||
|
||||
const Base = require('./Base');
|
||||
const ApplicationCommandPermissionsManager = require('../managers/ApplicationCommandPermissionsManager');
|
||||
const { ApplicationCommandOptionTypes, ApplicationCommandTypes } = require('../util/Constants');
|
||||
const SnowflakeUtil = require('../util/SnowflakeUtil');
|
||||
|
||||
/**
|
||||
* Represents an application command.
|
||||
* @extends {Base}
|
||||
*/
|
||||
class ApplicationCommand extends Base {
|
||||
constructor(client, data, guild, guildId) {
|
||||
super(client);
|
||||
|
||||
/**
|
||||
* The command's id
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.id = data.id;
|
||||
|
||||
/**
|
||||
* The parent application's id
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.applicationId = data.application_id;
|
||||
|
||||
/**
|
||||
* The guild this command is part of
|
||||
* @type {?Guild}
|
||||
*/
|
||||
this.guild = guild ?? null;
|
||||
|
||||
/**
|
||||
* The guild's id this command is part of, this may be non-null when `guild` is `null` if the command
|
||||
* was fetched from the `ApplicationCommandManager`
|
||||
* @type {?Snowflake}
|
||||
*/
|
||||
this.guildId = guild?.id ?? guildId ?? null;
|
||||
|
||||
/**
|
||||
* The manager for permissions of this command on its guild or arbitrary guilds when the command is global
|
||||
* @type {ApplicationCommandPermissionsManager}
|
||||
*/
|
||||
this.permissions = new ApplicationCommandPermissionsManager(this);
|
||||
|
||||
/**
|
||||
* The type of this application command
|
||||
* @type {ApplicationCommandType}
|
||||
*/
|
||||
this.type = ApplicationCommandTypes[data.type];
|
||||
|
||||
this._patch(data);
|
||||
}
|
||||
|
||||
_patch(data) {
|
||||
/**
|
||||
* The name of this command
|
||||
* @type {string}
|
||||
*/
|
||||
this.name = data.name;
|
||||
|
||||
/**
|
||||
* The description of this command
|
||||
* @type {string}
|
||||
*/
|
||||
this.description = data.description;
|
||||
|
||||
/**
|
||||
* The options of this command
|
||||
* @type {ApplicationCommandOption[]}
|
||||
*/
|
||||
this.options = data.options?.map(o => this.constructor.transformOption(o, true)) ?? [];
|
||||
|
||||
/**
|
||||
* Whether the command is enabled by default when the app is added to a guild
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.defaultPermission = data.default_permission;
|
||||
}
|
||||
|
||||
/**
|
||||
* The timestamp the command was created at
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get createdTimestamp() {
|
||||
return SnowflakeUtil.deconstruct(this.id).timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* The time the command was created at
|
||||
* @type {Date}
|
||||
* @readonly
|
||||
*/
|
||||
get createdAt() {
|
||||
return new Date(this.createdTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* The manager that this command belongs to
|
||||
* @type {ApplicationCommandManager}
|
||||
* @readonly
|
||||
*/
|
||||
get manager() {
|
||||
return (this.guild ?? this.client.application).commands;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data for creating or editing an application command.
|
||||
* @typedef {Object} ApplicationCommandData
|
||||
* @property {string} name The name of the command
|
||||
* @property {string} description The description of the command
|
||||
* @property {ApplicationCommandType} [type] The type of the command
|
||||
* @property {ApplicationCommandOptionData[]} [options] Options for the command
|
||||
* @property {boolean} [defaultPermission] Whether the command is enabled by default when the app is added to a guild
|
||||
*/
|
||||
|
||||
/**
|
||||
* An option for an application command or subcommand.
|
||||
* @typedef {Object} ApplicationCommandOptionData
|
||||
* @property {ApplicationCommandOptionType|number} type The type of the option
|
||||
* @property {string} name The name of the option
|
||||
* @property {string} description The description of the option
|
||||
* @property {boolean} [required] Whether the option is required
|
||||
* @property {ApplicationCommandOptionChoice[]} [choices] The choices of the option for the user to pick from
|
||||
* @property {ApplicationCommandOptionData[]} [options] Additional options if this option is a subcommand (group)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Edits this application command.
|
||||
* @param {ApplicationCommandData} data The data to update the command with
|
||||
* @returns {Promise<ApplicationCommand>}
|
||||
* @example
|
||||
* // Edit the description of this command
|
||||
* command.edit({
|
||||
* description: 'New description',
|
||||
* })
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
*/
|
||||
edit(data) {
|
||||
return this.manager.edit(this, data, this.guildId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes this command.
|
||||
* @returns {Promise<ApplicationCommand>}
|
||||
* @example
|
||||
* // Delete this command
|
||||
* command.delete()
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
*/
|
||||
delete() {
|
||||
return this.manager.delete(this, this.guildId);
|
||||
}
|
||||
|
||||
/**
|
||||
* An option for an application command or subcommand.
|
||||
* @typedef {Object} ApplicationCommandOption
|
||||
* @property {ApplicationCommandOptionType} type The type of the option
|
||||
* @property {string} name The name of the option
|
||||
* @property {string} description The description of the option
|
||||
* @property {boolean} [required] Whether the option is required
|
||||
* @property {ApplicationCommandOptionChoice[]} [choices] The choices of the option for the user to pick from
|
||||
* @property {ApplicationCommandOption[]} [options] Additional options if this option is a subcommand (group)
|
||||
*/
|
||||
|
||||
/**
|
||||
* A choice for an application command option.
|
||||
* @typedef {Object} ApplicationCommandOptionChoice
|
||||
* @property {string} name The name of the choice
|
||||
* @property {string|number} value The value of the choice
|
||||
*/
|
||||
|
||||
/**
|
||||
* Transforms an {@link ApplicationCommandOptionData} object into something that can be used with the API.
|
||||
* @param {ApplicationCommandOptionData} option The option to transform
|
||||
* @param {boolean} [received] Whether this option has been received from Discord
|
||||
* @returns {APIApplicationCommandOption}
|
||||
* @private
|
||||
*/
|
||||
static transformOption(option, received) {
|
||||
const stringType = typeof option.type === 'string' ? option.type : ApplicationCommandOptionTypes[option.type];
|
||||
return {
|
||||
type: typeof option.type === 'number' && !received ? option.type : ApplicationCommandOptionTypes[option.type],
|
||||
name: option.name,
|
||||
description: option.description,
|
||||
required:
|
||||
option.required ?? (stringType === 'SUB_COMMAND' || stringType === 'SUB_COMMAND_GROUP' ? undefined : false),
|
||||
choices: option.choices,
|
||||
options: option.options?.map(o => this.transformOption(o, received)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ApplicationCommand;
|
||||
|
||||
/* eslint-disable max-len */
|
||||
/**
|
||||
* @external APIApplicationCommand
|
||||
* @see {@link https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-structure}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external APIApplicationCommandOption
|
||||
* @see {@link https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-structure}
|
||||
*/
|
||||
+43
@@ -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;
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const Interaction = require('./Interaction');
|
||||
const InteractionWebhook = require('./InteractionWebhook');
|
||||
const InteractionResponses = require('./interfaces/InteractionResponses');
|
||||
const { ApplicationCommandOptionTypes } = require('../util/Constants');
|
||||
|
||||
/**
|
||||
* Represents a command interaction.
|
||||
* @extends {Interaction}
|
||||
* @implements {InteractionResponses}
|
||||
* @abstract
|
||||
*/
|
||||
class BaseCommandInteraction extends Interaction {
|
||||
constructor(client, data) {
|
||||
super(client, data);
|
||||
|
||||
/**
|
||||
* The channel this interaction was sent in
|
||||
* @type {?TextBasedChannels}
|
||||
* @name BaseCommandInteraction#channel
|
||||
* @readonly
|
||||
*/
|
||||
|
||||
/**
|
||||
* The id of the channel this interaction was sent in
|
||||
* @type {Snowflake}
|
||||
* @name BaseCommandInteraction#channelId
|
||||
*/
|
||||
|
||||
/**
|
||||
* The invoked application command's id
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.commandId = data.data.id;
|
||||
|
||||
/**
|
||||
* The invoked application command's name
|
||||
* @type {string}
|
||||
*/
|
||||
this.commandName = data.data.name;
|
||||
|
||||
/**
|
||||
* Whether the reply to this interaction has been deferred
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.deferred = false;
|
||||
|
||||
/**
|
||||
* Whether this interaction has already been replied to
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.replied = false;
|
||||
|
||||
/**
|
||||
* Whether the reply to this interaction is ephemeral
|
||||
* @type {?boolean}
|
||||
*/
|
||||
this.ephemeral = null;
|
||||
|
||||
/**
|
||||
* An associated interaction webhook, can be used to further interact with this interaction
|
||||
* @type {InteractionWebhook}
|
||||
*/
|
||||
this.webhook = new InteractionWebhook(this.client, this.applicationId, this.token);
|
||||
}
|
||||
|
||||
/**
|
||||
* The invoked application command, if it was fetched before
|
||||
* @type {?ApplicationCommand}
|
||||
*/
|
||||
get command() {
|
||||
const id = this.commandId;
|
||||
return this.guild?.commands.cache.get(id) ?? this.client.application.commands.cache.get(id) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the resolved data of a received command interaction.
|
||||
* @typedef {Object} CommandInteractionResolvedData
|
||||
* @property {Collection<string, User>} [users] The resolved users
|
||||
* @property {Collection<string, GuildMember|APIGuildMember>} [members] The resolved guild members
|
||||
* @property {Collection<string, Role|APIRole>} [roles] The resolved roles
|
||||
* @property {Collection<string, Channel|APIChannel>} [channels] The resolved channels
|
||||
* @property {Collection<string, Message|APIMessage>} [messages] The resolved messages
|
||||
*/
|
||||
|
||||
/**
|
||||
* Transforms the resolved received from the API.
|
||||
* @param {APIInteractionDataResolved} resolved The received resolved objects
|
||||
* @returns {CommandInteractionResolvedData}
|
||||
* @private
|
||||
*/
|
||||
transformResolved({ members, users, channels, roles, messages }) {
|
||||
const result = {};
|
||||
|
||||
if (members) {
|
||||
result.members = new Collection();
|
||||
for (const [id, member] of Object.entries(members)) {
|
||||
const user = users[id];
|
||||
result.members.set(id, this.guild?.members._add({ user, ...member }) ?? member);
|
||||
}
|
||||
}
|
||||
|
||||
if (users) {
|
||||
result.users = new Collection();
|
||||
for (const user of Object.values(users)) {
|
||||
result.users.set(user.id, this.client.users._add(user));
|
||||
}
|
||||
}
|
||||
|
||||
if (roles) {
|
||||
result.roles = new Collection();
|
||||
for (const role of Object.values(roles)) {
|
||||
result.roles.set(role.id, this.guild?.roles._add(role) ?? role);
|
||||
}
|
||||
}
|
||||
|
||||
if (channels) {
|
||||
result.channels = new Collection();
|
||||
for (const channel of Object.values(channels)) {
|
||||
result.channels.set(channel.id, this.client.channels._add(channel, this.guild) ?? channel);
|
||||
}
|
||||
}
|
||||
|
||||
if (messages) {
|
||||
result.messages = new Collection();
|
||||
for (const message of Object.values(messages)) {
|
||||
result.messages.set(message.id, this.channel?.messages?._add(message) ?? message);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an option of a received command interaction.
|
||||
* @typedef {Object} CommandInteractionOption
|
||||
* @property {string} name The name of the option
|
||||
* @property {ApplicationCommandOptionType} type The type of the option
|
||||
* @property {string|number|boolean} [value] The value of the option
|
||||
* @property {CommandInteractionOption[]} [options] Additional options if this option is a
|
||||
* subcommand (group)
|
||||
* @property {User} [user] The resolved user
|
||||
* @property {GuildMember|APIGuildMember} [member] The resolved member
|
||||
* @property {GuildChannel|APIChannel} [channel] The resolved channel
|
||||
* @property {Role|APIRole} [role] The resolved role
|
||||
*/
|
||||
|
||||
/**
|
||||
* Transforms an option received from the API.
|
||||
* @param {APIApplicationCommandOption} option The received option
|
||||
* @param {APIInteractionDataResolved} resolved The resolved interaction data
|
||||
* @returns {CommandInteractionOption}
|
||||
* @private
|
||||
*/
|
||||
transformOption(option, resolved) {
|
||||
const result = {
|
||||
name: option.name,
|
||||
type: ApplicationCommandOptionTypes[option.type],
|
||||
};
|
||||
|
||||
if ('value' in option) result.value = option.value;
|
||||
if ('options' in option) result.options = option.options.map(opt => this.transformOption(opt, resolved));
|
||||
|
||||
if (resolved) {
|
||||
const user = resolved.users?.[option.value];
|
||||
if (user) result.user = this.client.users._add(user);
|
||||
|
||||
const member = resolved.members?.[option.value];
|
||||
if (member) result.member = this.guild?.members._add({ user, ...member }) ?? member;
|
||||
|
||||
const channel = resolved.channels?.[option.value];
|
||||
if (channel) result.channel = this.client.channels._add(channel, this.guild) ?? channel;
|
||||
|
||||
const role = resolved.roles?.[option.value];
|
||||
if (role) result.role = this.guild?.roles._add(role) ?? role;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// These are here only for documentation purposes - they are implemented by InteractionResponses
|
||||
/* eslint-disable no-empty-function */
|
||||
defer() {}
|
||||
reply() {}
|
||||
fetchReply() {}
|
||||
editReply() {}
|
||||
deleteReply() {}
|
||||
followUp() {}
|
||||
}
|
||||
|
||||
InteractionResponses.applyToClass(BaseCommandInteraction, ['deferUpdate', 'update']);
|
||||
|
||||
module.exports = BaseCommandInteraction;
|
||||
|
||||
/* eslint-disable max-len */
|
||||
/**
|
||||
* @external APIInteractionDataResolved
|
||||
* @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-resolved-data-structure}
|
||||
*/
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
'use strict';
|
||||
|
||||
const Base = require('./Base');
|
||||
const SnowflakeUtil = require('../util/SnowflakeUtil');
|
||||
|
||||
/**
|
||||
* The base class for {@link Guild}, {@link OAuth2Guild} and {@link InviteGuild}.
|
||||
* @extends {Base}
|
||||
* @abstract
|
||||
*/
|
||||
class BaseGuild extends Base {
|
||||
constructor(client, data) {
|
||||
super(client);
|
||||
|
||||
/**
|
||||
* The guild's id
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.id = data.id;
|
||||
|
||||
/**
|
||||
* The name of this guild
|
||||
* @type {string}
|
||||
*/
|
||||
this.name = data.name;
|
||||
|
||||
/**
|
||||
* The icon hash of this guild
|
||||
* @type {?string}
|
||||
*/
|
||||
this.icon = data.icon;
|
||||
|
||||
/**
|
||||
* An array of features available to this guild
|
||||
* @type {Features[]}
|
||||
*/
|
||||
this.features = data.features;
|
||||
}
|
||||
|
||||
/**
|
||||
* The timestamp this guild was created at
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get createdTimestamp() {
|
||||
return SnowflakeUtil.deconstruct(this.id).timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* The time this guild was created at
|
||||
* @type {Date}
|
||||
* @readonly
|
||||
*/
|
||||
get createdAt() {
|
||||
return new Date(this.createdTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* The acronym that shows up in place of a guild icon
|
||||
* @type {string}
|
||||
* @readonly
|
||||
*/
|
||||
get nameAcronym() {
|
||||
return this.name
|
||||
.replace(/'s /g, ' ')
|
||||
.replace(/\w+/g, e => e[0])
|
||||
.replace(/\s/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this guild is partnered
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get partnered() {
|
||||
return this.features.includes('PARTNERED');
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this guild is verified
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get verified() {
|
||||
return this.features.includes('VERIFIED');
|
||||
}
|
||||
|
||||
/**
|
||||
* The URL to this guild's icon.
|
||||
* @param {ImageURLOptions} [options={}] Options for the Image URL
|
||||
* @returns {?string}
|
||||
*/
|
||||
iconURL({ format, size, dynamic } = {}) {
|
||||
if (!this.icon) return null;
|
||||
return this.client.rest.cdn.Icon(this.id, this.icon, format, size, dynamic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches this guild.
|
||||
* @returns {Promise<Guild>}
|
||||
*/
|
||||
async fetch() {
|
||||
const data = await this.client.api.guilds(this.id).get({ query: { with_counts: true } });
|
||||
return this.client.guilds._add(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* When concatenated with a string, this automatically returns the guild's name instead of the Guild object.
|
||||
* @returns {string}
|
||||
*/
|
||||
toString() {
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseGuild;
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
'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.requiresColons = null;
|
||||
this.managed = null;
|
||||
this.available = null;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseGuildEmoji;
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const GuildChannel = require('./GuildChannel');
|
||||
const Webhook = require('./Webhook');
|
||||
const TextBasedChannel = require('./interfaces/TextBasedChannel');
|
||||
const MessageManager = require('../managers/MessageManager');
|
||||
const ThreadManager = require('../managers/ThreadManager');
|
||||
const DataResolver = require('../util/DataResolver');
|
||||
|
||||
/**
|
||||
* Represents a text-based guild channel on Discord.
|
||||
* @extends {GuildChannel}
|
||||
* @implements {TextBasedChannel}
|
||||
*/
|
||||
class BaseGuildTextChannel extends GuildChannel {
|
||||
/**
|
||||
* @param {Guild} guild The guild the text channel is part of
|
||||
* @param {APIChannel} data The data for the text channel
|
||||
* @param {Client} [client] A safety parameter for the client that instantiated this
|
||||
*/
|
||||
constructor(guild, data, client) {
|
||||
super(guild, data, client, false);
|
||||
|
||||
/**
|
||||
* A manager of the messages sent to this channel
|
||||
* @type {MessageManager}
|
||||
*/
|
||||
this.messages = new MessageManager(this);
|
||||
|
||||
/**
|
||||
* A manager of the threads belonging to this channel
|
||||
* @type {ThreadManager}
|
||||
*/
|
||||
this.threads = new ThreadManager(this);
|
||||
|
||||
/**
|
||||
* If the guild considers this channel NSFW
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.nsfw = Boolean(data.nsfw);
|
||||
|
||||
this._patch(data);
|
||||
}
|
||||
|
||||
_patch(data) {
|
||||
super._patch(data);
|
||||
|
||||
if ('topic' in data) {
|
||||
/**
|
||||
* The topic of the text channel
|
||||
* @type {?string}
|
||||
*/
|
||||
this.topic = data.topic;
|
||||
}
|
||||
|
||||
if ('nsfw' in data) {
|
||||
this.nsfw = Boolean(data.nsfw);
|
||||
}
|
||||
|
||||
if ('last_message_id' in data) {
|
||||
/**
|
||||
* The last message id sent in the channel, if one was sent
|
||||
* @type {?Snowflake}
|
||||
*/
|
||||
this.lastMessageId = data.last_message_id;
|
||||
}
|
||||
|
||||
if ('last_pin_timestamp' in data) {
|
||||
/**
|
||||
* 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 ('default_auto_archive_duration' in data) {
|
||||
/**
|
||||
* The default auto archive duration for newly created threads in this channel
|
||||
* @type {?ThreadAutoArchiveDuration}
|
||||
*/
|
||||
this.defaultAutoArchiveDuration = data.default_auto_archive_duration;
|
||||
}
|
||||
|
||||
if ('messages' in data) {
|
||||
for (const message of data.messages) this.messages._add(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default auto archive duration for all newly created threads in this channel.
|
||||
* @param {ThreadAutoArchiveDuration} defaultAutoArchiveDuration The new default auto archive duration
|
||||
* @param {string} [reason] Reason for changing the channel's default auto archive duration
|
||||
* @returns {Promise<TextChannel>}
|
||||
*/
|
||||
setDefaultAutoArchiveDuration(defaultAutoArchiveDuration, reason) {
|
||||
return this.edit({ defaultAutoArchiveDuration }, 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the type of this channel (only conversion between text and news is supported)
|
||||
* @param {string} type The new channel type
|
||||
* @param {string} [reason] Reason for changing the channel's type
|
||||
* @returns {Promise<GuildChannel>}
|
||||
*/
|
||||
setType(type, reason) {
|
||||
return this.edit({ type }, 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);
|
||||
*/
|
||||
async fetchWebhooks() {
|
||||
const data = await this.client.api.channels[this.id].webhooks.get();
|
||||
const hooks = new Collection();
|
||||
for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook));
|
||||
return hooks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options used to create a {@link Webhook} for {@link TextChannel} and {@link NewsChannel}.
|
||||
* @typedef {Object} ChannelWebhookCreateOptions
|
||||
* @property {BufferResolvable|Base64Resolvable} [avatar] Avatar for the webhook
|
||||
* @property {string} [reason] Reason for creating the webhook
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a webhook for the channel.
|
||||
* @param {string} name The name of the webhook
|
||||
* @param {ChannelWebhookCreateOptions} [options] Options for creating the webhook
|
||||
* @returns {Promise<Webhook>} Returns 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);
|
||||
}
|
||||
const data = await this.client.api.channels[this.id].webhooks.post({
|
||||
data: {
|
||||
name,
|
||||
avatar,
|
||||
},
|
||||
reason,
|
||||
});
|
||||
return new Webhook(this.client, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Options used to create an invite to a guild channel.
|
||||
* @typedef {Object} CreateInviteOptions
|
||||
* @property {boolean} [temporary=false] Whether members that joined via the invite should be automatically
|
||||
* kicked after 24 hours if they have not yet received a role
|
||||
* @property {number} [maxAge=86400] How long the invite should last (in seconds, 0 for forever)
|
||||
* @property {number} [maxUses=0] Maximum number of uses
|
||||
* @property {boolean} [unique=false] Create a unique invite, or use an existing one with similar settings
|
||||
* @property {UserResolvable} [targetUser] The user whose stream to display for this invite,
|
||||
* required if `targetType` is 1, the user must be streaming in the channel
|
||||
* @property {ApplicationResolvable} [targetApplication] The embedded application to open for this invite,
|
||||
* required if `targetType` is 2, the application must have the `EMBEDDED` flag
|
||||
* @property {TargetType} [targetType] The type of the target for this voice channel invite
|
||||
* @property {string} [reason] The reason for creating the invite
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates an invite to this guild channel.
|
||||
* @param {CreateInviteOptions} [options={}] The options for creating the invite
|
||||
* @returns {Promise<Invite>}
|
||||
* @example
|
||||
* // Create an invite to a channel
|
||||
* channel.createInvite()
|
||||
* .then(invite => console.log(`Created an invite with a code of ${invite.code}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
createInvite(options) {
|
||||
return this.guild.invites.create(this.id, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a collection of invites to this guild channel.
|
||||
* Resolves with a collection mapping invites by their codes.
|
||||
* @param {boolean} [cache=true] Whether or not to cache the fetched invites
|
||||
* @returns {Promise<Collection<string, Invite>>}
|
||||
*/
|
||||
fetchInvites(cache = true) {
|
||||
return this.guild.invites.fetch({ channelId: this.id, cache });
|
||||
}
|
||||
|
||||
// These are here only for documentation purposes - they are implemented by TextBasedChannel
|
||||
/* eslint-disable no-empty-function */
|
||||
get lastMessage() {}
|
||||
get lastPinAt() {}
|
||||
send() {}
|
||||
sendTyping() {}
|
||||
createMessageCollector() {}
|
||||
awaitMessages() {}
|
||||
createMessageComponentCollector() {}
|
||||
awaitMessageComponent() {}
|
||||
bulkDelete() {}
|
||||
}
|
||||
|
||||
TextBasedChannel.applyToClass(BaseGuildTextChannel, true);
|
||||
|
||||
module.exports = BaseGuildTextChannel;
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const GuildChannel = require('./GuildChannel');
|
||||
const Permissions = require('../util/Permissions');
|
||||
|
||||
/**
|
||||
* Represents a voice-based guild channel on Discord.
|
||||
* @extends {GuildChannel}
|
||||
*/
|
||||
class BaseGuildVoiceChannel extends GuildChannel {
|
||||
_patch(data) {
|
||||
super._patch(data);
|
||||
|
||||
/**
|
||||
* The RTC region for this voice-based channel. This region is automatically selected if `null`.
|
||||
* @type {?string}
|
||||
*/
|
||||
this.rtcRegion = data.rtc_region;
|
||||
|
||||
/**
|
||||
* The bitrate of this voice-based channel
|
||||
* @type {number}
|
||||
*/
|
||||
this.bitrate = data.bitrate;
|
||||
|
||||
/**
|
||||
* The maximum amount of users allowed in this channel.
|
||||
* @type {number}
|
||||
*/
|
||||
this.userLimit = data.user_limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* The members in this voice-based 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-based channel is full
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get full() {
|
||||
return this.userLimit > 0 && this.members.size >= this.userLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the channel is joinable by the client user
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get joinable() {
|
||||
if (!this.viewable) return false;
|
||||
if (!this.permissionsFor(this.client.user).has(Permissions.FLAGS.CONNECT, false)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the RTC region of the channel.
|
||||
* @param {?string} region The new region of the channel. Set to `null` to remove a specific region for the channel
|
||||
* @returns {Promise<BaseGuildVoiceChannel>}
|
||||
* @example
|
||||
* // Set the RTC region to europe
|
||||
* channel.setRTCRegion('europe');
|
||||
* @example
|
||||
* // Remove a fixed region for this channel - let Discord decide automatically
|
||||
* channel.setRTCRegion(null);
|
||||
*/
|
||||
setRTCRegion(region) {
|
||||
return this.edit({ rtcRegion: region });
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an invite to this guild channel.
|
||||
* @param {CreateInviteOptions} [options={}] The options for creating the invite
|
||||
* @returns {Promise<Invite>}
|
||||
* @example
|
||||
* // Create an invite to a channel
|
||||
* channel.createInvite()
|
||||
* .then(invite => console.log(`Created an invite with a code of ${invite.code}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
createInvite(options) {
|
||||
return this.guild.invites.create(this.id, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a collection of invites to this guild channel.
|
||||
* Resolves with a collection mapping invites by their codes.
|
||||
* @param {boolean} [cache=true] Whether or not to cache the fetched invites
|
||||
* @returns {Promise<Collection<string, Invite>>}
|
||||
*/
|
||||
fetchInvites(cache = true) {
|
||||
return this.guild.invites.fetch({ channelId: this.id, cache });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseGuildVoiceChannel;
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
'use strict';
|
||||
|
||||
const { TypeError } = require('../errors');
|
||||
const { MessageComponentTypes, Events } = require('../util/Constants');
|
||||
|
||||
/**
|
||||
* Represents an interactive component of a Message. It should not be necessary to construct this directly.
|
||||
* See {@link MessageComponent}
|
||||
*/
|
||||
class BaseMessageComponent {
|
||||
/**
|
||||
* Options for a BaseMessageComponent
|
||||
* @typedef {Object} BaseMessageComponentOptions
|
||||
* @property {MessageComponentTypeResolvable} type The type of this component
|
||||
*/
|
||||
|
||||
/**
|
||||
* Data that can be resolved into options for a MessageComponent. This can be:
|
||||
* * MessageActionRowOptions
|
||||
* * MessageButtonOptions
|
||||
* * MessageSelectMenuOptions
|
||||
* @typedef {MessageActionRowOptions|MessageButtonOptions|MessageSelectMenuOptions} MessageComponentOptions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Components that can be sent in a message. These can be:
|
||||
* * MessageActionRow
|
||||
* * MessageButton
|
||||
* * MessageSelectMenu
|
||||
* @typedef {MessageActionRow|MessageButton|MessageSelectMenu} MessageComponent
|
||||
* @see {@link https://discord.com/developers/docs/interactions/message-components#component-object-component-types}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Data that can be resolved to a MessageComponentType. This can be:
|
||||
* * MessageComponentType
|
||||
* * string
|
||||
* * number
|
||||
* @typedef {string|number|MessageComponentType} MessageComponentTypeResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {BaseMessageComponent|BaseMessageComponentOptions} [data={}] The options for this component
|
||||
*/
|
||||
constructor(data) {
|
||||
/**
|
||||
* The type of this component
|
||||
* @type {?MessageComponentType}
|
||||
*/
|
||||
this.type = 'type' in data ? BaseMessageComponent.resolveType(data.type) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a MessageComponent based on the type of the incoming data
|
||||
* @param {MessageComponentOptions} data Data for a MessageComponent
|
||||
* @param {Client|WebhookClient} [client] Client constructing this component
|
||||
* @returns {?MessageComponent}
|
||||
* @private
|
||||
*/
|
||||
static create(data, client) {
|
||||
let component;
|
||||
let type = data.type;
|
||||
|
||||
if (typeof type === 'string') type = MessageComponentTypes[type];
|
||||
|
||||
switch (type) {
|
||||
case MessageComponentTypes.ACTION_ROW: {
|
||||
const MessageActionRow = require('./MessageActionRow');
|
||||
component = new MessageActionRow(data, client);
|
||||
break;
|
||||
}
|
||||
case MessageComponentTypes.BUTTON: {
|
||||
const MessageButton = require('./MessageButton');
|
||||
component = new MessageButton(data);
|
||||
break;
|
||||
}
|
||||
case MessageComponentTypes.SELECT_MENU: {
|
||||
const MessageSelectMenu = require('./MessageSelectMenu');
|
||||
component = new MessageSelectMenu(data);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (client) {
|
||||
client.emit(Events.DEBUG, `[BaseMessageComponent] Received component with unknown type: ${data.type}`);
|
||||
} else {
|
||||
throw new TypeError('INVALID_TYPE', 'data.type', 'valid MessageComponentType');
|
||||
}
|
||||
}
|
||||
return component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the type of a MessageComponent
|
||||
* @param {MessageComponentTypeResolvable} type The type to resolve
|
||||
* @returns {MessageComponentType}
|
||||
* @private
|
||||
*/
|
||||
static resolveType(type) {
|
||||
return typeof type === 'string' ? type : MessageComponentTypes[type];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseMessageComponent;
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
const MessageComponentInteraction = require('./MessageComponentInteraction');
|
||||
|
||||
/**
|
||||
* Represents a button interaction.
|
||||
* @extends {MessageComponentInteraction}
|
||||
*/
|
||||
class ButtonInteraction extends MessageComponentInteraction {}
|
||||
|
||||
module.exports = ButtonInteraction;
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
'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 {?CategoryChannelResolvable} channel The channel to set as parent
|
||||
* @param {SetParentOptions} [options={}] The options for setting the parent
|
||||
* @returns {Promise<GuildChannel>}
|
||||
*/
|
||||
}
|
||||
|
||||
module.exports = CategoryChannel;
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
'use strict';
|
||||
|
||||
const Base = require('./Base');
|
||||
let CategoryChannel;
|
||||
let DMChannel;
|
||||
let NewsChannel;
|
||||
let StageChannel;
|
||||
let StoreChannel;
|
||||
let TextChannel;
|
||||
let ThreadChannel;
|
||||
let VoiceChannel;
|
||||
const { ChannelTypes, ThreadChannelTypes, VoiceBasedChannelTypes } = require('../util/Constants');
|
||||
const SnowflakeUtil = require('../util/SnowflakeUtil');
|
||||
|
||||
/**
|
||||
* Represents any channel on Discord.
|
||||
* @extends {Base}
|
||||
* @abstract
|
||||
*/
|
||||
class Channel extends Base {
|
||||
constructor(client, data, immediatePatch = true) {
|
||||
super(client);
|
||||
|
||||
const type = ChannelTypes[data.type];
|
||||
/**
|
||||
* The type of the channel
|
||||
* @type {ChannelType}
|
||||
*/
|
||||
this.type = type ?? 'UNKNOWN';
|
||||
|
||||
/**
|
||||
* Whether the channel has been deleted
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.deleted = false;
|
||||
|
||||
if (data && immediatePatch) this._patch(data);
|
||||
}
|
||||
|
||||
_patch(data) {
|
||||
/**
|
||||
* The channel's id
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.id = data.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* The timestamp the channel was created at
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get createdTimestamp() {
|
||||
return SnowflakeUtil.deconstruct(this.id).timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* The time the channel was created at
|
||||
* @type {Date}
|
||||
* @readonly
|
||||
*/
|
||||
get createdAt() {
|
||||
return new Date(this.createdTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this Channel is a partial
|
||||
* <info>This is always false outside of DM channels.</info>
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get partial() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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=true] Whether to skip the cache check and request the API
|
||||
* @returns {Promise<Channel>}
|
||||
*/
|
||||
fetch(force = true) {
|
||||
return this.client.channels.fetch(this.id, { force });
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this channel is {@link TextBasedChannels text-based}.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isText() {
|
||||
return 'messages' in this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this channel is voice-based
|
||||
* ({@link VoiceChannel} or {@link StageChannel}).
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isVoice() {
|
||||
return VoiceBasedChannelTypes.includes(this.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this channel is a {@link ThreadChannel}.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isThread() {
|
||||
return ThreadChannelTypes.includes(this.type);
|
||||
}
|
||||
|
||||
static create(client, data, guild, { allowUnknownGuild, fromInteraction } = {}) {
|
||||
if (!CategoryChannel) CategoryChannel = require('./CategoryChannel');
|
||||
if (!DMChannel) DMChannel = require('./DMChannel');
|
||||
if (!NewsChannel) NewsChannel = require('./NewsChannel');
|
||||
if (!StageChannel) StageChannel = require('./StageChannel');
|
||||
if (!StoreChannel) StoreChannel = require('./StoreChannel');
|
||||
if (!TextChannel) TextChannel = require('./TextChannel');
|
||||
if (!ThreadChannel) ThreadChannel = require('./ThreadChannel');
|
||||
if (!VoiceChannel) VoiceChannel = require('./VoiceChannel');
|
||||
|
||||
let channel;
|
||||
if (!data.guild_id && !guild) {
|
||||
if ((data.recipients && data.type !== ChannelTypes.GROUP_DM) || data.type === ChannelTypes.DM) {
|
||||
channel = new DMChannel(client, data);
|
||||
} else if (data.type === ChannelTypes.GROUP_DM) {
|
||||
const PartialGroupDMChannel = require('./PartialGroupDMChannel');
|
||||
channel = new PartialGroupDMChannel(client, data);
|
||||
}
|
||||
} else {
|
||||
if (!guild) guild = client.guilds.cache.get(data.guild_id);
|
||||
|
||||
if (guild || allowUnknownGuild) {
|
||||
switch (data.type) {
|
||||
case ChannelTypes.GUILD_TEXT: {
|
||||
channel = new TextChannel(guild, data, client);
|
||||
break;
|
||||
}
|
||||
case ChannelTypes.GUILD_VOICE: {
|
||||
channel = new VoiceChannel(guild, data, client);
|
||||
break;
|
||||
}
|
||||
case ChannelTypes.GUILD_CATEGORY: {
|
||||
channel = new CategoryChannel(guild, data, client);
|
||||
break;
|
||||
}
|
||||
case ChannelTypes.GUILD_NEWS: {
|
||||
channel = new NewsChannel(guild, data, client);
|
||||
break;
|
||||
}
|
||||
case ChannelTypes.GUILD_STORE: {
|
||||
channel = new StoreChannel(guild, data, client);
|
||||
break;
|
||||
}
|
||||
case ChannelTypes.GUILD_STAGE_VOICE: {
|
||||
channel = new StageChannel(guild, data, client);
|
||||
break;
|
||||
}
|
||||
case ChannelTypes.GUILD_NEWS_THREAD:
|
||||
case ChannelTypes.GUILD_PUBLIC_THREAD:
|
||||
case ChannelTypes.GUILD_PRIVATE_THREAD: {
|
||||
channel = new ThreadChannel(guild, data, client, fromInteraction);
|
||||
if (!allowUnknownGuild) channel.parent?.threads.cache.set(channel.id, channel);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (channel && !allowUnknownGuild) guild.channels?.cache.set(channel.id, channel);
|
||||
}
|
||||
}
|
||||
return channel;
|
||||
}
|
||||
|
||||
toJSON(...props) {
|
||||
return super.toJSON({ createdTimestamp: true }, ...props);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Channel;
|
||||
|
||||
/**
|
||||
* @external APIChannel
|
||||
* @see {@link https://discord.com/developers/docs/resources/channel#channel-object}
|
||||
*/
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
'use strict';
|
||||
|
||||
const Team = require('./Team');
|
||||
const Application = require('./interfaces/Application');
|
||||
const ApplicationCommandManager = require('../managers/ApplicationCommandManager');
|
||||
const ApplicationFlags = require('../util/ApplicationFlags');
|
||||
|
||||
/**
|
||||
* Represents a Client OAuth2 Application.
|
||||
* @extends {Application}
|
||||
*/
|
||||
class ClientApplication extends Application {
|
||||
constructor(client, data) {
|
||||
super(client, data);
|
||||
|
||||
/**
|
||||
* The application command manager for this application
|
||||
* @type {ApplicationCommandManager}
|
||||
*/
|
||||
this.commands = new ApplicationCommandManager(this.client);
|
||||
}
|
||||
|
||||
_patch(data) {
|
||||
super._patch(data);
|
||||
|
||||
/**
|
||||
* The flags this application has
|
||||
* @type {ApplicationFlags}
|
||||
*/
|
||||
this.flags = 'flags' in data ? new ApplicationFlags(data.flags).freeze() : this.flags;
|
||||
|
||||
/**
|
||||
* The hash of the application's cover image
|
||||
* @type {?string}
|
||||
*/
|
||||
this.cover = data.cover_image ?? this.cover ?? null;
|
||||
|
||||
/**
|
||||
* The application's RPC origins, if enabled
|
||||
* @type {string[]}
|
||||
*/
|
||||
this.rpcOrigins = data.rpc_origins ?? this.rpcOrigins ?? [];
|
||||
|
||||
/**
|
||||
* If this application's bot requires a code grant when using the OAuth2 flow
|
||||
* @type {?boolean}
|
||||
*/
|
||||
this.botRequireCodeGrant = data.bot_require_code_grant ?? this.botRequireCodeGrant ?? null;
|
||||
|
||||
/**
|
||||
* If this application's bot is public
|
||||
* @type {?boolean}
|
||||
*/
|
||||
this.botPublic = data.bot_public ?? this.botPublic ?? 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)
|
||||
: this.owner ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this application is partial
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get partial() {
|
||||
return !this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains this application from Discord.
|
||||
* @returns {Promise<ClientApplication>}
|
||||
*/
|
||||
async fetch() {
|
||||
const app = await this.client.api.oauth2.applications('@me').get();
|
||||
this._patch(app);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ClientApplication;
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
'use strict';
|
||||
|
||||
const { Presence } = require('./Presence');
|
||||
const { TypeError } = require('../errors');
|
||||
const { ActivityTypes, Opcodes } = require('../util/Constants');
|
||||
|
||||
class ClientPresence extends Presence {
|
||||
/**
|
||||
* @param {Client} client The instantiating client
|
||||
* @param {APIPresence} [data={}] The data for the client presence
|
||||
*/
|
||||
constructor(client, data = {}) {
|
||||
super(client, Object.assign(data, { status: data.status ?? 'online', user: { id: null } }));
|
||||
}
|
||||
|
||||
set(presence) {
|
||||
const packet = 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;
|
||||
}
|
||||
|
||||
_parse({ status, since, afk, activities }) {
|
||||
const data = {
|
||||
activities: [],
|
||||
afk: typeof afk === 'boolean' ? afk : false,
|
||||
since: typeof since === 'number' && !Number.isNaN(since) ? since : null,
|
||||
status: status ?? this.status,
|
||||
};
|
||||
if (activities?.length) {
|
||||
for (const [i, activity] of activities.entries()) {
|
||||
if (typeof activity.name !== 'string') throw new TypeError('INVALID_TYPE', `activities[${i}].name`, 'string');
|
||||
if (!activity.type) activity.type = 0;
|
||||
|
||||
data.activities.push({
|
||||
type: typeof activity.type === 'number' ? activity.type : ActivityTypes[activity.type],
|
||||
name: activity.name,
|
||||
url: activity.url,
|
||||
});
|
||||
}
|
||||
} else if (!activities && (status || afk || since) && this.activities.length) {
|
||||
data.activities.push(
|
||||
...this.activities.map(a => ({
|
||||
name: a.name,
|
||||
type: ActivityTypes[a.type],
|
||||
url: a.url ?? undefined,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ClientPresence;
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
'use strict';
|
||||
|
||||
const User = require('./User');
|
||||
const DataResolver = require('../util/DataResolver');
|
||||
|
||||
/**
|
||||
* Represents the logged in client's Discord user.
|
||||
* @extends {User}
|
||||
*/
|
||||
class ClientUser extends User {
|
||||
_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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data used to edit the logged in client
|
||||
* @typdef {Object} ClientUserEditData
|
||||
* @property {string} [username] The new username
|
||||
* @property {BufferResolvable|Base64Resolvable} [avatar] The new avatar
|
||||
*/
|
||||
|
||||
/**
|
||||
* Edits the logged in client.
|
||||
* @param {ClientUserEditData} data The new data
|
||||
*/
|
||||
async edit(data) {
|
||||
const newData = await this.client.api.users('@me').patch({ data });
|
||||
this.client.token = newData.token;
|
||||
const { updated } = this.client.actions.UserUpdate.handle(newData);
|
||||
return updated ?? 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) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for setting activities
|
||||
* @typedef {Object} ActivitiesOptions
|
||||
* @property {string} [name] Name of the activity
|
||||
* @property {ActivityType|number} [type] Type of the activity
|
||||
* @property {string} [url] Twitch / YouTube stream URL
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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 {ActivitiesOptions[]} [activities] Activity the user is playing
|
||||
* @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 {Presence}
|
||||
* @example
|
||||
* // Set the client user's presence
|
||||
* client.user.setPresence({ activities: [{ name: 'with discord.js' }], status: 'idle' });
|
||||
*/
|
||||
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 {Presence}
|
||||
* @example
|
||||
* // Set the client user's status
|
||||
* client.user.setStatus('idle');
|
||||
*/
|
||||
setStatus(status, shardId) {
|
||||
return this.setPresence({ status, shardId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for setting an activity.
|
||||
* @typedef {Object} ActivityOptions
|
||||
* @property {string} [name] Name of the activity
|
||||
* @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 {Presence}
|
||||
* @example
|
||||
* // Set the client user's activity
|
||||
* client.user.setActivity('discord.js', { type: 'WATCHING' });
|
||||
*/
|
||||
setActivity(name, options = {}) {
|
||||
if (!name) return this.setPresence({ activities: [], shardId: options.shardId });
|
||||
|
||||
const activity = Object.assign({}, options, typeof name === 'object' ? name : { name });
|
||||
return this.setPresence({ activities: [activity], shardId: activity.shardId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets/removes the AFK flag for the client user.
|
||||
* @param {boolean} afk Whether or not the user is AFK
|
||||
* @param {number|number[]} [shardId] Shard Id(s) to have the AFK flag set on
|
||||
* @returns {Presence}
|
||||
*/
|
||||
setAFK(afk, shardId) {
|
||||
return this.setPresence({ afk, shardId });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ClientUser;
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
const BaseCommandInteraction = require('./BaseCommandInteraction');
|
||||
const CommandInteractionOptionResolver = require('./CommandInteractionOptionResolver');
|
||||
|
||||
/**
|
||||
* Represents a command interaction.
|
||||
* @extends {BaseCommandInteraction}
|
||||
*/
|
||||
class CommandInteraction extends BaseCommandInteraction {
|
||||
constructor(client, data) {
|
||||
super(client, data);
|
||||
|
||||
/**
|
||||
* The options passed to the command.
|
||||
* @type {CommandInteractionOptionResolver}
|
||||
*/
|
||||
this.options = new CommandInteractionOptionResolver(
|
||||
this.client,
|
||||
data.data.options?.map(option => this.transformOption(option, data.data.resolved)) ?? [],
|
||||
this.transformResolved(data.data.resolved ?? {}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CommandInteraction;
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
'use strict';
|
||||
|
||||
const { TypeError } = require('../errors');
|
||||
|
||||
/**
|
||||
* A resolver for command interaction options.
|
||||
*/
|
||||
class CommandInteractionOptionResolver {
|
||||
constructor(client, options, resolved) {
|
||||
/**
|
||||
* The client that instantiated this.
|
||||
* @name CommandInteractionOptionResolver#client
|
||||
* @type {Client}
|
||||
* @readonly
|
||||
*/
|
||||
Object.defineProperty(this, 'client', { value: client });
|
||||
|
||||
/**
|
||||
* The name of the subcommand group.
|
||||
* @type {?string}
|
||||
* @private
|
||||
*/
|
||||
this._group = null;
|
||||
|
||||
/**
|
||||
* The name of the subcommand.
|
||||
* @type {?string}
|
||||
* @private
|
||||
*/
|
||||
this._subcommand = null;
|
||||
|
||||
/**
|
||||
* The bottom-level options for the interaction.
|
||||
* If there is a subcommand (or subcommand and group), this is the options for the subcommand.
|
||||
* @type {CommandInteractionOption[]}
|
||||
* @private
|
||||
*/
|
||||
this._hoistedOptions = options;
|
||||
|
||||
// Hoist subcommand group if present
|
||||
if (this._hoistedOptions[0]?.type === 'SUB_COMMAND_GROUP') {
|
||||
this._group = this._hoistedOptions[0].name;
|
||||
this._hoistedOptions = this._hoistedOptions[0].options ?? [];
|
||||
}
|
||||
// Hoist subcommand if present
|
||||
if (this._hoistedOptions[0]?.type === 'SUB_COMMAND') {
|
||||
this._subcommand = this._hoistedOptions[0].name;
|
||||
this._hoistedOptions = this._hoistedOptions[0].options ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* The interaction options array.
|
||||
* @name CommandInteractionOptionResolver#data
|
||||
* @type {ReadonlyArray<CommandInteractionOption>}
|
||||
* @readonly
|
||||
*/
|
||||
Object.defineProperty(this, 'data', { value: Object.freeze([...options]) });
|
||||
|
||||
/**
|
||||
* The interaction resolved data
|
||||
* @name CommandInteractionOptionResolver#resolved
|
||||
* @type {Readonly<CommandInteractionResolvedData>}
|
||||
*/
|
||||
Object.defineProperty(this, 'resolved', { value: Object.freeze(resolved) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an option by its name.
|
||||
* @param {string} name The name of the option.
|
||||
* @param {boolean} [required=false] Whether to throw an error if the option is not found.
|
||||
* @returns {?CommandInteractionOption} The option, if found.
|
||||
*/
|
||||
get(name, required = false) {
|
||||
const option = this._hoistedOptions.find(opt => opt.name === name);
|
||||
if (!option) {
|
||||
if (required) {
|
||||
throw new TypeError('COMMAND_INTERACTION_OPTION_NOT_FOUND', name);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return option;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an option by name and property and checks its type.
|
||||
* @param {string} name The name of the option.
|
||||
* @param {ApplicationCommandOptionType} type The type of the option.
|
||||
* @param {string[]} properties The properties to check for for `required`.
|
||||
* @param {boolean} required Whether to throw an error if the option is not found.
|
||||
* @returns {?CommandInteractionOption} The option, if found.
|
||||
* @private
|
||||
*/
|
||||
_getTypedOption(name, type, properties, required) {
|
||||
const option = this.get(name, required);
|
||||
if (!option) {
|
||||
return null;
|
||||
} else if (option.type !== type) {
|
||||
throw new TypeError('COMMAND_INTERACTION_OPTION_TYPE', name, option.type, type);
|
||||
} else if (required && properties.every(prop => option[prop] === null || typeof option[prop] === 'undefined')) {
|
||||
throw new TypeError('COMMAND_INTERACTION_OPTION_EMPTY', name, option.type);
|
||||
}
|
||||
return option;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the selected subcommand.
|
||||
* @param {boolean} [required=true] Whether to throw an error if there is no subcommand.
|
||||
* @returns {?string} The name of the selected subcommand, or null if not set and not required.
|
||||
*/
|
||||
getSubcommand(required = true) {
|
||||
if (required && !this._subcommand) {
|
||||
throw new TypeError('COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND');
|
||||
}
|
||||
return this._subcommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the selected subcommand group.
|
||||
* @param {boolean} [required=true] Whether to throw an error if there is no subcommand group.
|
||||
* @returns {?string} The name of the selected subcommand group, or null if not set and not required.
|
||||
*/
|
||||
getSubcommandGroup(required = true) {
|
||||
if (required && !this._group) {
|
||||
throw new TypeError('COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND_GROUP');
|
||||
}
|
||||
return this._group;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a boolean option.
|
||||
* @param {string} name The name of the option.
|
||||
* @param {boolean} [required=false] Whether to throw an error if the option is not found.
|
||||
* @returns {?boolean} The value of the option, or null if not set and not required.
|
||||
*/
|
||||
getBoolean(name, required = false) {
|
||||
const option = this._getTypedOption(name, 'BOOLEAN', ['value'], required);
|
||||
return option?.value ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a channel option.
|
||||
* @param {string} name The name of the option.
|
||||
* @param {boolean} [required=false] Whether to throw an error if the option is not found.
|
||||
* @returns {?(GuildChannel|APIGuildChannel)}
|
||||
* The value of the option, or null if not set and not required.
|
||||
*/
|
||||
getChannel(name, required = false) {
|
||||
const option = this._getTypedOption(name, 'CHANNEL', ['channel'], required);
|
||||
return option?.channel ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a string option.
|
||||
* @param {string} name The name of the option.
|
||||
* @param {boolean} [required=false] Whether to throw an error if the option is not found.
|
||||
* @returns {?string} The value of the option, or null if not set and not required.
|
||||
*/
|
||||
getString(name, required = false) {
|
||||
const option = this._getTypedOption(name, 'STRING', ['value'], required);
|
||||
return option?.value ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an integer option.
|
||||
* @param {string} name The name of the option.
|
||||
* @param {boolean} [required=false] Whether to throw an error if the option is not found.
|
||||
* @returns {?number} The value of the option, or null if not set and not required.
|
||||
*/
|
||||
getInteger(name, required = false) {
|
||||
const option = this._getTypedOption(name, 'INTEGER', ['value'], required);
|
||||
return option?.value ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a number option.
|
||||
* @param {string} name The name of the option.
|
||||
* @param {boolean} [required=false] Whether to throw an error if the option is not found.
|
||||
* @returns {?number} The value of the option, or null if not set and not required.
|
||||
*/
|
||||
getNumber(name, required = false) {
|
||||
const option = this._getTypedOption(name, 'NUMBER', ['value'], required);
|
||||
return option?.value ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a user option.
|
||||
* @param {string} name The name of the option.
|
||||
* @param {boolean} [required=false] Whether to throw an error if the option is not found.
|
||||
* @returns {?User} The value of the option, or null if not set and not required.
|
||||
*/
|
||||
getUser(name, required = false) {
|
||||
const option = this._getTypedOption(name, 'USER', ['user'], required);
|
||||
return option?.user ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a member option.
|
||||
* @param {string} name The name of the option.
|
||||
* @param {boolean} [required=false] Whether to throw an error if the option is not found.
|
||||
* @returns {?(GuildMember|APIGuildMember)}
|
||||
* The value of the option, or null if not set and not required.
|
||||
*/
|
||||
getMember(name, required = false) {
|
||||
const option = this._getTypedOption(name, 'USER', ['member'], required);
|
||||
return option?.member ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a role option.
|
||||
* @param {string} name The name of the option.
|
||||
* @param {boolean} [required=false] Whether to throw an error if the option is not found.
|
||||
* @returns {?(Role|APIRole)} The value of the option, or null if not set and not required.
|
||||
*/
|
||||
getRole(name, required = false) {
|
||||
const option = this._getTypedOption(name, 'ROLE', ['role'], required);
|
||||
return option?.role ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a mentionable option.
|
||||
* @param {string} name The name of the option.
|
||||
* @param {boolean} [required=false] Whether to throw an error if the option is not found.
|
||||
* @returns {?(User|GuildMember|APIGuildMember|Role|APIRole)}
|
||||
* The value of the option, or null if not set and not required.
|
||||
*/
|
||||
getMentionable(name, required = false) {
|
||||
const option = this._getTypedOption(name, 'MENTIONABLE', ['user', 'member', 'role'], required);
|
||||
return option?.member ?? option?.user ?? option?.role ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a message option.
|
||||
* @param {string} name The name of the option.
|
||||
* @param {boolean} [required=false] Whether to throw an error if the option is not found.
|
||||
* @returns {?(Message|APIMessage)}
|
||||
* The value of the option, or null if not set and not required.
|
||||
*/
|
||||
getMessage(name, required = false) {
|
||||
const option = this._getTypedOption(name, '_MESSAGE', ['message'], required);
|
||||
return option?.message ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CommandInteractionOptionResolver;
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
'use strict';
|
||||
|
||||
const BaseCommandInteraction = require('./BaseCommandInteraction');
|
||||
const CommandInteractionOptionResolver = require('./CommandInteractionOptionResolver');
|
||||
const { ApplicationCommandOptionTypes, ApplicationCommandTypes } = require('../util/Constants');
|
||||
|
||||
/**
|
||||
* Represents a context menu interaction.
|
||||
* @extends {BaseCommandInteraction}
|
||||
*/
|
||||
class ContextMenuInteraction extends BaseCommandInteraction {
|
||||
constructor(client, data) {
|
||||
super(client, data);
|
||||
/**
|
||||
* The target of the interaction, parsed into options
|
||||
* @type {CommandInteractionOptionResolver}
|
||||
*/
|
||||
this.options = new CommandInteractionOptionResolver(
|
||||
this.client,
|
||||
this.resolveContextMenuOptions(data.data),
|
||||
this.transformResolved(data.data.resolved),
|
||||
);
|
||||
|
||||
/**
|
||||
* The id of the target of the interaction
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.targetId = data.data.target_id;
|
||||
|
||||
/**
|
||||
* The type of the target of the interaction; either USER or MESSAGE
|
||||
* @type {ApplicationCommandType}
|
||||
*/
|
||||
this.targetType = ApplicationCommandTypes[data.data.type];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves and transforms options received from the API for a context menu interaction.
|
||||
* @param {APIApplicationCommandInteractionData} data The interaction data
|
||||
* @returns {CommandInteractionOption[]}
|
||||
* @private
|
||||
*/
|
||||
resolveContextMenuOptions({ target_id, resolved }) {
|
||||
const result = [];
|
||||
|
||||
if (resolved.users?.[target_id]) {
|
||||
result.push(
|
||||
this.transformOption({ name: 'user', type: ApplicationCommandOptionTypes.USER, value: target_id }, resolved),
|
||||
);
|
||||
}
|
||||
|
||||
if (resolved.messages?.[target_id]) {
|
||||
result.push({
|
||||
name: 'message',
|
||||
type: '_MESSAGE',
|
||||
value: target_id,
|
||||
message: this.channel?.messages._add(resolved.messages[target_id]) ?? resolved.messages[target_id],
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ContextMenuInteraction;
|
||||
+99
@@ -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 {APIChannel} 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);
|
||||
}
|
||||
|
||||
_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 channel's last message id, 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=true] Whether to skip the cache check and request the API
|
||||
* @returns {Promise<DMChannel>}
|
||||
*/
|
||||
fetch(force = true) {
|
||||
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() {}
|
||||
sendTyping() {}
|
||||
createMessageCollector() {}
|
||||
awaitMessages() {}
|
||||
createMessageComponentCollector() {}
|
||||
awaitMessageComponent() {}
|
||||
// Doesn't work on DM channels; bulkDelete() {}
|
||||
}
|
||||
|
||||
TextBasedChannel.applyToClass(DMChannel, true, ['bulkDelete']);
|
||||
|
||||
module.exports = DMChannel;
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
'use strict';
|
||||
|
||||
const Base = require('./Base');
|
||||
const SnowflakeUtil = require('../util/SnowflakeUtil');
|
||||
|
||||
/**
|
||||
* Represents raw emoji data from the API
|
||||
* @typedef {APIEmoji} RawEmoji
|
||||
* @property {?Snowflake} id The emoji's id
|
||||
* @property {?string} name The emoji's name
|
||||
* @property {?boolean} animated Whether the emoji is animated
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents an emoji, see {@link GuildEmoji} and {@link ReactionEmoji}.
|
||||
* @extends {Base}
|
||||
*/
|
||||
class Emoji extends Base {
|
||||
constructor(client, emoji) {
|
||||
super(client);
|
||||
/**
|
||||
* Whether or not the emoji is animated
|
||||
* @type {?boolean}
|
||||
*/
|
||||
this.animated = emoji.animated ?? null;
|
||||
|
||||
/**
|
||||
* The emoji's name
|
||||
* @type {?string}
|
||||
*/
|
||||
this.name = emoji.name ?? null;
|
||||
|
||||
/**
|
||||
* The emoji's id
|
||||
* @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() {
|
||||
return this.id && 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() {
|
||||
return this.id && SnowflakeUtil.deconstruct(this.id).timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* The time the emoji was created at, or null if unicode
|
||||
* @type {?Date}
|
||||
* @readonly
|
||||
*/
|
||||
get createdAt() {
|
||||
return this.id && 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.channel.send(`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;
|
||||
|
||||
/**
|
||||
* @external APIEmoji
|
||||
* @see {@link https://discord.com/developers/docs/resources/emoji#emoji-object}
|
||||
*/
|
||||
+1338
File diff suppressed because it is too large.
Load diff
+610
@@ -0,0 +1,610 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const Integration = require('./Integration');
|
||||
const StageInstance = require('./StageInstance');
|
||||
const Sticker = require('./Sticker');
|
||||
const Webhook = require('./Webhook');
|
||||
const { OverwriteTypes, PartialTypes } = require('../util/Constants');
|
||||
const Permissions = require('../util/Permissions');
|
||||
const SnowflakeUtil = require('../util/SnowflakeUtil');
|
||||
const Util = require('../util/Util');
|
||||
|
||||
/**
|
||||
* The target type of an entry. Here are the available types:
|
||||
* * GUILD
|
||||
* * CHANNEL
|
||||
* * USER
|
||||
* * ROLE
|
||||
* * INVITE
|
||||
* * WEBHOOK
|
||||
* * EMOJI
|
||||
* * MESSAGE
|
||||
* * INTEGRATION
|
||||
* * STAGE_INSTANCE
|
||||
* * STICKER
|
||||
* * THREAD
|
||||
* @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',
|
||||
STAGE_INSTANCE: 'STAGE_INSTANCE',
|
||||
STICKER: 'STICKER',
|
||||
THREAD: 'THREAD',
|
||||
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
|
||||
* * STAGE_INSTANCE_CREATE: 83
|
||||
* * STAGE_INSTANCE_UPDATE: 84
|
||||
* * STAGE_INSTANCE_DELETE: 85
|
||||
* * STICKER_CREATE: 90
|
||||
* * STICKER_UPDATE: 91
|
||||
* * STICKER_DELETE: 92
|
||||
* * THREAD_CREATE: 110
|
||||
* * THREAD_UPDATE: 111
|
||||
* * THREAD_DELETE: 112
|
||||
* @typedef {?(number|string)} AuditLogAction
|
||||
* @see {@link https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-audit-log-events}
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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,
|
||||
STAGE_INSTANCE_CREATE: 83,
|
||||
STAGE_INSTANCE_UPDATE: 84,
|
||||
STAGE_INSTANCE_DELETE: 85,
|
||||
STICKER_CREATE: 90,
|
||||
STICKER_UPDATE: 91,
|
||||
STICKER_DELETE: 92,
|
||||
THREAD_CREATE: 110,
|
||||
THREAD_UPDATE: 111,
|
||||
THREAD_DELETE: 112,
|
||||
};
|
||||
|
||||
/**
|
||||
* 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);
|
||||
if (data.threads) for (const thread of data.threads) guild.client.channels._add(thread, guild);
|
||||
/**
|
||||
* 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 async build(...args) {
|
||||
const logs = new GuildAuditLogs(...args);
|
||||
await Promise.all(logs.entries.map(e => e.target));
|
||||
return logs;
|
||||
}
|
||||
|
||||
/**
|
||||
* The target of an entry. It can be one of:
|
||||
* * A guild
|
||||
* * A channel
|
||||
* * A user
|
||||
* * A role
|
||||
* * An invite
|
||||
* * A webhook
|
||||
* * An emoji
|
||||
* * A message
|
||||
* * An integration
|
||||
* * A stage instance
|
||||
* * A sticker
|
||||
* * A thread
|
||||
* * 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|Channel|User|Role|Invite|Webhook|GuildEmoji|Message|Integration|StageInstance|Sticker)}
|
||||
* 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 < 83) return Targets.INTEGRATION;
|
||||
if (target < 86) return Targets.STAGE_INSTANCE;
|
||||
if (target < 100) return Targets.STICKER;
|
||||
if (target < 110) return Targets.UNKNOWN;
|
||||
if (target < 120) return Targets.THREAD;
|
||||
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,
|
||||
Actions.STAGE_INSTANCE_CREATE,
|
||||
Actions.STICKER_CREATE,
|
||||
Actions.THREAD_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,
|
||||
Actions.STAGE_INSTANCE_DELETE,
|
||||
Actions.STICKER_DELETE,
|
||||
Actions.THREAD_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,
|
||||
Actions.STAGE_INSTANCE_UPDATE,
|
||||
Actions.STICKER_UPDATE,
|
||||
Actions.THREAD_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 = data.user_id
|
||||
? guild.client.options.partials.includes(PartialTypes.USER)
|
||||
? guild.client.users._add({ id: data.user_id })
|
||||
: guild.client.users.cache.get(data.user_id)
|
||||
: null;
|
||||
|
||||
/**
|
||||
* 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?.map(c => ({ key: c.key, old: c.old_value, new: c.new_value })) ?? null;
|
||||
|
||||
/**
|
||||
* The entry's id
|
||||
* @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 (Number(data.options.type)) {
|
||||
case OverwriteTypes.role:
|
||||
this.extra = guild.roles.cache.get(data.options.id) ?? {
|
||||
id: data.options.id,
|
||||
name: data.options.role_name,
|
||||
type: OverwriteTypes[OverwriteTypes.role],
|
||||
};
|
||||
break;
|
||||
|
||||
case OverwriteTypes.member:
|
||||
this.extra = guild.members.cache.get(data.options.id) ?? {
|
||||
id: data.options.id,
|
||||
type: OverwriteTypes[OverwriteTypes.member],
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case Actions.STAGE_INSTANCE_CREATE:
|
||||
case Actions.STAGE_INSTANCE_DELETE:
|
||||
case Actions.STAGE_INSTANCE_UPDATE:
|
||||
this.extra = {
|
||||
channel: guild.client.channels.cache.get(data.options?.channel_id) ?? { id: data.options?.channel_id },
|
||||
};
|
||||
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(async me => {
|
||||
if (me.permissions.has(Permissions.FLAGS.MANAGE_GUILD)) {
|
||||
let change = this.changes.find(c => c.key === 'code');
|
||||
change = change.new ?? change.old;
|
||||
const invites = await guild.invites.fetch();
|
||||
this.target = invites.find(i => i.code === change) ?? null;
|
||||
} else {
|
||||
this.target = this.changes.reduce((o, c) => {
|
||||
o[c.key] = c.new ?? c.old;
|
||||
return o;
|
||||
}, {});
|
||||
}
|
||||
});
|
||||
} 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 (targetType === Targets.CHANNEL || targetType === Targets.THREAD) {
|
||||
this.target =
|
||||
guild.channels.cache.get(data.target_id) ??
|
||||
this.changes.reduce(
|
||||
(o, c) => {
|
||||
o[c.key] = c.new ?? c.old;
|
||||
return o;
|
||||
},
|
||||
{ id: data.target_id },
|
||||
);
|
||||
} else if (targetType === Targets.STAGE_INSTANCE) {
|
||||
this.target =
|
||||
guild.stageInstances.cache.get(data.target_id) ??
|
||||
new StageInstance(
|
||||
guild.client,
|
||||
this.changes.reduce(
|
||||
(o, c) => {
|
||||
o[c.key] = c.new ?? c.old;
|
||||
return o;
|
||||
},
|
||||
{
|
||||
id: data.target_id,
|
||||
channel_id: data.options?.channel_id,
|
||||
guild_id: guild.id,
|
||||
},
|
||||
),
|
||||
);
|
||||
} else if (targetType === Targets.STICKER) {
|
||||
this.target =
|
||||
guild.stickers.cache.get(data.target_id) ??
|
||||
new Sticker(
|
||||
guild.client,
|
||||
this.changes.reduce(
|
||||
(o, c) => {
|
||||
o[c.key] = c.new ?? c.old;
|
||||
return o;
|
||||
},
|
||||
{ id: data.target_id },
|
||||
),
|
||||
);
|
||||
} 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 SnowflakeUtil.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;
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
'use strict';
|
||||
|
||||
const Base = require('./Base');
|
||||
|
||||
/**
|
||||
* Represents a ban in a guild on Discord.
|
||||
* @extends {Base}
|
||||
*/
|
||||
class GuildBan extends Base {
|
||||
/**
|
||||
* @param {Client} client The instantiating client
|
||||
* @param {APIBanData} data The data for the ban
|
||||
* @param {Guild} guild The guild in which the ban is
|
||||
*/
|
||||
constructor(client, data, guild) {
|
||||
super(client);
|
||||
|
||||
/**
|
||||
* The guild in which the ban is
|
||||
* @type {Guild}
|
||||
*/
|
||||
this.guild = guild;
|
||||
|
||||
this._patch(data);
|
||||
}
|
||||
|
||||
_patch(data) {
|
||||
/**
|
||||
* The user this ban applies to
|
||||
* @type {User}
|
||||
*/
|
||||
this.user = this.client.users._add(data.user, true);
|
||||
|
||||
if ('reason' in data) {
|
||||
/**
|
||||
* The reason for the ban
|
||||
* @type {?string}
|
||||
*/
|
||||
this.reason = data.reason;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this GuildBan is partial. If the reason is not provided the value is null
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get partial() {
|
||||
return !('reason' in this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches this GuildBan.
|
||||
* @param {boolean} [force=true] Whether to skip the cache check and request the API
|
||||
* @returns {Promise<GuildBan>}
|
||||
*/
|
||||
fetch(force = true) {
|
||||
return this.guild.bans.fetch({ user: this.user, cache: true, force });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GuildBan;
|
||||
|
||||
/**
|
||||
* @external APIBanData
|
||||
* @see {@link https://discord.com/developers/docs/resources/guild#create-guild-ban-json-params}
|
||||
*/
|
||||
+552
@@ -0,0 +1,552 @@
|
||||
'use strict';
|
||||
|
||||
const Channel = require('./Channel');
|
||||
const PermissionOverwrites = require('./PermissionOverwrites');
|
||||
const { Error } = require('../errors');
|
||||
const PermissionOverwriteManager = require('../managers/PermissionOverwriteManager');
|
||||
const { ChannelTypes, VoiceBasedChannelTypes } = require('../util/Constants');
|
||||
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}
|
||||
* - {@link StageChannel}
|
||||
* @extends {Channel}
|
||||
* @abstract
|
||||
*/
|
||||
class GuildChannel extends Channel {
|
||||
/**
|
||||
* @param {Guild} guild The guild the guild channel is part of
|
||||
* @param {APIChannel} data The data for the guild channel
|
||||
* @param {Client} [client] A safety parameter for the client that instantiated this
|
||||
* @param {boolean} [immediatePatch=true] Control variable for patching
|
||||
*/
|
||||
constructor(guild, data, client, immediatePatch = true) {
|
||||
super(guild?.client ?? client, data, false);
|
||||
|
||||
/**
|
||||
* The guild the channel is in
|
||||
* @type {Guild}
|
||||
*/
|
||||
this.guild = guild;
|
||||
|
||||
/**
|
||||
* The id of the guild the channel is in
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.guildId = guild?.id ?? data.guild_id;
|
||||
|
||||
this.parentId = this.parentId ?? null;
|
||||
/**
|
||||
* A manager of permission overwrites that belong to this channel
|
||||
* @type {PermissionOverwriteManager}
|
||||
*/
|
||||
this.permissionOverwrites = new PermissionOverwriteManager(this);
|
||||
|
||||
if (data && immediatePatch) this._patch(data);
|
||||
}
|
||||
|
||||
_patch(data) {
|
||||
super._patch(data);
|
||||
|
||||
if ('name' in data) {
|
||||
/**
|
||||
* The name of the guild channel
|
||||
* @type {string}
|
||||
*/
|
||||
this.name = data.name;
|
||||
}
|
||||
|
||||
if ('position' in data) {
|
||||
/**
|
||||
* The raw position of the channel from discord
|
||||
* @type {number}
|
||||
*/
|
||||
this.rawPosition = data.position;
|
||||
}
|
||||
|
||||
if ('guild_id' in data) {
|
||||
this.guildId = data.guild_id;
|
||||
}
|
||||
|
||||
if ('parent_id' in data) {
|
||||
/**
|
||||
* The id of the category parent of this channel
|
||||
* @type {?Snowflake}
|
||||
*/
|
||||
this.parentId = data.parent_id;
|
||||
}
|
||||
|
||||
if ('permission_overwrites' in data) {
|
||||
this.permissionOverwrites.cache.clear();
|
||||
for (const overwrite of data.permission_overwrites) {
|
||||
this.permissionOverwrites._add(overwrite);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_clone() {
|
||||
const clone = super._clone();
|
||||
clone.permissionOverwrites = new PermissionOverwriteManager(clone, this.permissionOverwrites.cache.values());
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* The category parent of this channel
|
||||
* @type {?CategoryChannel}
|
||||
* @readonly
|
||||
*/
|
||||
get parent() {
|
||||
return this.guild.channels.resolve(this.parentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the permissionOverwrites match the parent channel, null if no parent
|
||||
* @type {?boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get permissionsLocked() {
|
||||
if (!this.parent) return null;
|
||||
|
||||
// Get all overwrites
|
||||
const overwriteIds = new Set([
|
||||
...this.permissionOverwrites.cache.keys(),
|
||||
...this.parent.permissionOverwrites.cache.keys(),
|
||||
]);
|
||||
|
||||
// Compare all overwrites
|
||||
return [...overwriteIds].every(key => {
|
||||
const channelVal = this.permissionOverwrites.cache.get(key);
|
||||
const parentVal = this.parent.permissionOverwrites.cache.get(key);
|
||||
|
||||
// Handle empty overwrite
|
||||
if (
|
||||
(!channelVal &&
|
||||
parentVal.deny.bitfield === Permissions.defaultBit &&
|
||||
parentVal.allow.bitfield === Permissions.defaultBit) ||
|
||||
(!parentVal &&
|
||||
channelVal.deny.bitfield === Permissions.defaultBit &&
|
||||
channelVal.allow.bitfield === Permissions.defaultBit)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Compare overwrites
|
||||
return (
|
||||
typeof channelVal !== 'undefined' &&
|
||||
typeof parentVal !== 'undefined' &&
|
||||
channelVal.deny.bitfield === parentVal.deny.bitfield &&
|
||||
channelVal.allow.bitfield === parentVal.allow.bitfield
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The position of the channel
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get position() {
|
||||
const sorted = this.guild._sortedChannels(this);
|
||||
return [...sorted.values()].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);
|
||||
return role && this.rolePermissions(role);
|
||||
}
|
||||
|
||||
overwritesFor(member, verified = false, roles = null) {
|
||||
if (!verified) member = this.guild.members.resolve(member);
|
||||
if (!member) return [];
|
||||
|
||||
if (!roles) roles = member.roles.cache;
|
||||
const roleOverwrites = [];
|
||||
let memberOverwrites;
|
||||
let everyoneOverwrites;
|
||||
|
||||
for (const overwrite of this.permissionOverwrites.cache.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?.deny ?? Permissions.defaultBit)
|
||||
.add(overwrites.everyone?.allow ?? Permissions.defaultBit)
|
||||
.remove(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.deny) : Permissions.defaultBit)
|
||||
.add(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.allow) : Permissions.defaultBit)
|
||||
.remove(overwrites.member?.deny ?? Permissions.defaultBit)
|
||||
.add(overwrites.member?.allow ?? Permissions.defaultBit)
|
||||
.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.cache.get(this.guild.id);
|
||||
const roleOverwrites = this.permissionOverwrites.cache.get(role.id);
|
||||
|
||||
return role.permissions
|
||||
.remove(everyoneOverwrites?.deny ?? Permissions.defaultBit)
|
||||
.add(everyoneOverwrites?.allow ?? Permissions.defaultBit)
|
||||
.remove(roleOverwrites?.deny ?? Permissions.defaultBit)
|
||||
.add(roleOverwrites?.allow ?? Permissions.defaultBit)
|
||||
.freeze();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.cache.map(overwrite => overwrite.toJSON());
|
||||
return this.edit({ permissionOverwrites });
|
||||
}
|
||||
|
||||
/**
|
||||
* A collection of cached members of this channel, mapped by their ids.
|
||||
* Members that can view this channel, if the channel is text based.
|
||||
* Members in the channel, if the channel is voice based.
|
||||
* @type {Collection<Snowflake, GuildMember>}
|
||||
* @readonly
|
||||
*/
|
||||
get members() {
|
||||
return this.guild.members.cache.filter(m => this.permissionsFor(m).has(Permissions.FLAGS.VIEW_CHANNEL, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* The data for a guild channel.
|
||||
* @typedef {Object} ChannelData
|
||||
* @property {string} [name] The name of the channel
|
||||
* @property {ChannelType} [type] The type of the the channel (only conversion between text and news is supported)
|
||||
* @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 {?CategoryChannelResolvable} [parent] The parent 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
|
||||
* @property {ThreadAutoArchiveDuration} [defaultAutoArchiveDuration]
|
||||
* The default auto archive duration for all new threads in this channel
|
||||
* @property {?string} [rtcRegion] The RTC region of the channel
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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 (data.parent) data.parent = this.client.channels.resolveId(data.parent);
|
||||
|
||||
if (typeof data.position !== 'undefined') {
|
||||
const updatedChannels = await Util.setPosition(
|
||||
this,
|
||||
data.position,
|
||||
false,
|
||||
this.guild._sortedChannels(this),
|
||||
this.client.api.guilds(this.guild.id).channels,
|
||||
reason,
|
||||
);
|
||||
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.parent) {
|
||||
const newParent = this.guild.channels.resolve(data.parent);
|
||||
if (newParent?.type === 'GUILD_CATEGORY') {
|
||||
permission_overwrites = newParent.permissionOverwrites.cache.map(o =>
|
||||
PermissionOverwrites.resolve(o, this.guild),
|
||||
);
|
||||
}
|
||||
} else if (this.parent) {
|
||||
permission_overwrites = this.parent.permissionOverwrites.cache.map(o =>
|
||||
PermissionOverwrites.resolve(o, this.guild),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const newData = await this.client.api.channels(this.id).patch({
|
||||
data: {
|
||||
name: (data.name ?? this.name).trim(),
|
||||
type: ChannelTypes[data.type],
|
||||
topic: data.topic,
|
||||
nsfw: data.nsfw,
|
||||
bitrate: data.bitrate ?? this.bitrate,
|
||||
user_limit: data.userLimit ?? this.userLimit,
|
||||
rtc_region: data.rtcRegion ?? this.rtcRegion,
|
||||
parent_id: data.parent,
|
||||
lock_permissions: data.lockPermissions,
|
||||
rate_limit_per_user: data.rateLimitPerUser,
|
||||
default_auto_archive_duration: data.defaultAutoArchiveDuration,
|
||||
permission_overwrites,
|
||||
},
|
||||
reason,
|
||||
});
|
||||
|
||||
return this.client.actions.ChannelUpdate.handle(newData).updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Options used to set parent of a channel.
|
||||
* @typedef {Object} SetParentOptions
|
||||
* @property {boolean} [lockPermissions=true] Whether to lock the permissions to what the parent's permissions are
|
||||
* @property {string} [reason] The reason for modifying the parent of the channel
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sets the parent of this channel.
|
||||
* @param {?CategoryChannelResolvable} channel The category channel to set as the parent
|
||||
* @param {SetParentOptions} [options={}] The options for setting the parent
|
||||
* @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(
|
||||
{
|
||||
parent: channel ?? null,
|
||||
lockPermissions,
|
||||
},
|
||||
reason,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Options used to set position of a channel.
|
||||
* @typedef {Object} SetChannelPositionOptions
|
||||
* @param {boolean} [relative=false] Whether or not to change the position relative to its current value
|
||||
* @param {string} [reason] The reason for changing the position
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sets a new position for the guild channel.
|
||||
* @param {number} position The new position for the guild channel
|
||||
* @param {SetChannelPositionOptions} [options] Options for setting 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);
|
||||
*/
|
||||
async setPosition(position, { relative, reason } = {}) {
|
||||
const updatedChannels = await Util.setPosition(
|
||||
this,
|
||||
position,
|
||||
relative,
|
||||
this.guild._sortedChannels(this),
|
||||
this.client.api.guilds(this.guild.id).channels,
|
||||
reason,
|
||||
);
|
||||
this.client.actions.GuildChannelsPositionUpdate.handle({
|
||||
guild_id: this.guild.id,
|
||||
channels: updatedChannels,
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data that can be resolved to an Application. This can be:
|
||||
* * An Application
|
||||
* * An Activity with associated Application
|
||||
* * A Snowflake
|
||||
* @typedef {Application|Snowflake} ApplicationResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options used to clone a guild channel.
|
||||
* @typedef {GuildChannelCreateOptions} GuildChannelCloneOptions
|
||||
* @property {string} [name=this.name] Name of the new channel
|
||||
*/
|
||||
|
||||
/**
|
||||
* Clones this channel.
|
||||
* @param {GuildChannelCloneOptions} [options] The options for cloning this channel
|
||||
* @returns {Promise<GuildChannel>}
|
||||
*/
|
||||
clone(options = {}) {
|
||||
return this.guild.channels.create(options.name ?? this.name, {
|
||||
permissionOverwrites: this.permissionOverwrites.cache,
|
||||
topic: this.topic,
|
||||
type: this.type,
|
||||
nsfw: this.nsfw,
|
||||
parent: this.parent,
|
||||
bitrate: this.bitrate,
|
||||
userLimit: this.userLimit,
|
||||
rateLimitPerUser: this.rateLimitPerUser,
|
||||
position: this.position,
|
||||
reason: null,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.cache.equals(channel.permissionOverwrites.cache);
|
||||
} 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) &&
|
||||
this.guild.rulesChannelId !== this.id &&
|
||||
this.guild.publicUpdatesChannelId !== this.id
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (VoiceBasedChannelTypes.includes(this.type)) {
|
||||
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);
|
||||
*/
|
||||
async delete(reason) {
|
||||
await this.client.api.channels(this.id).delete({ reason });
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GuildChannel;
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
'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 {APIEmoji} 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;
|
||||
|
||||
/**
|
||||
* Array of role ids this emoji is active for
|
||||
* @name GuildEmoji#_roles
|
||||
* @type {Snowflake[]}
|
||||
* @private
|
||||
*/
|
||||
Object.defineProperty(this, '_roles', { value: [], writable: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (data.user) this.author = this.client.users._add(data.user);
|
||||
if (data.roles) this._roles = data.roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS_AND_STICKERS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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_AND_STICKERS)) {
|
||||
throw new Error('MISSING_MANAGE_EMOJIS_AND_STICKERS_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);
|
||||
*/
|
||||
async edit(data, reason) {
|
||||
const roles = data.roles?.map(r => r.id ?? r);
|
||||
const newData = await this.client.api
|
||||
.guilds(this.guild.id)
|
||||
.emojis(this.id)
|
||||
.patch({
|
||||
data: {
|
||||
name: data.name,
|
||||
roles,
|
||||
},
|
||||
reason,
|
||||
});
|
||||
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>}
|
||||
*/
|
||||
async delete(reason) {
|
||||
await this.client.api.guilds(this.guild.id).emojis(this.id).delete({ reason });
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this emoji is the same as another one.
|
||||
* @param {GuildEmoji|APIEmoji} other The emoji to compare it to
|
||||
* @returns {boolean}
|
||||
*/
|
||||
equals(other) {
|
||||
if (other instanceof GuildEmoji) {
|
||||
return (
|
||||
other.id === this.id &&
|
||||
other.name === this.name &&
|
||||
other.managed === this.managed &&
|
||||
other.available === this.available &&
|
||||
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;
|
||||
+364
@@ -0,0 +1,364 @@
|
||||
'use strict';
|
||||
|
||||
const Base = require('./Base');
|
||||
const VoiceState = require('./VoiceState');
|
||||
const TextBasedChannel = require('./interfaces/TextBasedChannel');
|
||||
const { Error } = require('../errors');
|
||||
const GuildMemberRoleManager = require('../managers/GuildMemberRoleManager');
|
||||
const Permissions = require('../util/Permissions');
|
||||
|
||||
/**
|
||||
* Represents a member of a guild on Discord.
|
||||
* @implements {TextBasedChannel}
|
||||
* @extends {Base}
|
||||
*/
|
||||
class GuildMember extends Base {
|
||||
/**
|
||||
* @param {Client} client The instantiating client
|
||||
* @param {APIGuildMember} 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 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;
|
||||
|
||||
/**
|
||||
* Whether this member has yet to pass the guild's membership gate
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.pending = false;
|
||||
|
||||
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 = data.premium_since ? new Date(data.premium_since).getTime() : null;
|
||||
}
|
||||
if ('roles' in data) this._roles = data.roles;
|
||||
this.pending = data.pending ?? false;
|
||||
}
|
||||
|
||||
_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 voice state of this member
|
||||
* @type {VoiceState}
|
||||
* @readonly
|
||||
*/
|
||||
get voice() {
|
||||
return this.guild.voiceStates.cache.get(this.id) ?? new VoiceState(this.guild, { user_id: this.id });
|
||||
}
|
||||
|
||||
/**
|
||||
* The time this member joined the guild
|
||||
* @type {?Date}
|
||||
* @readonly
|
||||
*/
|
||||
get joinedAt() {
|
||||
return this.joinedTimestamp ? new Date(this.joinedTimestamp) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The time of when the member used their Nitro boost on the guild, if it was used
|
||||
* @type {?Date}
|
||||
* @readonly
|
||||
*/
|
||||
get premiumSince() {
|
||||
return this.premiumSinceTimestamp ? new Date(this.premiumSinceTimestamp) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The presence of this guild member
|
||||
* @type {?Presence}
|
||||
* @readonly
|
||||
*/
|
||||
get presence() {
|
||||
return this.guild.presences.resolve(this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* The displayed color of this member in base 10
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get displayColor() {
|
||||
return this.roles.color?.color ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* The displayed color of this member in hexadecimal
|
||||
* @type {string}
|
||||
* @readonly
|
||||
*/
|
||||
get displayHexColor() {
|
||||
return this.roles.color?.hexColor ?? '#000000';
|
||||
}
|
||||
|
||||
/**
|
||||
* The member's id
|
||||
* @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 and owner status 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 {GuildChannelResolvable} 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.permissionsFor(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {GuildVoiceChannelResolvable|null} [channel] Channel to move the member to
|
||||
* (if they are connected to voice), or `null` if you want to disconnect 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>}
|
||||
*/
|
||||
edit(data, reason) {
|
||||
return this.guild.members.edit(this, data, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the nickname for this member.
|
||||
* @param {?string} nick The nickname for the guild member, or `null` if you want to reset their nickname
|
||||
* @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.guild.members.kick(this, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bans this guild member.
|
||||
* @param {BanOptions} [options] Options for the ban
|
||||
* @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=true] Whether to skip the cache check and request the API
|
||||
* @returns {Promise<GuildMember>}
|
||||
*/
|
||||
fetch(force = true) {
|
||||
return this.guild.members.fetch({ user: this.id, cache: true, force });
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this guild member equals another guild member. It compares all properties, so for most
|
||||
* comparison it is advisable to just compare `member.id === member2.id` as it is significantly faster
|
||||
* and is often what most users need.
|
||||
* @param {GuildMember} member The member to compare with
|
||||
* @returns {boolean}
|
||||
*/
|
||||
equals(member) {
|
||||
return (
|
||||
member instanceof this.constructor &&
|
||||
this.id === member.id &&
|
||||
this.partial === member.partial &&
|
||||
this.guild.id === member.guild.id &&
|
||||
this.joinedTimestamp === member.joinedTimestamp &&
|
||||
this.nickname === member.nickname &&
|
||||
this.pending === member.pending &&
|
||||
(this._roles === member._roles ||
|
||||
(this._roles.length === member._roles.length && this._roles.every((role, i) => role === member._roles[i])))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
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;
|
||||
|
||||
/**
|
||||
* @external APIGuildMember
|
||||
* @see {@link https://discord.com/developers/docs/resources/guild#guild-member-object}
|
||||
*/
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const Base = require('./Base');
|
||||
const GuildPreviewEmoji = require('./GuildPreviewEmoji');
|
||||
const SnowflakeUtil = require('../util/SnowflakeUtil');
|
||||
|
||||
/**
|
||||
* 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 timestamp this guild was created at
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get createdTimestamp() {
|
||||
return SnowflakeUtil.deconstruct(this.id).timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* The time this guild was created at
|
||||
* @type {Date}
|
||||
* @readonly
|
||||
*/
|
||||
get createdAt() {
|
||||
return new Date(this.createdTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* The URL to this guild's splash.
|
||||
* @param {StaticImageURLOptions} [options={}] Options for the Image URL
|
||||
* @returns {?string}
|
||||
*/
|
||||
splashURL({ format, size } = {}) {
|
||||
return this.splash && this.client.rest.cdn.Splash(this.id, this.splash, format, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* The URL to this guild's discovery splash.
|
||||
* @param {StaticImageURLOptions} [options={}] Options for the Image URL
|
||||
* @returns {?string}
|
||||
*/
|
||||
discoverySplashURL({ format, size } = {}) {
|
||||
return this.discoverySplash && 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 } = {}) {
|
||||
return this.icon && this.client.rest.cdn.Icon(this.id, this.icon, format, size, dynamic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches this guild.
|
||||
* @returns {Promise<GuildPreview>}
|
||||
*/
|
||||
async fetch() {
|
||||
const data = await this.client.api.guilds(this.id).preview.get();
|
||||
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;
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
'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
|
||||
*/
|
||||
|
||||
constructor(client, data, guild) {
|
||||
super(client, data, guild);
|
||||
|
||||
/**
|
||||
* The roles this emoji is active for
|
||||
* @type {Snowflake[]}
|
||||
*/
|
||||
this.roles = data.roles;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GuildPreviewEmoji;
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
'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 {APIGuildTemplate} 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 {APIGuildTemplate} 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 {APIGuild}
|
||||
*/
|
||||
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),
|
||||
},
|
||||
});
|
||||
|
||||
if (client.guilds.cache.has(data.id)) return client.guilds.cache.get(data.id);
|
||||
|
||||
return new Promise(resolve => {
|
||||
const resolveGuild = guild => {
|
||||
client.off(Events.GUILD_CREATE, handleGuild);
|
||||
client.decrementMaxListeners();
|
||||
resolve(guild);
|
||||
};
|
||||
|
||||
const handleGuild = guild => {
|
||||
if (guild.id === data.id) {
|
||||
clearTimeout(timeout);
|
||||
resolveGuild(guild);
|
||||
}
|
||||
};
|
||||
|
||||
client.incrementMaxListeners();
|
||||
client.on(Events.GUILD_CREATE, handleGuild);
|
||||
|
||||
const timeout = setTimeout(() => resolveGuild(client.guilds._add(data)), 10000).unref();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Options used to edit a guild template.
|
||||
* @typedef {Object} EditGuildTemplateOptions
|
||||
* @property {string} [name] The name of this template
|
||||
* @property {string} [description] The description of this template
|
||||
*/
|
||||
|
||||
/**
|
||||
* Updates the metadata of this template.
|
||||
* @param {EditGuildTemplateOptions} [options] Options for editing the template
|
||||
* @returns {Promise<GuildTemplate>}
|
||||
*/
|
||||
async edit({ name, description } = {}) {
|
||||
const data = await this.client.api.guilds(this.guildId).templates(this.code).patch({ data: { name, description } });
|
||||
return this._patch(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes this template.
|
||||
* @returns {Promise<GuildTemplate>}
|
||||
*/
|
||||
async delete() {
|
||||
await this.client.api.guilds(this.guildId).templates(this.code).delete();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs this template to the current state of the guild.
|
||||
* @returns {Promise<GuildTemplate>}
|
||||
*/
|
||||
async sync() {
|
||||
const data = await this.client.api.guilds(this.guildId).templates(this.code).put();
|
||||
return 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.resolve(this.guildId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 template's code instead of the template object.
|
||||
* @returns {string}
|
||||
* @example
|
||||
* // Logs: Template: FKvmczH2HyUf
|
||||
* console.log(`Template: ${guildTemplate}!`);
|
||||
*/
|
||||
toString() {
|
||||
return this.code;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Regular expression that globally matches guild template links
|
||||
* @type {RegExp}
|
||||
*/
|
||||
GuildTemplate.GUILD_TEMPLATES_PATTERN = /discord(?:app)?\.(?:com\/template|new)\/([\w-]{2,255})/gi;
|
||||
|
||||
module.exports = GuildTemplate;
|
||||
|
||||
/* eslint-disable max-len */
|
||||
/**
|
||||
* @external APIGuildTemplate
|
||||
* @see {@link https://discord.com/developers/docs/resources/guild-template#guild-template-object-guild-template-structure}
|
||||
*/
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
'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);
|
||||
}
|
||||
|
||||
/**
|
||||
* All roles that are managed by this integration
|
||||
* @type {Collection<Snowflake, Role>}
|
||||
* @readonly
|
||||
*/
|
||||
get roles() {
|
||||
const roles = this.guild.roles.cache;
|
||||
return roles.filter(role => role.tags?.integrationId === this.id);
|
||||
}
|
||||
|
||||
_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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes this integration.
|
||||
* @returns {Promise<Integration>}
|
||||
* @param {string} [reason] Reason for deleting this integration
|
||||
*/
|
||||
async delete(reason) {
|
||||
await this.client.api.guilds(this.guild.id).integrations(this.id).delete({ reason });
|
||||
return this;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return super.toJSON({
|
||||
role: 'roleId',
|
||||
guild: 'guildId',
|
||||
user: 'userId',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Integration;
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
'use strict';
|
||||
|
||||
const Application = require('./interfaces/Application');
|
||||
|
||||
/**
|
||||
* Represents an Integration's OAuth2 Application.
|
||||
* @extends {Application}
|
||||
*/
|
||||
class IntegrationApplication extends Application {
|
||||
_patch(data) {
|
||||
super._patch(data);
|
||||
|
||||
/**
|
||||
* The bot user for this application
|
||||
* @type {?User}
|
||||
*/
|
||||
this.bot = data.bot ? this.client.users._add(data.bot) : this.bot ?? null;
|
||||
|
||||
/**
|
||||
* The url of the application's terms of service
|
||||
* @type {?string}
|
||||
*/
|
||||
this.termsOfServiceURL = data.terms_of_service_url ?? this.termsOfServiceURL ?? null;
|
||||
|
||||
/**
|
||||
* The url of the application's privacy policy
|
||||
* @type {?string}
|
||||
*/
|
||||
this.privacyPolicyURL = data.privacy_policy_url ?? this.privacyPolicyURL ?? null;
|
||||
|
||||
/**
|
||||
* The Array of RPC origin urls
|
||||
* @type {string[]}
|
||||
*/
|
||||
this.rpcOrigins = data.rpc_origins ?? this.rpcOrigins ?? [];
|
||||
|
||||
/**
|
||||
* The application's summary
|
||||
* @type {?string}
|
||||
*/
|
||||
this.summary = data.summary ?? this.summary ?? null;
|
||||
|
||||
/**
|
||||
* Whether the application can be default hooked by the client
|
||||
* @type {?boolean}
|
||||
*/
|
||||
this.hook = data.hook ?? this.hook ?? null;
|
||||
|
||||
/**
|
||||
* The hash of the application's cover image
|
||||
* @type {?string}
|
||||
*/
|
||||
this.cover = data.cover_image ?? this.cover ?? null;
|
||||
|
||||
/**
|
||||
* The hex-encoded key for verification in interactions and the GameSDK's GetTicket
|
||||
* @type {?string}
|
||||
*/
|
||||
this.verifyKey = data.verify_key ?? this.verifyKey ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = IntegrationApplication;
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
'use strict';
|
||||
|
||||
const Base = require('./Base');
|
||||
const { InteractionTypes, MessageComponentTypes } = require('../util/Constants');
|
||||
const SnowflakeUtil = require('../util/SnowflakeUtil');
|
||||
|
||||
/**
|
||||
* Represents an interaction.
|
||||
* @extends {Base}
|
||||
*/
|
||||
class Interaction extends Base {
|
||||
constructor(client, data) {
|
||||
super(client);
|
||||
|
||||
/**
|
||||
* The interaction's type
|
||||
* @type {InteractionType}
|
||||
*/
|
||||
this.type = InteractionTypes[data.type];
|
||||
|
||||
/**
|
||||
* The interaction's id
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.id = data.id;
|
||||
|
||||
/**
|
||||
* The interaction's token
|
||||
* @type {string}
|
||||
* @name Interaction#token
|
||||
* @readonly
|
||||
*/
|
||||
Object.defineProperty(this, 'token', { value: data.token });
|
||||
|
||||
/**
|
||||
* The application's id
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.applicationId = data.application_id;
|
||||
|
||||
/**
|
||||
* The id of the channel this interaction was sent in
|
||||
* @type {?Snowflake}
|
||||
*/
|
||||
this.channelId = data.channel_id ?? null;
|
||||
|
||||
/**
|
||||
* The id of the guild this interaction was sent in
|
||||
* @type {?Snowflake}
|
||||
*/
|
||||
this.guildId = data.guild_id ?? null;
|
||||
|
||||
/**
|
||||
* The user which sent this interaction
|
||||
* @type {User}
|
||||
*/
|
||||
this.user = this.client.users._add(data.user ?? data.member.user);
|
||||
|
||||
/**
|
||||
* If this interaction was sent in a guild, the member which sent it
|
||||
* @type {?(GuildMember|APIGuildMember)}
|
||||
*/
|
||||
this.member = data.member ? this.guild?.members._add(data.member) ?? data.member : null;
|
||||
|
||||
/**
|
||||
* The version
|
||||
* @type {number}
|
||||
*/
|
||||
this.version = data.version;
|
||||
}
|
||||
|
||||
/**
|
||||
* The timestamp the interaction was created at
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get createdTimestamp() {
|
||||
return SnowflakeUtil.deconstruct(this.id).timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* The time the interaction was created at
|
||||
* @type {Date}
|
||||
* @readonly
|
||||
*/
|
||||
get createdAt() {
|
||||
return new Date(this.createdTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* The channel this interaction was sent in
|
||||
* @type {?TextBasedChannels}
|
||||
* @readonly
|
||||
*/
|
||||
get channel() {
|
||||
return this.client.channels.cache.get(this.channelId) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The guild this interaction was sent in
|
||||
* @type {?Guild}
|
||||
* @readonly
|
||||
*/
|
||||
get guild() {
|
||||
return this.client.guilds.cache.get(this.guildId) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this interaction is received from a guild.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
inGuild() {
|
||||
return Boolean(this.guildId && this.member);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this interaction is a {@link CommandInteraction}.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isCommand() {
|
||||
return InteractionTypes[this.type] === InteractionTypes.APPLICATION_COMMAND && typeof this.targetId === 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this interaction is a {@link ContextMenuInteraction}
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isContextMenu() {
|
||||
return InteractionTypes[this.type] === InteractionTypes.APPLICATION_COMMAND && typeof this.targetId !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this interaction is a {@link MessageComponentInteraction}.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isMessageComponent() {
|
||||
return InteractionTypes[this.type] === InteractionTypes.MESSAGE_COMPONENT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this interaction is a {@link ButtonInteraction}.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isButton() {
|
||||
return (
|
||||
InteractionTypes[this.type] === InteractionTypes.MESSAGE_COMPONENT &&
|
||||
MessageComponentTypes[this.componentType] === MessageComponentTypes.BUTTON
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this interaction is a {@link SelectMenuInteraction}.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isSelectMenu() {
|
||||
return (
|
||||
InteractionTypes[this.type] === InteractionTypes.MESSAGE_COMPONENT &&
|
||||
MessageComponentTypes[this.componentType] === MessageComponentTypes.SELECT_MENU
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Interaction;
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const Collector = require('./interfaces/Collector');
|
||||
const { Events } = require('../util/Constants');
|
||||
const { InteractionTypes, MessageComponentTypes } = require('../util/Constants');
|
||||
|
||||
/**
|
||||
* @typedef {CollectorOptions} InteractionCollectorOptions
|
||||
* @property {TextBasedChannels} [channel] The channel to listen to interactions from
|
||||
* @property {MessageComponentType} [componentType] The type of component to listen for
|
||||
* @property {Guild} [guild] The guild to listen to interactions from
|
||||
* @property {InteractionType} [interactionType] The type of interaction to listen for
|
||||
* @property {number} [max] The maximum total amount of interactions to collect
|
||||
* @property {number} [maxComponents] The maximum number of components to collect
|
||||
* @property {number} [maxUsers] The maximum number of users to interact
|
||||
* @property {Message|APIMessage} [message] The message to listen to interactions from
|
||||
*/
|
||||
|
||||
/**
|
||||
* Collects interactions.
|
||||
* Will automatically stop if the message ({@link Client#messageDelete messageDelete}),
|
||||
* channel ({@link Client#channelDelete channelDelete}), or guild ({@link Client#guildDelete guildDelete}) is deleted.
|
||||
* @extends {Collector}
|
||||
*/
|
||||
class InteractionCollector extends Collector {
|
||||
/**
|
||||
* @param {Client} client The client on which to collect interactions
|
||||
* @param {InteractionCollectorOptions} [options={}] The options to apply to this collector
|
||||
*/
|
||||
constructor(client, options = {}) {
|
||||
super(client, options);
|
||||
|
||||
/**
|
||||
* The message from which to collect interactions, if provided
|
||||
* @type {?Snowflake}
|
||||
*/
|
||||
this.messageId = options.message?.id ?? null;
|
||||
|
||||
/**
|
||||
* The channel from which to collect interactions, if provided
|
||||
* @type {?Snowflake}
|
||||
*/
|
||||
this.channelId =
|
||||
this.client.channels.resolveId(options.message?.channel) ??
|
||||
options.message?.channel_id ??
|
||||
this.client.channels.resolveId(options.channel);
|
||||
|
||||
/**
|
||||
* The guild from which to collect interactions, if provided
|
||||
* @type {?Snowflake}
|
||||
*/
|
||||
this.guildId =
|
||||
this.client.guilds.resolveId(options.message?.guild) ??
|
||||
options.message?.guild_id ??
|
||||
this.client.guilds.resolveId(options.channel?.guild) ??
|
||||
this.client.guilds.resolveId(options.guild);
|
||||
|
||||
/**
|
||||
* The the type of interaction to collect
|
||||
* @type {?InteractionType}
|
||||
*/
|
||||
this.interactionType =
|
||||
typeof options.interactionType === 'number'
|
||||
? InteractionTypes[options.interactionType]
|
||||
: options.interactionType ?? null;
|
||||
|
||||
/**
|
||||
* The the type of component to collect
|
||||
* @type {?MessageComponentType}
|
||||
*/
|
||||
this.componentType =
|
||||
typeof options.componentType === 'number'
|
||||
? MessageComponentTypes[options.componentType]
|
||||
: options.componentType ?? null;
|
||||
|
||||
/**
|
||||
* The users which have interacted to this collector
|
||||
* @type {Collection<Snowflake, User>}
|
||||
*/
|
||||
this.users = new Collection();
|
||||
|
||||
/**
|
||||
* The total number of interactions collected
|
||||
* @type {number}
|
||||
*/
|
||||
this.total = 0;
|
||||
|
||||
this.empty = this.empty.bind(this);
|
||||
this.client.incrementMaxListeners();
|
||||
|
||||
if (this.messageId) {
|
||||
this._handleMessageDeletion = this._handleMessageDeletion.bind(this);
|
||||
this.client.on(Events.MESSAGE_DELETE, this._handleMessageDeletion);
|
||||
}
|
||||
|
||||
if (this.channelId) {
|
||||
this._handleChannelDeletion = this._handleChannelDeletion.bind(this);
|
||||
this.client.on(Events.CHANNEL_DELETE, this._handleChannelDeletion);
|
||||
}
|
||||
|
||||
if (this.guildId) {
|
||||
this._handleGuildDeletion = this._handleGuildDeletion.bind(this);
|
||||
this.client.on(Events.GUILD_DELETE, this._handleGuildDeletion);
|
||||
}
|
||||
|
||||
this.client.on(Events.INTERACTION_CREATE, this.handleCollect);
|
||||
|
||||
this.once('end', () => {
|
||||
this.client.removeListener(Events.INTERACTION_CREATE, this.handleCollect);
|
||||
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', interaction => {
|
||||
this.total++;
|
||||
this.users.set(interaction.user.id, interaction.user);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an incoming interaction for possible collection.
|
||||
* @param {Interaction} interaction The interaction to possibly collect
|
||||
* @returns {?Snowflake}
|
||||
* @private
|
||||
*/
|
||||
collect(interaction) {
|
||||
/**
|
||||
* Emitted whenever an interaction is collected.
|
||||
* @event InteractionCollector#collect
|
||||
* @param {Interaction} interaction The interaction that was collected
|
||||
*/
|
||||
if (this.interactionType && interaction.type !== this.interactionType) return null;
|
||||
if (this.componentType && interaction.componentType !== this.componentType) return null;
|
||||
if (this.messageId && interaction.message?.id !== this.messageId) return null;
|
||||
if (this.channelId && interaction.channelId !== this.channelId) return null;
|
||||
if (this.guildId && interaction.guildId !== this.guildId) return null;
|
||||
|
||||
return interaction.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an interaction for possible disposal.
|
||||
* @param {Interaction} interaction The interaction that could be disposed of
|
||||
* @returns {?Snowflake}
|
||||
*/
|
||||
dispose(interaction) {
|
||||
/**
|
||||
* Emitted whenever an interaction is disposed of.
|
||||
* @event InteractionCollector#dispose
|
||||
* @param {Interaction} interaction The interaction that was disposed of
|
||||
*/
|
||||
if (this.type && interaction.type !== this.type) return null;
|
||||
if (this.componentType && interaction.componentType !== this.componentType) return null;
|
||||
if (this.messageId && interaction.message?.id !== this.messageId) return null;
|
||||
if (this.channelId && interaction.channelId !== this.channelId) return null;
|
||||
if (this.guildId && interaction.guildId !== this.guildId) return null;
|
||||
|
||||
return interaction.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Empties this interaction collector.
|
||||
*/
|
||||
empty() {
|
||||
this.total = 0;
|
||||
this.collected.clear();
|
||||
this.users.clear();
|
||||
this.checkEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* The reason this collector has ended with, or null if it hasn't ended yet
|
||||
* @type {?string}
|
||||
* @readonly
|
||||
*/
|
||||
get endReason() {
|
||||
if (this.options.max && this.total >= this.options.max) return 'limit';
|
||||
if (this.options.maxComponents && this.collected.size >= this.options.maxComponents) return 'componentLimit';
|
||||
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.messageId) {
|
||||
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.channelId) {
|
||||
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 (guild.id === this.guildId) {
|
||||
this.stop('guildDelete');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = InteractionCollector;
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
'use strict';
|
||||
|
||||
const Webhook = require('./Webhook');
|
||||
|
||||
/**
|
||||
* Represents a webhook for an Interaction
|
||||
* @implements {Webhook}
|
||||
*/
|
||||
class InteractionWebhook {
|
||||
/**
|
||||
* @param {Client} client The instantiating client
|
||||
* @param {Snowflake} id The application's id
|
||||
* @param {string} token The interaction's token
|
||||
*/
|
||||
constructor(client, id, token) {
|
||||
/**
|
||||
* The client that instantiated the interaction webhook
|
||||
* @name InteractionWebhook#client
|
||||
* @type {Client}
|
||||
* @readonly
|
||||
*/
|
||||
Object.defineProperty(this, 'client', { value: client });
|
||||
this.id = id;
|
||||
Object.defineProperty(this, 'token', { value: token, writable: true, configurable: true });
|
||||
}
|
||||
|
||||
// These are here only for documentation purposes - they are implemented by Webhook
|
||||
/* eslint-disable no-empty-function, valid-jsdoc */
|
||||
/**
|
||||
* Sends a message with this webhook.
|
||||
* @param {string|MessagePayload|InteractionReplyOptions} options The content for the reply
|
||||
* @returns {Promise<Message|APIMessage>}
|
||||
*/
|
||||
send() {}
|
||||
fetchMessage() {}
|
||||
editMessage() {}
|
||||
deleteMessage() {}
|
||||
get url() {}
|
||||
}
|
||||
|
||||
Webhook.applyToClass(InteractionWebhook, ['sendSlackMessage', 'edit', 'delete', 'createdTimestamp', 'createdAt']);
|
||||
|
||||
module.exports = InteractionWebhook;
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
'use strict';
|
||||
|
||||
const Base = require('./Base');
|
||||
const IntegrationApplication = require('./IntegrationApplication');
|
||||
const InviteStageInstance = require('./InviteStageInstance');
|
||||
const { Error } = require('../errors');
|
||||
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) {
|
||||
const InviteGuild = require('./InviteGuild');
|
||||
const Guild = require('./Guild');
|
||||
/**
|
||||
* The guild the invite is for including welcome screen data if present
|
||||
* @type {?(Guild|InviteGuild)}
|
||||
*/
|
||||
this.guild = null;
|
||||
if (data.guild) {
|
||||
this.guild = data.guild instanceof Guild ? data.guild : new InviteGuild(this.client, data.guild);
|
||||
}
|
||||
|
||||
/**
|
||||
* The code for this invite
|
||||
* @type {string}
|
||||
*/
|
||||
this.code = data.code;
|
||||
|
||||
/**
|
||||
* The approximate number of online members of the guild this invite is for
|
||||
* @type {?number}
|
||||
*/
|
||||
this.presenceCount = data.approximate_presence_count ?? null;
|
||||
|
||||
/**
|
||||
* The approximate total number of members of the guild this invite is for
|
||||
* @type {?number}
|
||||
*/
|
||||
this.memberCount = data.approximate_member_count ?? null;
|
||||
|
||||
/**
|
||||
* Whether or not this invite is temporary
|
||||
* @type {?boolean}
|
||||
*/
|
||||
this.temporary = data.temporary ?? null;
|
||||
|
||||
/**
|
||||
* The maximum age of the invite, in seconds, 0 if never expires
|
||||
* @type {?number}
|
||||
*/
|
||||
this.maxAge = data.max_age ?? null;
|
||||
|
||||
/**
|
||||
* How many times this invite has been used
|
||||
* @type {?number}
|
||||
*/
|
||||
this.uses = data.uses ?? null;
|
||||
|
||||
/**
|
||||
* The maximum uses of this invite
|
||||
* @type {?number}
|
||||
*/
|
||||
this.maxUses = data.max_uses ?? null;
|
||||
|
||||
/**
|
||||
* The user who created this invite
|
||||
* @type {?User}
|
||||
*/
|
||||
this.inviter = data.inviter ? this.client.users._add(data.inviter) : null;
|
||||
|
||||
/**
|
||||
* The user whose stream to display for this voice channel stream invite
|
||||
* @type {?User}
|
||||
*/
|
||||
this.targetUser = data.target_user ? this.client.users._add(data.target_user) : null;
|
||||
|
||||
/**
|
||||
* The embedded application to open for this voice channel embedded application invite
|
||||
* @type {?IntegrationApplication}
|
||||
*/
|
||||
this.targetApplication = data.target_application
|
||||
? new IntegrationApplication(this.client, data.target_application)
|
||||
: null;
|
||||
|
||||
/**
|
||||
* The type of the invite target:
|
||||
* * 1: STREAM
|
||||
* * 2: EMBEDDED_APPLICATION
|
||||
* @typedef {number} TargetType
|
||||
* @see {@link https://discord.com/developers/docs/resources/invite#invite-object-invite-target-types}
|
||||
*/
|
||||
|
||||
/**
|
||||
* The target type
|
||||
* @type {?TargetType}
|
||||
*/
|
||||
this.targetType = data.target_type ?? null;
|
||||
|
||||
/**
|
||||
* The channel the invite is for
|
||||
* @type {Channel}
|
||||
*/
|
||||
this.channel = this.client.channels._add(data.channel, this.guild, { cache: false });
|
||||
|
||||
/**
|
||||
* The timestamp the invite was created at
|
||||
* @type {?number}
|
||||
*/
|
||||
this.createdTimestamp = 'created_at' in data ? new Date(data.created_at).getTime() : null;
|
||||
|
||||
this._expiresTimestamp = 'expires_at' in data ? new Date(data.expires_at).getTime() : null;
|
||||
|
||||
/**
|
||||
* The stage instance data if there is a public {@link StageInstance} in the stage channel this invite is for
|
||||
* @type {?InviteStageInstance}
|
||||
*/
|
||||
this.stageInstance =
|
||||
'stage_instance' in data
|
||||
? new InviteStageInstance(this.client, data.stage_instance, this.channel.id, this.guild.id)
|
||||
: 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._expiresTimestamp ??
|
||||
(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>}
|
||||
*/
|
||||
async delete(reason) {
|
||||
await this.client.api.invites[this.code].delete({ reason });
|
||||
return 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Regular expression that globally matches Discord invite links
|
||||
* @type {RegExp}
|
||||
*/
|
||||
Invite.INVITES_PATTERN = /discord(?:(?:app)?\.com\/invite|\.gg(?:\/invite)?)\/([\w-]{2,255})/gi;
|
||||
|
||||
module.exports = Invite;
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
const AnonymousGuild = require('./AnonymousGuild');
|
||||
const WelcomeScreen = require('./WelcomeScreen');
|
||||
|
||||
/**
|
||||
* Represents a guild received from an invite, includes welcome screen data if available.
|
||||
* @extends {AnonymousGuild}
|
||||
*/
|
||||
class InviteGuild extends AnonymousGuild {
|
||||
constructor(client, data) {
|
||||
super(client, data);
|
||||
|
||||
/**
|
||||
* The welcome screen for this invite guild
|
||||
* @type {?WelcomeScreen}
|
||||
*/
|
||||
this.welcomeScreen =
|
||||
typeof data.welcome_screen !== 'undefined' ? new WelcomeScreen(this, data.welcome_screen) : null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = InviteGuild;
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const Base = require('./Base');
|
||||
|
||||
/**
|
||||
* Represents the data about a public {@link StageInstance} in an {@link Invite}.
|
||||
* @extends {Base}
|
||||
*/
|
||||
class InviteStageInstance extends Base {
|
||||
constructor(client, data, channelId, guildId) {
|
||||
super(client);
|
||||
|
||||
/**
|
||||
* The id of the stage channel this invite is for
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.channelId = channelId;
|
||||
|
||||
/**
|
||||
* The stage channel's guild id
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.guildId = guildId;
|
||||
|
||||
/**
|
||||
* The members speaking in the stage channel
|
||||
* @type {Collection<Snowflake, GuildMember>}
|
||||
*/
|
||||
this.members = new Collection();
|
||||
|
||||
this._patch(data);
|
||||
}
|
||||
|
||||
_patch(data) {
|
||||
/**
|
||||
* The topic of the stage instance
|
||||
* @type {string}
|
||||
*/
|
||||
this.topic = data.topic;
|
||||
|
||||
/**
|
||||
* The number of users in the stage channel
|
||||
* @type {number}
|
||||
*/
|
||||
this.participantCount = data.participant_count;
|
||||
|
||||
/**
|
||||
* The number of users speaking in the stage channel
|
||||
* @type {number}
|
||||
*/
|
||||
this.speakerCount = data.speaker_count;
|
||||
|
||||
this.members.clear();
|
||||
for (const rawMember of data.members) {
|
||||
const member = this.guild.members._add(rawMember);
|
||||
this.members.set(member.id, member);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The stage channel this invite is for
|
||||
* @type {?StageChannel}
|
||||
* @readonly
|
||||
*/
|
||||
get channel() {
|
||||
return this.client.channels.resolve(this.channelId);
|
||||
}
|
||||
|
||||
/**
|
||||
* The guild of the stage channel this invite is for
|
||||
* @type {?Guild}
|
||||
* @readonly
|
||||
*/
|
||||
get guild() {
|
||||
return this.client.guilds.resolve(this.guildId);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = InviteStageInstance;
|
||||
+868
@@ -0,0 +1,868 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const Base = require('./Base');
|
||||
const BaseMessageComponent = require('./BaseMessageComponent');
|
||||
const ClientApplication = require('./ClientApplication');
|
||||
const InteractionCollector = require('./InteractionCollector');
|
||||
const MessageAttachment = require('./MessageAttachment');
|
||||
const Embed = require('./MessageEmbed');
|
||||
const Mentions = require('./MessageMentions');
|
||||
const MessagePayload = require('./MessagePayload');
|
||||
const ReactionCollector = require('./ReactionCollector');
|
||||
const Sticker = require('./Sticker');
|
||||
const { Error } = require('../errors');
|
||||
const ReactionManager = require('../managers/ReactionManager');
|
||||
const { InteractionTypes, MessageTypes, SystemMessageTypes } = require('../util/Constants');
|
||||
const MessageFlags = require('../util/MessageFlags');
|
||||
const Permissions = require('../util/Permissions');
|
||||
const SnowflakeUtil = require('../util/SnowflakeUtil');
|
||||
const Util = require('../util/Util');
|
||||
|
||||
/**
|
||||
* Represents a message on Discord.
|
||||
* @extends {Base}
|
||||
*/
|
||||
class Message extends Base {
|
||||
/**
|
||||
* @param {Client} client The instantiating client
|
||||
* @param {APIMessage} data The data for the message
|
||||
*/
|
||||
constructor(client, data) {
|
||||
super(client);
|
||||
|
||||
/**
|
||||
* The id of the channel the message was sent in
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.channelId = data.channel_id;
|
||||
|
||||
/**
|
||||
* The id of the guild the message was sent in, if any
|
||||
* @type {?Snowflake}
|
||||
*/
|
||||
this.guildId = data.guild_id ?? this.channel?.guild?.id ?? null;
|
||||
|
||||
/**
|
||||
* Whether this message has been deleted
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.deleted = false;
|
||||
|
||||
if (data) this._patch(data);
|
||||
}
|
||||
|
||||
_patch(data, partial = false) {
|
||||
/**
|
||||
* The message's id
|
||||
* @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 = SystemMessageTypes.includes(this.type);
|
||||
} 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;
|
||||
}
|
||||
|
||||
if (!partial) {
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
if ('embeds' in data || !partial) {
|
||||
/**
|
||||
* A list of embeds in the message - e.g. YouTube Player
|
||||
* @type {MessageEmbed[]}
|
||||
*/
|
||||
this.embeds = data.embeds?.map(e => new Embed(e, true)) ?? [];
|
||||
} else {
|
||||
this.embeds = this.embeds.slice();
|
||||
}
|
||||
|
||||
if ('components' in data || !partial) {
|
||||
/**
|
||||
* A list of MessageActionRows in the message
|
||||
* @type {MessageActionRow[]}
|
||||
*/
|
||||
this.components = data.components?.map(c => BaseMessageComponent.create(c, this.client)) ?? [];
|
||||
} else {
|
||||
this.components = this.components.slice();
|
||||
}
|
||||
|
||||
if ('attachments' in data || !partial) {
|
||||
/**
|
||||
* A collection of attachments in the message - e.g. Pictures - mapped by their ids
|
||||
* @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));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.attachments = new Collection(this.attachemnts);
|
||||
}
|
||||
|
||||
if ('sticker_items' in data || 'stickers' in data || !partial) {
|
||||
/**
|
||||
* A collection of stickers in the message
|
||||
* @type {Collection<Snowflake, Sticker>}
|
||||
*/
|
||||
this.stickers = new Collection(
|
||||
(data.sticker_items ?? data.stickers)?.map(s => [s.id, new Sticker(this.client, s)]),
|
||||
);
|
||||
} else {
|
||||
this.stickers = new Collection(this.stickers);
|
||||
}
|
||||
|
||||
if (!partial) {
|
||||
/**
|
||||
* The timestamp the message was sent at
|
||||
* @type {number}
|
||||
*/
|
||||
this.createdTimestamp = SnowflakeUtil.deconstruct(this.id).timestamp;
|
||||
}
|
||||
|
||||
if ('edited_timestamp' in data || !partial) {
|
||||
/**
|
||||
* The timestamp the message was last edited at (if applicable)
|
||||
* @type {?number}
|
||||
*/
|
||||
this.editedTimestamp = data.edited_timestamp ? new Date(data.edited_timestamp).getTime() : null;
|
||||
}
|
||||
|
||||
if ('reactions' in data || !partial) {
|
||||
/**
|
||||
* A manager of the reactions belonging to this message
|
||||
* @type {ReactionManager}
|
||||
*/
|
||||
this.reactions = new ReactionManager(this);
|
||||
if (data.reactions?.length > 0) {
|
||||
for (const reaction of data.reactions) {
|
||||
this.reactions._add(reaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!partial) {
|
||||
/**
|
||||
* 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,
|
||||
data.referenced_message?.author,
|
||||
);
|
||||
} else {
|
||||
this.mentions = new Mentions(
|
||||
this,
|
||||
data.mentions ?? this.mentions.users,
|
||||
data.mention_roles ?? this.mentions.roles,
|
||||
data.mention_everyone ?? this.mentions.everyone,
|
||||
data.mention_channels ?? this.mentions.crosspostedChannels,
|
||||
data.referenced_message?.author ?? this.mentions.repliedUser,
|
||||
);
|
||||
}
|
||||
|
||||
if ('webhook_id' in data || !partial) {
|
||||
/**
|
||||
* The id of the webhook that sent the message, if applicable
|
||||
* @type {?Snowflake}
|
||||
*/
|
||||
this.webhookId = data.webhook_id ?? null;
|
||||
}
|
||||
|
||||
if ('application' in data || !partial) {
|
||||
/**
|
||||
* Supplemental application information for group activities
|
||||
* @type {?ClientApplication}
|
||||
*/
|
||||
this.groupActivityApplication = data.application ? new ClientApplication(this.client, data.application) : null;
|
||||
}
|
||||
|
||||
if ('application_id' in data || !partial) {
|
||||
/**
|
||||
* The id of the application of the interaction that sent this message, if any
|
||||
* @type {?Snowflake}
|
||||
*/
|
||||
this.applicationId = data.application_id ?? null;
|
||||
}
|
||||
|
||||
if ('activity' in data || !partial) {
|
||||
/**
|
||||
* Group activity
|
||||
* @type {?MessageActivity}
|
||||
*/
|
||||
this.activity = data.activity
|
||||
? {
|
||||
partyId: data.activity.party_id,
|
||||
type: data.activity.type,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
if ('thread' in data) {
|
||||
this.client.channels._add(data.thread, this.guild);
|
||||
}
|
||||
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 }));
|
||||
}
|
||||
|
||||
if ('flags' in data || !partial) {
|
||||
/**
|
||||
* Flags that are applied to the message
|
||||
* @type {Readonly<MessageFlags>}
|
||||
*/
|
||||
this.flags = new MessageFlags(data.flags).freeze();
|
||||
} else {
|
||||
this.flags = new MessageFlags(this.flags).freeze();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reference data sent in a message that contains ids identifying the referenced message
|
||||
* @typedef {Object} MessageReference
|
||||
* @property {Snowflake} channelId The channel's id the message was referenced
|
||||
* @property {?Snowflake} guildId The guild's id the message was referenced
|
||||
* @property {?Snowflake} messageId The message's id that was referenced
|
||||
*/
|
||||
|
||||
if ('message_reference' in data || !partial) {
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
if (data.referenced_message) {
|
||||
this.channel?.messages._add({ guild_id: data.message_reference?.guild_id, ...data.referenced_message });
|
||||
}
|
||||
|
||||
/**
|
||||
* Partial data of the interaction that a message is a reply to
|
||||
* @typedef {Object} MessageInteraction
|
||||
* @property {Snowflake} id The interaction's id
|
||||
* @property {InteractionType} type The type of the interaction
|
||||
* @property {string} commandName The name of the interaction's application command
|
||||
* @property {User} user The user that invoked the interaction
|
||||
*/
|
||||
|
||||
if (data.interaction) {
|
||||
/**
|
||||
* Partial data of the interaction that this message is a reply to
|
||||
* @type {?MessageInteraction}
|
||||
*/
|
||||
this.interaction = {
|
||||
id: data.interaction.id,
|
||||
type: InteractionTypes[data.interaction.type],
|
||||
commandName: data.interaction.name,
|
||||
user: this.client.users._add(data.interaction.user),
|
||||
};
|
||||
} else if (!this.interaction) {
|
||||
this.interaction = null;
|
||||
}
|
||||
}
|
||||
|
||||
_update(data, partial = false) {
|
||||
const clone = this._clone();
|
||||
this._patch(data, partial);
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* The channel that the message was sent in
|
||||
* @type {TextChannel|DMChannel|NewsChannel|ThreadChannel}
|
||||
* @readonly
|
||||
*/
|
||||
get channel() {
|
||||
return this.client.channels.resolve(this.channelId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not this message is a partial
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get partial() {
|
||||
return typeof this.content !== 'string' || !this.author;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the author of the message as a guild member.
|
||||
* Only available if the message comes from a guild where the author is still a member
|
||||
* @type {?GuildMember}
|
||||
* @readonly
|
||||
*/
|
||||
get member() {
|
||||
return this.guild?.members.resolve(this.author) ?? 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.client.guilds.resolve(this.guildId) ?? this.channel?.guild ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this message has a thread associated with it
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get hasThread() {
|
||||
return this.flags.has(MessageFlags.FLAGS.HAS_THREAD);
|
||||
}
|
||||
|
||||
/**
|
||||
* The thread started by this message
|
||||
* <info>This property is not suitable for checking whether a message has a thread,
|
||||
* use {@link Message#hasThread} instead.</info>
|
||||
* @type {?ThreadChannel}
|
||||
* @readonly
|
||||
*/
|
||||
get thread() {
|
||||
return this.channel?.threads?.resolve(this.id) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The url to jump to this message
|
||||
* @type {string}
|
||||
* @readonly
|
||||
*/
|
||||
get url() {
|
||||
return `https://discord.com/channels/${this.guildId ?? '@me'}/${this.channelId}/${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.channel) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a reaction collector.
|
||||
* @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(options = {}) {
|
||||
return new ReactionCollector(this, 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 {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(options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const collector = this.createReactionCollector(options);
|
||||
collector.once('end', (reactions, reason) => {
|
||||
if (options.errors?.includes(reason)) reject(reactions);
|
||||
else resolve(reactions);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {CollectorOptions} MessageComponentCollectorOptions
|
||||
* @property {MessageComponentType} [componentType] The type of component to listen for
|
||||
* @property {number} [max] The maximum total amount of interactions to collect
|
||||
* @property {number} [maxComponents] The maximum number of components to collect
|
||||
* @property {number} [maxUsers] The maximum number of users to interact
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a message component interaction collector.
|
||||
* @param {MessageComponentCollectorOptions} [options={}] Options to send to the collector
|
||||
* @returns {InteractionCollector}
|
||||
* @example
|
||||
* // Create a message component interaction collector
|
||||
* const filter = (interaction) => interaction.customId === 'button' && interaction.user.id === 'someId';
|
||||
* const collector = message.createMessageComponentCollector({ filter, time: 15000 });
|
||||
* collector.on('collect', i => console.log(`Collected ${i.customId}`));
|
||||
* collector.on('end', collected => console.log(`Collected ${collected.size} items`));
|
||||
*/
|
||||
createMessageComponentCollector(options = {}) {
|
||||
return new InteractionCollector(this.client, {
|
||||
...options,
|
||||
interactionType: InteractionTypes.MESSAGE_COMPONENT,
|
||||
message: this,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* An object containing the same properties as CollectorOptions, but a few more:
|
||||
* @typedef {Object} AwaitMessageComponentOptions
|
||||
* @property {CollectorFilter} [filter] The filter applied to this collector
|
||||
* @property {number} [time] Time to wait for an interaction before rejecting
|
||||
* @property {MessageComponentType} [componentType] The type of component interaction to collect
|
||||
*/
|
||||
|
||||
/**
|
||||
* Collects a single component interaction that passes the filter.
|
||||
* The Promise will reject if the time expires.
|
||||
* @param {AwaitMessageComponentOptions} [options={}] Options to pass to the internal collector
|
||||
* @returns {Promise<MessageComponentInteraction>}
|
||||
* @example
|
||||
* // Collect a message component interaction
|
||||
* const filter = (interaction) => interaction.customId === 'button' && interaction.user.id === 'someId';
|
||||
* message.awaitMessageComponent({ filter, time: 15000 })
|
||||
* .then(interaction => console.log(`${interaction.customId} was clicked!`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
awaitMessageComponent(options = {}) {
|
||||
const _options = { ...options, max: 1 };
|
||||
return new Promise((resolve, reject) => {
|
||||
const collector = this.createMessageComponentCollector(_options);
|
||||
collector.once('end', (interactions, reason) => {
|
||||
const interaction = interactions.first();
|
||||
if (interaction) resolve(interaction);
|
||||
else reject(new Error('INTERACTION_COLLECTOR_ERROR', reason));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Boolean(
|
||||
!this.deleted &&
|
||||
(this.author.id === this.client.user.id ||
|
||||
this.channel.permissionsFor?.(this.client.user)?.has(Permissions.FLAGS.MANAGE_MESSAGES)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the message is pinnable by the client user
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get pinnable() {
|
||||
return (
|
||||
this.type === 'DEFAULT' &&
|
||||
(!this.guild || this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_MESSAGES, false))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the Message this crosspost/reply/pin-add references, if available to the client
|
||||
* @returns {Promise<Message>}
|
||||
*/
|
||||
async fetchReference() {
|
||||
if (!this.reference) throw new Error('MESSAGE_REFERENCE_MISSING');
|
||||
const { channelId, messageId } = this.reference;
|
||||
const channel = this.client.channels.resolve(channelId);
|
||||
if (!channel) throw new Error('GUILD_CHANNEL_RESOLVE');
|
||||
const message = await channel.messages.fetch(messageId);
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the message is crosspostable by the client user
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get crosspostable() {
|
||||
return (
|
||||
this.channel.type === 'GUILD_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 {@link Message#edit}.
|
||||
* @typedef {Object} MessageEditOptions
|
||||
* @property {?string} [content] Content to be edited
|
||||
* @property {MessageEmbed[]|APIEmbed[]} [embeds] Embeds to be added/edited
|
||||
* @property {MessageMentionOptions} [allowedMentions] Which mentions should be parsed from the message content
|
||||
* @property {MessageFlags} [flags] Which flags to set for the message. Only `SUPPRESS_EMBEDS` can be edited.
|
||||
* @property {MessageAttachment[]} [attachments] An array of attachments to keep,
|
||||
* all attachments will be kept if omitted
|
||||
* @property {FileOptions[]|BufferResolvable[]|MessageAttachment[]} [files] Files to add to the message
|
||||
* @property {MessageActionRow[]|MessageActionRowOptions[]} [components]
|
||||
* Action rows containing interactive components for the message (buttons, select menus)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Edits the content of the message.
|
||||
* @param {string|MessagePayload|MessageEditOptions} 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(options) {
|
||||
return this.channel.messages.edit(this, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishes a message in an announcement channel to all channels following it.
|
||||
* @returns {Promise<Message>}
|
||||
* @example
|
||||
* // Crosspost a message
|
||||
* if (message.channel.type === 'GUILD_NEWS') {
|
||||
* message.crosspost()
|
||||
* .then(() => console.log('Crossposted message'))
|
||||
* .catch(console.error);
|
||||
* }
|
||||
*/
|
||||
crosspost() {
|
||||
return this.channel.messages.crosspost(this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pins this message to the channel's pinned messages.
|
||||
* @returns {Promise<Message>}
|
||||
* @example
|
||||
* // Pin a message
|
||||
* message.pin()
|
||||
* .then(console.log)
|
||||
* .catch(console.error)
|
||||
*/
|
||||
async pin() {
|
||||
await this.channel.messages.pin(this.id);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpins this message from the channel's pinned messages.
|
||||
* @returns {Promise<Message>}
|
||||
* @example
|
||||
* // Unpin a message
|
||||
* message.unpin()
|
||||
* .then(console.log)
|
||||
* .catch(console.error)
|
||||
*/
|
||||
async unpin() {
|
||||
await this.channel.messages.unpin(this.id);
|
||||
return 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);
|
||||
*/
|
||||
async react(emoji) {
|
||||
emoji = this.client.emojis.resolveIdentifier(emoji);
|
||||
await this.channel.messages.react(this.id, emoji);
|
||||
return this.client.actions.MessageReactionAdd.handle({
|
||||
user: this.client.user,
|
||||
channel: this.channel,
|
||||
message: this,
|
||||
emoji: Util.parseEmoji(emoji),
|
||||
}).reaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the message.
|
||||
* @returns {Promise<Message>}
|
||||
* @example
|
||||
* // Delete a message
|
||||
* message.delete()
|
||||
* .then(msg => console.log(`Deleted message from ${msg.author.username}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async delete() {
|
||||
await this.channel.messages.delete(this.id);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options provided when sending a message as an inline reply.
|
||||
* @typedef {BaseMessageOptions} ReplyMessageOptions
|
||||
* @property {boolean} [failIfNotExists=true] Whether to error if the referenced message
|
||||
* does not exist (creates a standard message in this case when false)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Send an inline reply to this message.
|
||||
* @param {string|MessagePayload|ReplyMessageOptions} options The options to provide
|
||||
* @returns {Promise<Message|Message[]>}
|
||||
* @example
|
||||
* // Reply to a message
|
||||
* message.reply('This is a reply!')
|
||||
* .then(() => console.log(`Replied to message "${message.content}"`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
reply(options) {
|
||||
let data;
|
||||
|
||||
if (options instanceof MessagePayload) {
|
||||
data = options;
|
||||
} else {
|
||||
data = MessagePayload.create(this, options, {
|
||||
reply: {
|
||||
messageReference: this,
|
||||
failIfNotExists: options?.failIfNotExists ?? this.client.options.failIfNotExists,
|
||||
},
|
||||
});
|
||||
}
|
||||
return this.channel.send(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for starting a thread on a message.
|
||||
* @typedef {Object} StartThreadOptions
|
||||
* @property {string} name The name of the new thread
|
||||
* @property {ThreadAutoArchiveDuration} autoArchiveDuration The amount of time (in minutes) after which the thread
|
||||
* should automatically archive in case of no recent activity
|
||||
* @property {string} [reason] Reason for creating the thread
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a new public thread from this message
|
||||
* @see ThreadManager#create
|
||||
* @param {StartThreadOptions} [options] Options for starting a thread on this message
|
||||
* @returns {Promise<ThreadChannel>}
|
||||
*/
|
||||
startThread(options = {}) {
|
||||
if (!['GUILD_TEXT', 'GUILD_NEWS'].includes(this.channel.type)) {
|
||||
return Promise.reject(new Error('MESSAGE_THREAD_PARENT'));
|
||||
}
|
||||
if (this.hasThread) return Promise.reject(new Error('MESSAGE_EXISTING_THREAD'));
|
||||
return this.channel.threads.create({ ...options, startMessage: this });
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch this message.
|
||||
* @param {boolean} [force=true] Whether to skip the cache check and request the API
|
||||
* @returns {Promise<Message>}
|
||||
*/
|
||||
fetch(force = true) {
|
||||
return this.channel.messages.fetch(this.id, { 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 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the attachments from this message.
|
||||
* @returns {Promise<Message>}
|
||||
*/
|
||||
removeAttachments() {
|
||||
return this.edit({ attachments: [] });
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {APIMessage} 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',
|
||||
groupActivityApplication: 'groupActivityApplicationId',
|
||||
guild: 'guildId',
|
||||
cleanContent: true,
|
||||
member: false,
|
||||
reactions: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Message;
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
'use strict';
|
||||
|
||||
const BaseMessageComponent = require('./BaseMessageComponent');
|
||||
const { MessageComponentTypes } = require('../util/Constants');
|
||||
|
||||
/**
|
||||
* Represents an action row containing message components.
|
||||
* @extends {BaseMessageComponent}
|
||||
*/
|
||||
class MessageActionRow extends BaseMessageComponent {
|
||||
/**
|
||||
* Components that can be placed in an action row
|
||||
* * MessageButton
|
||||
* * MessageSelectMenu
|
||||
* @typedef {MessageButton|MessageSelectMenu} MessageActionRowComponent
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for components that can be placed in an action row
|
||||
* * MessageButtonOptions
|
||||
* * MessageSelectMenuOptions
|
||||
* @typedef {MessageButtonOptions|MessageSelectMenuOptions} MessageActionRowComponentOptions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Data that can be resolved into components that can be placed in an action row
|
||||
* * MessageActionRowComponent
|
||||
* * MessageActionRowComponentOptions
|
||||
* @typedef {MessageActionRowComponent|MessageActionRowComponentOptions} MessageActionRowComponentResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {BaseMessageComponentOptions} MessageActionRowOptions
|
||||
* @property {MessageActionRowComponentResolvable[]} [components]
|
||||
* The components to place in this action row
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {MessageActionRow|MessageActionRowOptions} [data={}] MessageActionRow to clone or raw data
|
||||
* @param {Client} [client] The client constructing this MessageActionRow, if provided
|
||||
*/
|
||||
constructor(data = {}, client = null) {
|
||||
super({ type: 'ACTION_ROW' });
|
||||
|
||||
/**
|
||||
* The components in this action row
|
||||
* @type {MessageActionRowComponent[]}
|
||||
*/
|
||||
this.components = data.components?.map(c => BaseMessageComponent.create(c, client)) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds components to the action row.
|
||||
* @param {...MessageActionRowComponentResolvable[]} components The components to add
|
||||
* @returns {MessageActionRow}
|
||||
*/
|
||||
addComponents(...components) {
|
||||
this.components.push(...components.flat(Infinity).map(c => BaseMessageComponent.create(c)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes, replaces, and inserts components in the action row.
|
||||
* @param {number} index The index to start at
|
||||
* @param {number} deleteCount The number of components to remove
|
||||
* @param {...MessageActionRowComponentResolvable[]} [components] The replacing components
|
||||
* @returns {MessageActionRow}
|
||||
*/
|
||||
spliceComponents(index, deleteCount, ...components) {
|
||||
this.components.splice(index, deleteCount, ...components.flat(Infinity).map(c => BaseMessageComponent.create(c)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the action row to a plain object.
|
||||
* @returns {APIMessageComponent} The raw data of this action row
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
components: this.components.map(c => c.toJSON()),
|
||||
type: MessageComponentTypes[this.type],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MessageActionRow;
|
||||
|
||||
/**
|
||||
* @external APIMessageComponent
|
||||
* @see {@link https://discord.com/developers/docs/interactions/message-components#component-object}
|
||||
*/
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
'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 {APIAttachment} [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 attachment's id
|
||||
* @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 = data.height ?? null;
|
||||
|
||||
/**
|
||||
* The width of this attachment (if an image or video)
|
||||
* @type {?number}
|
||||
*/
|
||||
this.width = data.width ?? null;
|
||||
|
||||
/**
|
||||
* This media type of this attachment
|
||||
* @type {?string}
|
||||
*/
|
||||
this.contentType = data.content_type ?? 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;
|
||||
|
||||
/**
|
||||
* @external APIAttachment
|
||||
* @see {@link https://discord.com/developers/docs/resources/channel#attachment-object}
|
||||
*/
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
'use strict';
|
||||
|
||||
const BaseMessageComponent = require('./BaseMessageComponent');
|
||||
const { RangeError } = require('../errors');
|
||||
const { MessageButtonStyles, MessageComponentTypes } = require('../util/Constants');
|
||||
const Util = require('../util/Util');
|
||||
|
||||
/**
|
||||
* Represents a button message component.
|
||||
* @extends {BaseMessageComponent}
|
||||
*/
|
||||
class MessageButton extends BaseMessageComponent {
|
||||
/**
|
||||
* @typedef {BaseMessageComponentOptions} MessageButtonOptions
|
||||
* @property {string} [label] The text to be displayed on this button
|
||||
* @property {string} [customId] A unique string to be sent in the interaction when clicked
|
||||
* @property {MessageButtonStyleResolvable} [style] The style of this button
|
||||
* @property {EmojiIdentifierResolvable} [emoji] The emoji to be displayed to the left of the text
|
||||
* @property {string} [url] Optional URL for link-style buttons
|
||||
* @property {boolean} [disabled=false] Disables the button to prevent interactions
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {MessageButton|MessageButtonOptions} [data={}] MessageButton to clone or raw data
|
||||
*/
|
||||
constructor(data = {}) {
|
||||
super({ type: 'BUTTON' });
|
||||
|
||||
this.setup(data);
|
||||
}
|
||||
|
||||
setup(data) {
|
||||
/**
|
||||
* The text to be displayed on this button
|
||||
* @type {?string}
|
||||
*/
|
||||
this.label = data.label ?? null;
|
||||
|
||||
/**
|
||||
* A unique string to be sent in the interaction when clicked
|
||||
* @type {?string}
|
||||
*/
|
||||
this.customId = data.custom_id ?? data.customId ?? null;
|
||||
|
||||
/**
|
||||
* The style of this button
|
||||
* @type {?MessageButtonStyle}
|
||||
*/
|
||||
this.style = data.style ? MessageButton.resolveStyle(data.style) : null;
|
||||
|
||||
/**
|
||||
* Emoji for this button
|
||||
* @type {?RawEmoji}
|
||||
*/
|
||||
this.emoji = data.emoji ? Util.resolvePartialEmoji(data.emoji) : null;
|
||||
|
||||
/**
|
||||
* The URL this button links to, if it is a Link style button
|
||||
* @type {?string}
|
||||
*/
|
||||
this.url = data.url ?? null;
|
||||
|
||||
/**
|
||||
* Whether this button is currently disabled
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.disabled = data.disabled ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the custom id for this button
|
||||
* @param {string} customId A unique string to be sent in the interaction when clicked
|
||||
* @returns {MessageButton}
|
||||
*/
|
||||
setCustomId(customId) {
|
||||
this.customId = Util.verifyString(customId, RangeError, 'BUTTON_CUSTOM_ID');
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the interactive status of the button
|
||||
* @param {boolean} [disabled=true] Whether this button should be disabled
|
||||
* @returns {MessageButton}
|
||||
*/
|
||||
setDisabled(disabled = true) {
|
||||
this.disabled = disabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the emoji of this button
|
||||
* @param {EmojiIdentifierResolvable} emoji The emoji to be displayed on this button
|
||||
* @returns {MessageButton}
|
||||
*/
|
||||
setEmoji(emoji) {
|
||||
this.emoji = Util.resolvePartialEmoji(emoji);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the label of this button
|
||||
* @param {string} label The text to be displayed on this button
|
||||
* @returns {MessageButton}
|
||||
*/
|
||||
setLabel(label) {
|
||||
this.label = Util.verifyString(label, RangeError, 'BUTTON_LABEL');
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the style of this button
|
||||
* @param {MessageButtonStyleResolvable} style The style of this button
|
||||
* @returns {MessageButton}
|
||||
*/
|
||||
setStyle(style) {
|
||||
this.style = MessageButton.resolveStyle(style);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the URL of this button.
|
||||
* <note>MessageButton#style must be LINK when setting a URL</note>
|
||||
* @param {string} url The URL of this button
|
||||
* @returns {MessageButton}
|
||||
*/
|
||||
setURL(url) {
|
||||
this.url = Util.verifyString(url, RangeError, 'BUTTON_URL');
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the button to a plain object.
|
||||
* @returns {APIMessageComponent} The raw data of this button
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
custom_id: this.customId,
|
||||
disabled: this.disabled,
|
||||
emoji: this.emoji,
|
||||
label: this.label,
|
||||
style: MessageButtonStyles[this.style],
|
||||
type: MessageComponentTypes[this.type],
|
||||
url: this.url,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Data that can be resolved to a MessageButtonStyle. This can be
|
||||
* * MessageButtonStyle
|
||||
* * number
|
||||
* @typedef {number|MessageButtonStyle} MessageButtonStyleResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves the style of a button
|
||||
* @param {MessageButtonStyleResolvable} style The style to resolve
|
||||
* @returns {MessageButtonStyle}
|
||||
* @private
|
||||
*/
|
||||
static resolveStyle(style) {
|
||||
return typeof style === 'string' ? style : MessageButtonStyles[style];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MessageButton;
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
'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 {TextBasedChannels} channel The channel
|
||||
* @param {MessageCollectorOptions} options The options to be applied to this collector
|
||||
* @emits MessageCollector#message
|
||||
*/
|
||||
constructor(channel, options = {}) {
|
||||
super(channel.client, options);
|
||||
|
||||
/**
|
||||
* The channel
|
||||
* @type {TextBasedChannels}
|
||||
*/
|
||||
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.channelId !== 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.channelId === this.channel.id ? message.id : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The reason this collector has ended with, or null if it hasn't ended yet
|
||||
* @type {?string}
|
||||
* @readonly
|
||||
*/
|
||||
get 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 (guild.id === this.channel.guild?.id) {
|
||||
this.stop('guildDelete');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MessageCollector;
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
'use strict';
|
||||
|
||||
const Interaction = require('./Interaction');
|
||||
const InteractionWebhook = require('./InteractionWebhook');
|
||||
const InteractionResponses = require('./interfaces/InteractionResponses');
|
||||
const { MessageComponentTypes } = require('../util/Constants');
|
||||
|
||||
/**
|
||||
* Represents a message component interaction.
|
||||
* @extends {Interaction}
|
||||
* @implements {InteractionResponses}
|
||||
*/
|
||||
class MessageComponentInteraction extends Interaction {
|
||||
constructor(client, data) {
|
||||
super(client, data);
|
||||
|
||||
/**
|
||||
* The message to which the component was attached
|
||||
* @type {Message|APIMessage}
|
||||
*/
|
||||
this.message = this.channel?.messages._add(data.message) ?? data.message;
|
||||
|
||||
/**
|
||||
* The custom id of the component which was interacted with
|
||||
* @type {string}
|
||||
*/
|
||||
this.customId = data.data.custom_id;
|
||||
|
||||
/**
|
||||
* The type of component which was interacted with
|
||||
* @type {string}
|
||||
*/
|
||||
this.componentType = MessageComponentInteraction.resolveType(data.data.component_type);
|
||||
|
||||
/**
|
||||
* Whether the reply to this interaction has been deferred
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.deferred = false;
|
||||
|
||||
/**
|
||||
* Whether the reply to this interaction is ephemeral
|
||||
* @type {?boolean}
|
||||
*/
|
||||
this.ephemeral = null;
|
||||
|
||||
/**
|
||||
* Whether this interaction has already been replied to
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.replied = false;
|
||||
|
||||
/**
|
||||
* An associated interaction webhook, can be used to further interact with this interaction
|
||||
* @type {InteractionWebhook}
|
||||
*/
|
||||
this.webhook = new InteractionWebhook(this.client, this.applicationId, this.token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw message components from the API
|
||||
* * APIMessageButton
|
||||
* * APIMessageSelectMenu
|
||||
* @typedef {APIMessageButton|APIMessageSelectMenu} APIMessageActionRowComponent
|
||||
*/
|
||||
|
||||
/**
|
||||
* The component which was interacted with
|
||||
* @type {?(MessageActionRowComponent|APIMessageActionRowComponent)}
|
||||
* @readonly
|
||||
*/
|
||||
get component() {
|
||||
return (
|
||||
this.message.components
|
||||
.flatMap(row => row.components)
|
||||
.find(component => (component.customId ?? component.custom_id) === this.customId) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the type of a MessageComponent
|
||||
* @param {MessageComponentTypeResolvable} type The type to resolve
|
||||
* @returns {MessageComponentType}
|
||||
* @private
|
||||
*/
|
||||
static resolveType(type) {
|
||||
return typeof type === 'string' ? type : MessageComponentTypes[type];
|
||||
}
|
||||
|
||||
// These are here only for documentation purposes - they are implemented by InteractionResponses
|
||||
/* eslint-disable no-empty-function */
|
||||
deferReply() {}
|
||||
reply() {}
|
||||
fetchReply() {}
|
||||
editReply() {}
|
||||
deleteReply() {}
|
||||
followUp() {}
|
||||
deferUpdate() {}
|
||||
update() {}
|
||||
}
|
||||
|
||||
InteractionResponses.applyToClass(MessageComponentInteraction);
|
||||
|
||||
module.exports = MessageComponentInteraction;
|
||||
|
||||
/**
|
||||
* @external APIMessageSelectMenu
|
||||
* @see {@link https://discord.com/developers/docs/interactions/message-components#select-menu-object}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external APIMessageButton
|
||||
* @see {@link https://discord.com/developers/docs/interactions/message-components#button-object}
|
||||
*/
|
||||
+474
@@ -0,0 +1,474 @@
|
||||
'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|MessageEmbedOptions} [data={}] MessageEmbed to clone or raw embed data
|
||||
*/
|
||||
|
||||
/**
|
||||
* A `Partial` object is a representation of any existing object.
|
||||
* This object contains between 0 and all of the original objects parameters.
|
||||
* This is true regardless of whether the parameters are optional in the base object.
|
||||
* @typedef {Object} Partial
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents the possible options for a MessageEmbed
|
||||
* @typedef {Object} MessageEmbedOptions
|
||||
* @property {string} [title] The title of this embed
|
||||
* @property {string} [description] The description of this embed
|
||||
* @property {string} [url] The URL of this embed
|
||||
* @property {Date|number} [timestamp] The timestamp of this embed
|
||||
* @property {ColorResolvable} [color] The color of this embed
|
||||
* @property {EmbedFieldData[]} [fields] The fields of this embed
|
||||
* @property {Partial<MessageEmbedAuthor>} [author] The author of this embed
|
||||
* @property {Partial<MessageEmbedThumbnail>} [thumbnail] The thumbnail of this embed
|
||||
* @property {Partial<MessageEmbedImage>} [image] The image of this embed
|
||||
* @property {Partial<MessageEmbedVideo>} [video] The video of this embed
|
||||
* @property {Partial<MessageEmbedFooter>} [footer] The footer of this embed
|
||||
*/
|
||||
|
||||
constructor(data = {}, skipValidation = false) {
|
||||
this.setup(data, skipValidation);
|
||||
}
|
||||
|
||||
setup(data, skipValidation) {
|
||||
/**
|
||||
* The type of this embed, either:
|
||||
* * `rich` - a generic embed rendered from embed attributes
|
||||
* * `image` - an image embed
|
||||
* * `video` - a video embed
|
||||
* * `gifv` - an animated gif image embed rendered as a video embed
|
||||
* * `article` - an article embed
|
||||
* * `link` - a link embed
|
||||
* @type {string}
|
||||
* @see {@link https://discord.com/developers/docs/resources/channel#embed-object-embed-types}
|
||||
* @deprecated
|
||||
*/
|
||||
this.type = data.type ?? 'rich';
|
||||
|
||||
/**
|
||||
* The title of this embed
|
||||
* @type {?string}
|
||||
*/
|
||||
this.title = data.title ?? null;
|
||||
|
||||
/**
|
||||
* The description of this embed
|
||||
* @type {?string}
|
||||
*/
|
||||
this.description = data.description ?? null;
|
||||
|
||||
/**
|
||||
* The URL of this embed
|
||||
* @type {?string}
|
||||
*/
|
||||
this.url = 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 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, footer text, and author name
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get length() {
|
||||
return (
|
||||
(this.title?.length ?? 0) +
|
||||
(this.description?.length ?? 0) +
|
||||
(this.fields.length >= 1
|
||||
? this.fields.reduce((prev, curr) => prev + curr.name.length + curr.value.length, 0)
|
||||
: 0) +
|
||||
(this.footer?.text.length ?? 0) +
|
||||
(this.author?.name.length ?? 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a field to the embed (max 25).
|
||||
* @param {string} name The name of this field
|
||||
* @param {string} 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 embed's fields (max 25).
|
||||
* @param {...EmbedFieldData|EmbedFieldData[]} fields The fields to set
|
||||
* @returns {MessageEmbed}
|
||||
*/
|
||||
setFields(...fields) {
|
||||
this.spliceFields(0, this.fields.length, fields);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the author of this embed.
|
||||
* @param {string} 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.verifyString(name, RangeError, 'EMBED_AUTHOR_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 {string} description The description
|
||||
* @returns {MessageEmbed}
|
||||
*/
|
||||
setDescription(description) {
|
||||
this.description = Util.verifyString(description, RangeError, 'EMBED_DESCRIPTION');
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the footer of this embed.
|
||||
* @param {string} text The text of the footer
|
||||
* @param {string} [iconURL] The icon URL of the footer
|
||||
* @returns {MessageEmbed}
|
||||
*/
|
||||
setFooter(text, iconURL) {
|
||||
this.footer = { text: Util.verifyString(text, RangeError, 'EMBED_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 {string} title The title
|
||||
* @returns {MessageEmbed}
|
||||
*/
|
||||
setTitle(title) {
|
||||
this.title = Util.verifyString(title, RangeError, 'EMBED_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 {APIEmbed} 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),
|
||||
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,
|
||||
},
|
||||
footer: this.footer && {
|
||||
text: this.footer.text,
|
||||
icon_url: this.footer.iconURL,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes field input and verifies strings.
|
||||
* @param {string} name The name of the field
|
||||
* @param {string} value The value of the field
|
||||
* @param {boolean} [inline=false] Set the field to display inline
|
||||
* @returns {EmbedField}
|
||||
*/
|
||||
static normalizeField(name, value, inline = false) {
|
||||
return {
|
||||
name: Util.verifyString(name, RangeError, 'EMBED_FIELD_NAME', false),
|
||||
value: Util.verifyString(value, RangeError, 'EMBED_FIELD_VALUE', false),
|
||||
inline,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} EmbedFieldData
|
||||
* @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
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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.name, field.value, typeof field.inline === 'boolean' ? field.inline : false),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MessageEmbed;
|
||||
|
||||
/**
|
||||
* @external APIEmbed
|
||||
* @see {@link https://discord.com/developers/docs/resources/channel#embed-object}
|
||||
*/
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/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, repliedUser) {
|
||||
/**
|
||||
* 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 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 if (roles) {
|
||||
this.roles = new Collection();
|
||||
const guild = message.guild;
|
||||
if (guild) {
|
||||
for (const mention of roles) {
|
||||
const role = 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, Channel>}
|
||||
* @private
|
||||
*/
|
||||
this._channels = null;
|
||||
|
||||
/**
|
||||
* Crossposted channel data.
|
||||
* @typedef {Object} CrosspostedChannel
|
||||
* @property {Snowflake} channelId The mentioned channel's id
|
||||
* @property {Snowflake} guildId The id of the guild that has the channel
|
||||
* @property {ChannelType} type The channel's type
|
||||
* @property {string} name The channel's name
|
||||
*/
|
||||
|
||||
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 ?? 'UNKNOWN',
|
||||
name: d.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.crosspostedChannels = new Collection();
|
||||
}
|
||||
|
||||
/**
|
||||
* The author of the message that this message is a reply to
|
||||
* @type {?User}
|
||||
*/
|
||||
this.repliedUser = repliedUser ? this.client.users._add(repliedUser) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Any members that were mentioned (only in {@link Guild}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.members.resolve(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, Channel>}
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options used to check for a mention.
|
||||
* @typedef {Object} MessageMentionsHasOptions
|
||||
* @property {boolean} [ignoreDirect=false] Whether to ignore direct mentions to the item
|
||||
* @property {boolean} [ignoreRoles=false] Whether to ignore role mentions to a guild member
|
||||
* @property {boolean} [ignoreEveryone=false] Whether to ignore everyone/here mentions
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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|ChannelResolvable} data The User/Role/Channel to check for
|
||||
* @param {MessageMentionsHasOptions} [options] The options for the check
|
||||
* @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.guild?.roles.resolveId(data) ?? this.client.channels.resolveId(data) ?? this.client.users.resolveId(data);
|
||||
|
||||
return typeof id === 'string' && (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;
|
||||
+270
@@ -0,0 +1,270 @@
|
||||
'use strict';
|
||||
|
||||
const BaseMessageComponent = require('./BaseMessageComponent');
|
||||
const MessageEmbed = require('./MessageEmbed');
|
||||
const { RangeError } = require('../errors');
|
||||
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 MessagePayload {
|
||||
/**
|
||||
* @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 {?APIMessage}
|
||||
*/
|
||||
this.data = null;
|
||||
|
||||
/**
|
||||
* @typedef {Object} MessageFile
|
||||
* @property {Buffer|string|Stream} attachment The original attachment that generated this file
|
||||
* @property {string} name The name of this file
|
||||
* @property {Buffer|Stream} file The file to be sent to the API
|
||||
*/
|
||||
|
||||
/**
|
||||
* Files sendable to the API
|
||||
* @type {?MessageFile[]}
|
||||
*/
|
||||
this.files = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the target is a {@link Webhook} or a {@link WebhookClient}
|
||||
* @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 {@link 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 {@link Message}
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get isMessage() {
|
||||
const Message = require('./Message');
|
||||
return this.target instanceof Message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wether or not the target is a {@link MessageManager}
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get isMessageManager() {
|
||||
const MessageManager = require('../managers/MessageManager');
|
||||
return this.target instanceof MessageManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the target is an {@link Interaction} or an {@link InteractionWebhook}
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get isInteraction() {
|
||||
const Interaction = require('./Interaction');
|
||||
const InteractionWebhook = require('./InteractionWebhook');
|
||||
return this.target instanceof Interaction || this.target instanceof InteractionWebhook;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the content of this message.
|
||||
* @returns {?string}
|
||||
*/
|
||||
makeContent() {
|
||||
let content;
|
||||
if (this.options.content === null) {
|
||||
content = '';
|
||||
} else if (typeof this.options.content !== 'undefined') {
|
||||
content = Util.verifyString(this.options.content, RangeError, 'MESSAGE_CONTENT_TYPE', false);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves data.
|
||||
* @returns {MessagePayload}
|
||||
*/
|
||||
resolveData() {
|
||||
if (this.data) return this;
|
||||
const isInteraction = this.isInteraction;
|
||||
const isWebhook = this.isWebhook;
|
||||
|
||||
const content = this.makeContent();
|
||||
const tts = Boolean(this.options.tts);
|
||||
|
||||
let nonce;
|
||||
if (typeof this.options.nonce !== 'undefined') {
|
||||
nonce = this.options.nonce;
|
||||
// eslint-disable-next-line max-len
|
||||
if (typeof nonce === 'number' ? !Number.isInteger(nonce) : typeof nonce !== 'string') {
|
||||
throw new RangeError('MESSAGE_NONCE_TYPE');
|
||||
}
|
||||
}
|
||||
|
||||
const components = this.options.components?.map(c => BaseMessageComponent.create(c).toJSON());
|
||||
|
||||
let username;
|
||||
let avatarURL;
|
||||
if (isWebhook) {
|
||||
username = this.options.username ?? this.target.name;
|
||||
if (this.options.avatarURL) avatarURL = this.options.avatarURL;
|
||||
}
|
||||
|
||||
let flags;
|
||||
if (this.isMessage || this.isMessageManager) {
|
||||
// eslint-disable-next-line eqeqeq
|
||||
flags = this.options.flags != null ? new MessageFlags(this.options.flags).bitfield : this.target.flags?.bitfield;
|
||||
} else if (isInteraction && this.options.ephemeral) {
|
||||
flags = MessageFlags.FLAGS.EPHEMERAL;
|
||||
}
|
||||
|
||||
let allowedMentions =
|
||||
typeof this.options.allowedMentions === 'undefined'
|
||||
? this.target.client.options.allowedMentions
|
||||
: this.options.allowedMentions;
|
||||
|
||||
if (allowedMentions) {
|
||||
allowedMentions = Util.cloneObject(allowedMentions);
|
||||
allowedMentions.replied_user = allowedMentions.repliedUser;
|
||||
delete allowedMentions.repliedUser;
|
||||
}
|
||||
|
||||
let message_reference;
|
||||
if (typeof this.options.reply === 'object') {
|
||||
const reference = this.options.reply.messageReference;
|
||||
const message_id = this.isMessage ? reference.id ?? reference : this.target.messages.resolveId(reference);
|
||||
if (message_id) {
|
||||
message_reference = {
|
||||
message_id,
|
||||
fail_if_not_exists: this.options.reply.failIfNotExists ?? this.target.client.options.failIfNotExists,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
this.data = {
|
||||
content,
|
||||
tts,
|
||||
nonce,
|
||||
embeds: this.options.embeds?.map(embed => new MessageEmbed(embed).toJSON()),
|
||||
components,
|
||||
username,
|
||||
avatar_url: avatarURL,
|
||||
allowed_mentions:
|
||||
typeof content === 'undefined' && typeof message_reference === 'undefined' ? undefined : allowedMentions,
|
||||
flags,
|
||||
message_reference,
|
||||
attachments: this.options.attachments,
|
||||
sticker_ids: this.options.stickers?.map(sticker => sticker.id ?? sticker),
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves files.
|
||||
* @returns {Promise<MessagePayload>}
|
||||
*/
|
||||
async resolveFiles() {
|
||||
if (this.files) return this;
|
||||
|
||||
this.files = await Promise.all(this.options.files?.map(file => this.constructor.resolveFile(file)) ?? []);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {MessageFile}
|
||||
*/
|
||||
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 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 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link MessagePayload} from user-level arguments.
|
||||
* @param {MessageTarget} target Target to send to
|
||||
* @param {string|MessageOptions|WebhookMessageOptions} options Options or content to use
|
||||
* @param {MessageOptions|WebhookMessageOptions} [extra={}] - Extra options to add onto specified options
|
||||
* @returns {MessagePayload}
|
||||
*/
|
||||
static create(target, options, extra = {}) {
|
||||
return new this(
|
||||
target,
|
||||
typeof options !== 'object' || options === null ? { content: options, ...extra } : { ...options, ...extra },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MessagePayload;
|
||||
|
||||
/**
|
||||
* A target for a message.
|
||||
* @typedef {TextChannel|DMChannel|User|GuildMember|Webhook|WebhookClient|Interaction|InteractionWebhook|
|
||||
* Message|MessageManager} MessageTarget
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external APIMessage
|
||||
* @see {@link https://discord.com/developers/docs/resources/channel#message-object}
|
||||
*/
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
'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 {APIReaction} data The data for the message reaction
|
||||
* @param {Message} message The message the reaction refers to
|
||||
*/
|
||||
constructor(client, data, message) {
|
||||
/**
|
||||
* The client that instantiated this message reaction
|
||||
* @name MessageReaction#client
|
||||
* @type {Client}
|
||||
* @readonly
|
||||
*/
|
||||
Object.defineProperty(this, 'client', { value: client });
|
||||
|
||||
/**
|
||||
* The message that this reaction refers to
|
||||
* @type {Message}
|
||||
*/
|
||||
this.message = message;
|
||||
|
||||
/**
|
||||
* Whether the client has given this reaction
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.me = data.me;
|
||||
|
||||
/**
|
||||
* A manager of the users that have given this reaction
|
||||
* @type {ReactionUserManager}
|
||||
*/
|
||||
this.users = new ReactionUserManager(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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all users from this reaction.
|
||||
* @returns {Promise<MessageReaction>}
|
||||
*/
|
||||
async remove() {
|
||||
await this.client.api
|
||||
.channels(this.message.channelId)
|
||||
.messages(this.message.id)
|
||||
.reactions(this._emoji.identifier)
|
||||
.delete();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The emoji of this reaction. Either a {@link GuildEmoji} object for known custom emojis, or a {@link 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;
|
||||
|
||||
/**
|
||||
* @external APIReaction
|
||||
* @see {@link https://discord.com/developers/docs/resources/channel#reaction-object}
|
||||
*/
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
'use strict';
|
||||
|
||||
const BaseMessageComponent = require('./BaseMessageComponent');
|
||||
const { MessageComponentTypes } = require('../util/Constants');
|
||||
const Util = require('../util/Util');
|
||||
|
||||
/**
|
||||
* Represents a select menu message component
|
||||
* @extends {BaseMessageComponent}
|
||||
*/
|
||||
class MessageSelectMenu extends BaseMessageComponent {
|
||||
/**
|
||||
* @typedef {BaseMessageComponentOptions} MessageSelectMenuOptions
|
||||
* @property {string} [customId] A unique string to be sent in the interaction when clicked
|
||||
* @property {string} [placeholder] Custom placeholder text to display when nothing is selected
|
||||
* @property {number} [minValues] The minimum number of selections required
|
||||
* @property {number} [maxValues] The maximum number of selections allowed
|
||||
* @property {MessageSelectOption[]} [options] Options for the select menu
|
||||
* @property {boolean} [disabled=false] Disables the select menu to prevent interactions
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} MessageSelectOption
|
||||
* @property {string} label The text to be displayed on this option
|
||||
* @property {string} value The value to be sent for this option
|
||||
* @property {?string} description Optional description to show for this option
|
||||
* @property {?RawEmoji} emoji Emoji to display for this option
|
||||
* @property {boolean} default Render this option as the default selection
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} MessageSelectOptionData
|
||||
* @property {string} label The text to be displayed on this option
|
||||
* @property {string} value The value to be sent for this option
|
||||
* @property {string} [description] Optional description to show for this option
|
||||
* @property {EmojiIdentifierResolvable} [emoji] Emoji to display for this option
|
||||
* @property {boolean} [default] Render this option as the default selection
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {MessageSelectMenu|MessageSelectMenuOptions} [data={}] MessageSelectMenu to clone or raw data
|
||||
*/
|
||||
constructor(data = {}) {
|
||||
super({ type: 'SELECT_MENU' });
|
||||
|
||||
this.setup(data);
|
||||
}
|
||||
|
||||
setup(data) {
|
||||
/**
|
||||
* A unique string to be sent in the interaction when clicked
|
||||
* @type {?string}
|
||||
*/
|
||||
this.customId = data.custom_id ?? data.customId ?? null;
|
||||
|
||||
/**
|
||||
* Custom placeholder text to display when nothing is selected
|
||||
* @type {?string}
|
||||
*/
|
||||
this.placeholder = data.placeholder ?? null;
|
||||
|
||||
/**
|
||||
* The minimum number of selections required
|
||||
* @type {?number}
|
||||
*/
|
||||
this.minValues = data.min_values ?? data.minValues ?? null;
|
||||
|
||||
/**
|
||||
* The maximum number of selections allowed
|
||||
* @type {?number}
|
||||
*/
|
||||
this.maxValues = data.max_values ?? data.maxValues ?? null;
|
||||
|
||||
/**
|
||||
* Options for the select menu
|
||||
* @type {MessageSelectOption[]}
|
||||
*/
|
||||
this.options = this.constructor.normalizeOptions(data.options ?? []);
|
||||
|
||||
/**
|
||||
* Whether this select menu is currently disabled
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.disabled = data.disabled ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the custom id of this select menu
|
||||
* @param {string} customId A unique string to be sent in the interaction when clicked
|
||||
* @returns {MessageSelectMenu}
|
||||
*/
|
||||
setCustomId(customId) {
|
||||
this.customId = Util.verifyString(customId, RangeError, 'SELECT_MENU_CUSTOM_ID');
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the interactive status of the select menu
|
||||
* @param {boolean} [disabled=true] Whether this select menu should be disabled
|
||||
* @returns {MessageSelectMenu}
|
||||
*/
|
||||
setDisabled(disabled = true) {
|
||||
this.disabled = disabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum number of selections allowed for this select menu
|
||||
* @param {number} maxValues Number of selections to be allowed
|
||||
* @returns {MessageSelectMenu}
|
||||
*/
|
||||
setMaxValues(maxValues) {
|
||||
this.maxValues = maxValues;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the minimum number of selections required for this select menu
|
||||
* <info>This will default the maxValues to the number of options, unless manually set</info>
|
||||
* @param {number} minValues Number of selections to be required
|
||||
* @returns {MessageSelectMenu}
|
||||
*/
|
||||
setMinValues(minValues) {
|
||||
this.minValues = minValues;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the placeholder of this select menu
|
||||
* @param {string} placeholder Custom placeholder text to display when nothing is selected
|
||||
* @returns {MessageSelectMenu}
|
||||
*/
|
||||
setPlaceholder(placeholder) {
|
||||
this.placeholder = Util.verifyString(placeholder, RangeError, 'SELECT_MENU_PLACEHOLDER');
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds options to the select menu.
|
||||
* @param {...MessageSelectOptionData|MessageSelectOptionData[]} options The options to add
|
||||
* @returns {MessageSelectMenu}
|
||||
*/
|
||||
addOptions(...options) {
|
||||
this.options.push(...this.constructor.normalizeOptions(options));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes, replaces, and inserts options in the select menu.
|
||||
* @param {number} index The index to start at
|
||||
* @param {number} deleteCount The number of options to remove
|
||||
* @param {...MessageSelectOptionData|MessageSelectOptionData[]} [options] The replacing option objects
|
||||
* @returns {MessageSelectMenu}
|
||||
*/
|
||||
spliceOptions(index, deleteCount, ...options) {
|
||||
this.options.splice(index, deleteCount, ...this.constructor.normalizeOptions(...options));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms this select menu to a plain object
|
||||
* @returns {Object} The raw data of this select menu
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
custom_id: this.customId,
|
||||
disabled: this.disabled,
|
||||
placeholder: this.placeholder,
|
||||
min_values: this.minValues,
|
||||
max_values: this.maxValues ?? (this.minValues ? this.options.length : undefined),
|
||||
options: this.options,
|
||||
type: typeof this.type === 'string' ? MessageComponentTypes[this.type] : this.type,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes option input and resolves strings and emojis.
|
||||
* @param {MessageSelectOptionData} option The select menu option to normalize
|
||||
* @returns {MessageSelectOption}
|
||||
*/
|
||||
static normalizeOption(option) {
|
||||
let { label, value, description, emoji } = option;
|
||||
|
||||
label = Util.verifyString(label, RangeError, 'SELECT_OPTION_LABEL');
|
||||
value = Util.verifyString(value, RangeError, 'SELECT_OPTION_VALUE');
|
||||
emoji = emoji ? Util.resolvePartialEmoji(emoji) : null;
|
||||
description = description ? Util.verifyString(description, RangeError, 'SELECT_OPTION_DESCRIPTION', true) : null;
|
||||
|
||||
return { label, value, description, emoji, default: option.default ?? false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes option input and resolves strings and emojis.
|
||||
* @param {...MessageSelectOptionData|MessageSelectOptionData[]} options The select menu options to normalize
|
||||
* @returns {MessageSelectOption[]}
|
||||
*/
|
||||
static normalizeOptions(...options) {
|
||||
return options.flat(Infinity).map(option => this.normalizeOption(option));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MessageSelectMenu;
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
'use strict';
|
||||
|
||||
const BaseGuildTextChannel = require('./BaseGuildTextChannel');
|
||||
const { Error } = require('../errors');
|
||||
|
||||
/**
|
||||
* Represents a guild news channel on Discord.
|
||||
* @extends {BaseGuildTextChannel}
|
||||
*/
|
||||
class NewsChannel extends BaseGuildTextChannel {
|
||||
/**
|
||||
* 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 === 'GUILD_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;
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
const BaseGuild = require('./BaseGuild');
|
||||
const Permissions = require('../util/Permissions');
|
||||
|
||||
/**
|
||||
* A partial guild received when using {@link GuildManager#fetch} to fetch multiple guilds.
|
||||
* @extends {BaseGuild}
|
||||
*/
|
||||
class OAuth2Guild extends BaseGuild {
|
||||
constructor(client, data) {
|
||||
super(client, data);
|
||||
|
||||
/**
|
||||
* Whether the client user is the owner of the guild
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.owner = data.owner;
|
||||
|
||||
/**
|
||||
* The permissions that the client user has in this guild
|
||||
* @type {Readonly<Permissions>}
|
||||
*/
|
||||
this.permissions = new Permissions(BigInt(data.permissions)).freeze();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OAuth2Guild;
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
'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 {StaticImageURLOptions} [options={}] Options for the Image URL
|
||||
* @returns {?string}
|
||||
*/
|
||||
iconURL({ format, size } = {}) {
|
||||
return this.icon && 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;
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
'use strict';
|
||||
|
||||
const Base = require('./Base');
|
||||
const Role = require('./Role');
|
||||
const { TypeError } = require('../errors');
|
||||
const { OverwriteTypes } = require('../util/Constants');
|
||||
const Permissions = require('../util/Permissions');
|
||||
|
||||
/**
|
||||
* Represents a permission overwrite for a role or member in a guild channel.
|
||||
* @extends {Base}
|
||||
*/
|
||||
class PermissionOverwrites extends Base {
|
||||
constructor(client, data, channel) {
|
||||
super(client);
|
||||
|
||||
/**
|
||||
* The GuildChannel this overwrite is for
|
||||
* @name PermissionOverwrites#channel
|
||||
* @type {GuildChannel}
|
||||
* @readonly
|
||||
*/
|
||||
Object.defineProperty(this, 'channel', { value: channel });
|
||||
|
||||
if (data) this._patch(data);
|
||||
}
|
||||
|
||||
_patch(data) {
|
||||
/**
|
||||
* The overwrite's id, either a {@link User} or a {@link Role} id
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.id = data.id;
|
||||
|
||||
/**
|
||||
* The type of this overwrite
|
||||
* @type {OverwriteType}
|
||||
*/
|
||||
this.type = typeof data.type === 'number' ? OverwriteTypes[data.type] : data.type;
|
||||
|
||||
/**
|
||||
* The permissions that are denied for the user or role.
|
||||
* @type {Readonly<Permissions>}
|
||||
*/
|
||||
this.deny = new Permissions(BigInt(data.deny)).freeze();
|
||||
|
||||
/**
|
||||
* The permissions that are allowed for the user or role.
|
||||
* @type {Readonly<Permissions>}
|
||||
*/
|
||||
this.allow = new Permissions(BigInt(data.allow)).freeze();
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits this Permission Overwrite.
|
||||
* @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.edit({
|
||||
* SEND_MESSAGES: false
|
||||
* })
|
||||
* .then(channel => console.log(channel.permissionOverwrites.get(message.author.id)))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async edit(options, reason) {
|
||||
await this.channel.permissionOverwrites.upsert(this.id, options, { type: OverwriteTypes[this.type], reason }, this);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes this Permission Overwrite.
|
||||
* @param {string} [reason] Reason for deleting this overwrite
|
||||
* @returns {Promise<PermissionOverwrites>}
|
||||
*/
|
||||
async delete(reason) {
|
||||
await this.channel.permissionOverwrites.delete(this.id, reason);
|
||||
return this;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
type: OverwriteTypes[this.type],
|
||||
allow: this.allow,
|
||||
deny: this.deny,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {ResolvedOverwriteOptions} initialPermissions The initial 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(perm);
|
||||
deny.remove(perm);
|
||||
} else if (value === false) {
|
||||
allow.remove(perm);
|
||||
deny.add(perm);
|
||||
} else if (value === null) {
|
||||
allow.remove(perm);
|
||||
deny.remove(perm);
|
||||
}
|
||||
}
|
||||
|
||||
return { allow, deny };
|
||||
}
|
||||
|
||||
/**
|
||||
* The raw data for a permission overwrite
|
||||
* @typedef {Object} RawOverwriteData
|
||||
* @property {Snowflake} id The id of the {@link Role} or {@link User} this overwrite belongs to
|
||||
* @property {string} allow The permissions to allow
|
||||
* @property {string} deny The permissions to deny
|
||||
* @property {number} type The type of this OverwriteData
|
||||
*/
|
||||
|
||||
/**
|
||||
* Data that can be resolved into {@link RawOverwriteData}. This can be:
|
||||
* * PermissionOverwrites
|
||||
* * OverwriteData
|
||||
* @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' && overwrite.type in OverwriteTypes) {
|
||||
return {
|
||||
id: overwrite.id,
|
||||
type: OverwriteTypes[overwrite.type],
|
||||
allow: Permissions.resolve(overwrite.allow ?? Permissions.defaultBit).toString(),
|
||||
deny: Permissions.resolve(overwrite.deny ?? Permissions.defaultBit).toString(),
|
||||
};
|
||||
}
|
||||
|
||||
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 ? OverwriteTypes.role : OverwriteTypes.member;
|
||||
|
||||
return {
|
||||
id: userOrRole.id,
|
||||
type,
|
||||
allow: Permissions.resolve(overwrite.allow ?? Permissions.defaultBit).toString(),
|
||||
deny: Permissions.resolve(overwrite.deny ?? Permissions.defaultBit).toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PermissionOverwrites;
|
||||
+382
@@ -0,0 +1,382 @@
|
||||
'use strict';
|
||||
|
||||
const Base = require('./Base');
|
||||
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.
|
||||
* @extends {Base}
|
||||
*/
|
||||
class Presence extends Base {
|
||||
/**
|
||||
* @param {Client} client The instantiating client
|
||||
* @param {APIPresence} [data={}] The data for the presence
|
||||
*/
|
||||
constructor(client, data = {}) {
|
||||
super(client);
|
||||
|
||||
/**
|
||||
* The presence's user id
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.userId = data.user.id;
|
||||
|
||||
/**
|
||||
* The guild this presence is in
|
||||
* @type {?Guild}
|
||||
*/
|
||||
this.guild = data.guild ?? null;
|
||||
|
||||
this._patch(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* The user of this presence
|
||||
* @type {?User}
|
||||
* @readonly
|
||||
*/
|
||||
get user() {
|
||||
return this.client.users.resolve(this.userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* The member of this presence
|
||||
* @type {?GuildMember}
|
||||
* @readonly
|
||||
*/
|
||||
get member() {
|
||||
return this.guild.members.resolve(this.userId);
|
||||
}
|
||||
|
||||
_patch(data) {
|
||||
/**
|
||||
* The status of this presence
|
||||
* @type {PresenceStatus}
|
||||
*/
|
||||
this.status = data.status ?? this.status ?? 'offline';
|
||||
|
||||
/**
|
||||
* The activities of this presence
|
||||
* @type {Activity[]}
|
||||
*/
|
||||
this.activities = data.activities?.map(activity => new Activity(this, activity)) ?? [];
|
||||
|
||||
/**
|
||||
* 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The platform of this activity:
|
||||
* * **`desktop`**
|
||||
* * **`samsung`** - playing on Samsung Galaxy
|
||||
* * **`xbox`** - playing on Xbox Live
|
||||
* @typedef {string} ActivityPlatform
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents an activity that is part of a user's presence.
|
||||
*/
|
||||
class Activity {
|
||||
constructor(presence, data) {
|
||||
Object.defineProperty(this, 'presence', { value: presence });
|
||||
|
||||
/**
|
||||
* The activity's id
|
||||
* @type {string}
|
||||
*/
|
||||
this.id = data.id;
|
||||
|
||||
/**
|
||||
* The activity's name
|
||||
* @type {string}
|
||||
*/
|
||||
this.name = data.name;
|
||||
|
||||
/**
|
||||
* The activity status's type
|
||||
* @type {ActivityType}
|
||||
*/
|
||||
this.type = typeof data.type === 'number' ? ActivityTypes[data.type] : 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;
|
||||
|
||||
/**
|
||||
* The id of the application associated with this activity
|
||||
* @type {?Snowflake}
|
||||
*/
|
||||
this.applicationId = data.application_id ?? null;
|
||||
|
||||
/**
|
||||
* Represents timestamps of an activity
|
||||
* @typedef {Object} ActivityTimestamps
|
||||
* @property {?Date} start When the activity started
|
||||
* @property {?Date} end When the activity will end
|
||||
*/
|
||||
|
||||
/**
|
||||
* Timestamps for the activity
|
||||
* @type {?ActivityTimestamps}
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* The Spotify song's id
|
||||
* @type {?string}
|
||||
*/
|
||||
this.syncId = data.sync_id ?? null;
|
||||
|
||||
/**
|
||||
* The platform the game is being played on
|
||||
* @type {?ActivityPlatform}
|
||||
*/
|
||||
this.platform = data.platform ?? null;
|
||||
|
||||
/**
|
||||
* Represents a party of an activity
|
||||
* @typedef {Object} ActivityParty
|
||||
* @property {?string} id The party's id
|
||||
* @property {number[]} size Size of the party as `[current, max]`
|
||||
*/
|
||||
|
||||
/**
|
||||
* Party of the activity
|
||||
* @type {?ActivityParty}
|
||||
*/
|
||||
this.party = data.party ?? null;
|
||||
|
||||
/**
|
||||
* Assets for rich presence
|
||||
* @type {?RichPresenceAssets}
|
||||
*/
|
||||
this.assets = data.assets ? new RichPresenceAssets(this, data.assets) : null;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* The game's or Spotify session's id
|
||||
* @type {?string}
|
||||
*/
|
||||
this.sessionId = data.session_id ?? null;
|
||||
|
||||
/**
|
||||
* The labels of the buttons of this rich presence
|
||||
* @type {string[]}
|
||||
*/
|
||||
this.buttons = data.buttons ?? [];
|
||||
|
||||
/**
|
||||
* 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 &&
|
||||
this.state === activity.state &&
|
||||
this.details === activity.details)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* The large image asset's id
|
||||
* @type {?Snowflake}
|
||||
*/
|
||||
this.largeImage = assets.large_image ?? null;
|
||||
|
||||
/**
|
||||
* The small image asset's id
|
||||
* @type {?Snowflake}
|
||||
*/
|
||||
this.smallImage = assets.small_image ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the URL of the small image asset
|
||||
* @param {StaticImageURLOptions} [options] Options for the image url
|
||||
* @returns {?string}
|
||||
*/
|
||||
smallImageURL({ format, size } = {}) {
|
||||
return (
|
||||
this.smallImage &&
|
||||
this.activity.presence.client.rest.cdn.AppAsset(this.activity.applicationId, this.smallImage, {
|
||||
format,
|
||||
size,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the URL of the large image asset
|
||||
* @param {StaticImageURLOptions} [options] Options for the image url
|
||||
* @returns {?string}
|
||||
*/
|
||||
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;
|
||||
|
||||
/* eslint-disable max-len */
|
||||
/**
|
||||
* @external APIPresence
|
||||
* @see {@link https://discord.com/developers/docs/rich-presence/how-to#updating-presence-update-presence-payload-fields}
|
||||
*/
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const Collector = require('./interfaces/Collector');
|
||||
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 {ReactionCollectorOptions} [options={}] The options to apply to this collector
|
||||
*/
|
||||
constructor(message, options = {}) {
|
||||
super(message.client, 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
|
||||
* @param {User} user The user that added the reaction
|
||||
* @returns {Promise<?(Snowflake|string)>}
|
||||
* @private
|
||||
*/
|
||||
async collect(reaction, user) {
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Emitted whenever a reaction is newly created on a message. Will emit only when a new reaction is
|
||||
* added to the message, as opposed to {@link Collector#collect} which which will
|
||||
* be emitted even when a reaction has already been added to the message.
|
||||
* @event ReactionCollector#create
|
||||
* @param {MessageReaction} reaction The reaction that was added
|
||||
* @param {User} user The user that added the reaction
|
||||
*/
|
||||
if (reaction.count === 1 && (await this.filter(reaction, user, this.collected))) {
|
||||
this.emit('create', reaction, user);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* The reason this collector has ended with, or null if it hasn't ended yet
|
||||
* @type {?string}
|
||||
* @readonly
|
||||
*/
|
||||
get 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.channelId) {
|
||||
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 (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
@@ -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;
|
||||
+399
@@ -0,0 +1,399 @@
|
||||
'use strict';
|
||||
|
||||
const Base = require('./Base');
|
||||
const { Error, TypeError } = require('../errors');
|
||||
const Permissions = require('../util/Permissions');
|
||||
const SnowflakeUtil = require('../util/SnowflakeUtil');
|
||||
const Util = require('../util/Util');
|
||||
|
||||
/**
|
||||
* Represents a role on Discord.
|
||||
* @extends {Base}
|
||||
*/
|
||||
class Role extends Base {
|
||||
/**
|
||||
* @param {Client} client The instantiating client
|
||||
* @param {APIRole} 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 role's id (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(BigInt(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 tags this role has
|
||||
* @type {?Object}
|
||||
* @property {Snowflake} [botId] The id of the bot this role belongs to
|
||||
* @property {Snowflake} [integrationId] The id of the integration this role belongs to
|
||||
* @property {true} [premiumSubscriberRole] Whether this is the guild's premium subscription role
|
||||
*/
|
||||
this.tags = data.tags ? {} : null;
|
||||
if (data.tags) {
|
||||
if ('bot_id' in data.tags) {
|
||||
this.tags.botId = data.tags.bot_id;
|
||||
}
|
||||
if ('integration_id' in data.tags) {
|
||||
this.tags.integrationId = data.tags.integration_id;
|
||||
}
|
||||
if ('premium_subscriber' in data.tags) {
|
||||
this.tags.premiumSubscriberRole = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The timestamp the role was created at
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get createdTimestamp() {
|
||||
return SnowflakeUtil.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.members.resolve(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.values()].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);
|
||||
*/
|
||||
edit(data, reason) {
|
||||
return this.guild.roles.edit(this, data, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `channel.permissionsFor(role)`. Returns permissions for a role in a guild channel,
|
||||
* taking into account permission overwrites.
|
||||
* @param {GuildChannel|Snowflake} 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([Permissions.FLAGS.KICK_MEMBERS, Permissions.FLAGS.BAN_MEMBERS])
|
||||
* .then(updated => console.log(`Updated permissions to ${updated.permissions.bitfield}`))
|
||||
* .catch(console.error);
|
||||
* @example
|
||||
* // Remove all permissions from a role
|
||||
* role.setPermissions(0n)
|
||||
* .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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Options used to set position of a role.
|
||||
* @typedef {Object} SetRolePositionOptions
|
||||
* @property {boolean} [relative=false] Whether to change the position relative to its current value or not
|
||||
* @property {string} [reason] The reason for changing the position
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sets the new position of the role.
|
||||
* @param {number} position The new position for the role
|
||||
* @param {SetRolePositionOptions} [options] Options for setting 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);
|
||||
*/
|
||||
async setPosition(position, { relative, reason } = {}) {
|
||||
const updatedRoles = await Util.setPosition(
|
||||
this,
|
||||
position,
|
||||
relative,
|
||||
this.guild._sortedRoles(),
|
||||
this.client.api.guilds(this.guild.id).roles,
|
||||
reason,
|
||||
);
|
||||
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);
|
||||
*/
|
||||
async delete(reason) {
|
||||
await this.client.api.guilds[this.guild.id].roles[this.id].delete({ reason });
|
||||
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;
|
||||
|
||||
/**
|
||||
* @external APIRole
|
||||
* @see {@link https://discord.com/developers/docs/topics/permissions#role-object}
|
||||
*/
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
const MessageComponentInteraction = require('./MessageComponentInteraction');
|
||||
|
||||
/**
|
||||
* Represents a select menu interaction.
|
||||
* @extends {MessageComponentInteraction}
|
||||
*/
|
||||
class SelectMenuInteraction extends MessageComponentInteraction {
|
||||
constructor(client, data) {
|
||||
super(client, data);
|
||||
|
||||
/**
|
||||
* The values selected, if the component which was interacted with was a select menu
|
||||
* @type {string[]}
|
||||
*/
|
||||
this.values = data.data.values ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SelectMenuInteraction;
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
'use strict';
|
||||
|
||||
const BaseGuildVoiceChannel = require('./BaseGuildVoiceChannel');
|
||||
|
||||
/**
|
||||
* Represents a guild stage channel on Discord.
|
||||
* @extends {BaseGuildVoiceChannel}
|
||||
*/
|
||||
class StageChannel extends BaseGuildVoiceChannel {
|
||||
_patch(data) {
|
||||
super._patch(data);
|
||||
|
||||
if ('topic' in data) {
|
||||
/**
|
||||
* The topic of the stage channel
|
||||
* @type {?string}
|
||||
*/
|
||||
this.topic = data.topic;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The stage instance of this stage channel, if it exists
|
||||
* @type {?StageInstance}
|
||||
* @readonly
|
||||
*/
|
||||
get stageInstance() {
|
||||
return this.guild.stageInstances.cache.find(stageInstance => stageInstance.channelId === this.id) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a stage instance associated to this stage channel.
|
||||
* @param {StageInstanceCreateOptions} options The options to create the stage instance
|
||||
* @returns {Promise<StageInstance>}
|
||||
*/
|
||||
createStageInstance(options) {
|
||||
return this.guild.stageInstances.create(this.id, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 the RTC region of the channel.
|
||||
* @name StageChannel#setRTCRegion
|
||||
* @param {?string} region The new region of the channel. Set to `null` to remove a specific region for the channel
|
||||
* @returns {Promise<StageChannel>}
|
||||
* @example
|
||||
* // Set the RTC region to europe
|
||||
* stageChannel.setRTCRegion('europe');
|
||||
* @example
|
||||
* // Remove a fixed region for this channel - let Discord decide automatically
|
||||
* stageChannel.setRTCRegion(null);
|
||||
*/
|
||||
}
|
||||
|
||||
module.exports = StageChannel;
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
'use strict';
|
||||
|
||||
const Base = require('./Base');
|
||||
const { PrivacyLevels } = require('../util/Constants');
|
||||
const SnowflakeUtil = require('../util/SnowflakeUtil');
|
||||
|
||||
/**
|
||||
* Represents a stage instance.
|
||||
* @extends {Base}
|
||||
*/
|
||||
class StageInstance extends Base {
|
||||
constructor(client, data) {
|
||||
super(client);
|
||||
|
||||
/**
|
||||
* The stage instance's id
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.id = data.id;
|
||||
|
||||
/**
|
||||
* Whether the stage instance has been deleted
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.deleted = false;
|
||||
|
||||
this._patch(data);
|
||||
}
|
||||
|
||||
_patch(data) {
|
||||
/**
|
||||
* The id of the guild associated with the stage channel
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.guildId = data.guild_id;
|
||||
|
||||
/**
|
||||
* The id of the channel associated with the stage channel
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.channelId = data.channel_id;
|
||||
|
||||
/**
|
||||
* The topic of the stage instance
|
||||
* @type {string}
|
||||
*/
|
||||
this.topic = data.topic;
|
||||
|
||||
/**
|
||||
* The privacy level of the stage instance
|
||||
* @type {PrivacyLevel}
|
||||
*/
|
||||
this.privacyLevel = PrivacyLevels[data.privacy_level];
|
||||
|
||||
/**
|
||||
* Whether or not stage discovery is disabled
|
||||
* @type {?boolean}
|
||||
*/
|
||||
this.discoverableDisabled = data.discoverable_disabled ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The stage channel associated with this stage instance
|
||||
* @type {?StageChannel}
|
||||
* @readonly
|
||||
*/
|
||||
get channel() {
|
||||
return this.client.channels.resolve(this.channelId);
|
||||
}
|
||||
|
||||
/**
|
||||
* The guild this stage instance belongs to
|
||||
* @type {?Guild}
|
||||
* @readonly
|
||||
*/
|
||||
get guild() {
|
||||
return this.client.guilds.resolve(this.guildId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits this stage instance.
|
||||
* @param {StageInstanceEditOptions} options The options to edit the stage instance
|
||||
* @returns {Promise<StageInstance>}
|
||||
* @example
|
||||
* // Edit a stage instance
|
||||
* stageInstance.edit({ topic: 'new topic' })
|
||||
* .then(stageInstance => console.log(stageInstance))
|
||||
* .catch(console.error)
|
||||
*/
|
||||
edit(options) {
|
||||
return this.guild.stageInstances.edit(this.channelId, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes this stage instance.
|
||||
* @returns {Promise<StageInstance>}
|
||||
* @example
|
||||
* // Delete a stage instance
|
||||
* stageInstance.delete()
|
||||
* .then(stageInstance => console.log(stageInstance))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async delete() {
|
||||
await this.guild.stageInstances.delete(this.channelId);
|
||||
const clone = this._clone();
|
||||
clone.deleted = true;
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the topic of this stage instance.
|
||||
* @param {string} topic The topic for the stage instance
|
||||
* @returns {Promise<StageInstance>}
|
||||
* @example
|
||||
* // Set topic of a stage instance
|
||||
* stageInstance.setTopic('new topic')
|
||||
* .then(stageInstance => console.log(`Set the topic to: ${stageInstance.topic}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
setTopic(topic) {
|
||||
return this.guild.stageInstances.edit(this.channelId, { topic });
|
||||
}
|
||||
|
||||
/**
|
||||
* The timestamp this stage instances was created at
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get createdTimestamp() {
|
||||
return SnowflakeUtil.deconstruct(this.id).timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* The time this stage instance was created at
|
||||
* @type {Date}
|
||||
* @readonly
|
||||
*/
|
||||
get createdAt() {
|
||||
return new Date(this.createdTimestamp);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StageInstance;
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
'use strict';
|
||||
|
||||
const Base = require('./Base');
|
||||
const { StickerFormatTypes, StickerTypes } = require('../util/Constants');
|
||||
const SnowflakeUtil = require('../util/SnowflakeUtil');
|
||||
|
||||
/**
|
||||
* Represents a Sticker.
|
||||
* @extends {Base}
|
||||
*/
|
||||
class Sticker extends Base {
|
||||
/**
|
||||
* @param {Client} client The instantiating client
|
||||
* @param {APISticker | APIStickerItem} sticker The data for the sticker
|
||||
*/
|
||||
constructor(client, sticker) {
|
||||
super(client);
|
||||
|
||||
this._patch(sticker);
|
||||
}
|
||||
|
||||
_patch(sticker) {
|
||||
/**
|
||||
* The sticker's id
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.id = sticker.id;
|
||||
|
||||
/**
|
||||
* The description of the sticker
|
||||
* @type {?string}
|
||||
*/
|
||||
this.description = sticker.description ?? null;
|
||||
|
||||
/**
|
||||
* The type of the sticker
|
||||
* @type {?StickerType}
|
||||
*/
|
||||
this.type = StickerTypes[sticker.type] ?? null;
|
||||
|
||||
/**
|
||||
* The format of the sticker
|
||||
* @type {StickerFormatType}
|
||||
*/
|
||||
this.format = StickerFormatTypes[sticker.format_type];
|
||||
|
||||
/**
|
||||
* The name of the sticker
|
||||
* @type {string}
|
||||
*/
|
||||
this.name = sticker.name;
|
||||
|
||||
/**
|
||||
* The id of the pack the sticker is from, for standard stickers
|
||||
* @type {?Snowflake}
|
||||
*/
|
||||
this.packId = sticker.pack_id ?? null;
|
||||
|
||||
/**
|
||||
* An array of tags for the sticker
|
||||
* @type {?string[]}
|
||||
*/
|
||||
this.tags = sticker.tags?.split(', ') ?? null;
|
||||
|
||||
/**
|
||||
* Whether or not the guild sticker is available
|
||||
* @type {?boolean}
|
||||
*/
|
||||
this.available = sticker.available ?? null;
|
||||
|
||||
/**
|
||||
* The id of the guild that owns this sticker
|
||||
* @type {?Snowflake}
|
||||
*/
|
||||
this.guildId = sticker.guild_id ?? null;
|
||||
|
||||
/**
|
||||
* The user that uploaded the guild sticker
|
||||
* @type {?User}
|
||||
*/
|
||||
this.user = sticker.user ? this.client.users.add(sticker.user) : null;
|
||||
|
||||
/**
|
||||
* The standard sticker's sort order within its pack
|
||||
* @type {?number}
|
||||
*/
|
||||
this.sortValue = sticker.sort_value ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The timestamp the sticker was created at
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get createdTimestamp() {
|
||||
return SnowflakeUtil.deconstruct(this.id).timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* The time the sticker was created at
|
||||
* @type {Date}
|
||||
* @readonly
|
||||
*/
|
||||
get createdAt() {
|
||||
return new Date(this.createdTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this sticker is partial
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get partial() {
|
||||
return !this.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* The guild that owns this sticker
|
||||
* @type {?Guild}
|
||||
* @readonly
|
||||
*/
|
||||
get guild() {
|
||||
return this.client.guilds.resolve(this.guildId);
|
||||
}
|
||||
|
||||
/**
|
||||
* A link to the sticker
|
||||
* <info>If the sticker's format is LOTTIE, it returns the URL of the Lottie json file.</info>
|
||||
* @type {string}
|
||||
*/
|
||||
get url() {
|
||||
return this.client.rest.cdn.Sticker(this.id, this.format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches this sticker.
|
||||
* @returns {Promise<Sticker>}
|
||||
*/
|
||||
async fetch() {
|
||||
const data = await this.client.api.stickers(this.id).get();
|
||||
this._patch(data);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the pack this sticker is part of from Discord, if this is a Nitro sticker.
|
||||
* @returns {Promise<?StickerPack>}
|
||||
*/
|
||||
async fetchPack() {
|
||||
return (this.packId && (await this.client.fetchPremiumStickerPacks()).get(this.packId)) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the user who uploaded this sticker, if this is a guild sticker.
|
||||
* @returns {Promise<?User>}
|
||||
*/
|
||||
async fetchUser() {
|
||||
if (this.partial) await this.fetch();
|
||||
if (!this.guildId) throw new Error('NOT_GUILD_STICKER');
|
||||
|
||||
const data = await this.client.api.guilds(this.guildId).stickers(this.id).get();
|
||||
this._patch(data);
|
||||
return this.user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data for editing a sticker.
|
||||
* @typedef {Object} GuildStickerEditData
|
||||
* @property {string} [name] The name of the sticker
|
||||
* @property {?string} [description] The description of the sticker
|
||||
* @property {string} [tags] The Discord name of a unicode emoji representing the sticker's expression
|
||||
*/
|
||||
|
||||
/**
|
||||
* Edits the sticker.
|
||||
* @param {GuildStickerEditData} [data] The new data for the sticker
|
||||
* @param {string} [reason] Reason for editing this sticker
|
||||
* @returns {Promise<Sticker>}
|
||||
* @example
|
||||
* // Update the name of a sticker
|
||||
* sticker.edit({ name: 'new name' })
|
||||
* .then(s => console.log(`Updated the name of the sticker to ${s.name}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
edit(data, reason) {
|
||||
return this.guild.stickers.edit(this, data, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the sticker.
|
||||
* @returns {Promise<Sticker>}
|
||||
* @param {string} [reason] Reason for deleting this sticker
|
||||
* @example
|
||||
* // Delete a message
|
||||
* sticker.delete()
|
||||
* .then(s => console.log(`Deleted sticker ${s.name}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async delete(reason) {
|
||||
await this.guild.stickers.delete(this, reason);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this sticker is the same as another one.
|
||||
* @param {Sticker|APISticker} other The sticker to compare it to
|
||||
* @returns {boolean}
|
||||
*/
|
||||
equals(other) {
|
||||
if (other instanceof Sticker) {
|
||||
return (
|
||||
other.id === this.id &&
|
||||
other.description === this.description &&
|
||||
other.type === this.type &&
|
||||
other.format === this.format &&
|
||||
other.name === this.name &&
|
||||
other.packId === this.packId &&
|
||||
other.tags.length === this.tags.length &&
|
||||
other.tags.every(tag => this.tags.includes(tag)) &&
|
||||
other.available === this.available &&
|
||||
other.guildId === this.guildId &&
|
||||
other.sortValue === this.sortValue
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
other.id === this.id &&
|
||||
other.description === this.description &&
|
||||
other.name === this.name &&
|
||||
other.tags === this.tags.join(', ')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Sticker;
|
||||
|
||||
/**
|
||||
* @external APISticker
|
||||
* @see {@link https://discord.com/developers/docs/resources/sticker#sticker-object}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external APIStickerItem
|
||||
* @see {@link https://discord.com/developers/docs/resources/sticker#sticker-item-object}
|
||||
*/
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const Base = require('./Base');
|
||||
const Sticker = require('./Sticker');
|
||||
const SnowflakeUtil = require('../util/SnowflakeUtil');
|
||||
|
||||
/**
|
||||
* Represents a pack of standard stickers.
|
||||
* @extends {Base}
|
||||
*/
|
||||
class StickerPack extends Base {
|
||||
/**
|
||||
* @param {Client} client The instantiating client
|
||||
* @param {APIStickerPack} pack The data for the sticker pack
|
||||
*/
|
||||
constructor(client, pack) {
|
||||
super(client);
|
||||
/**
|
||||
* The Sticker pack's id
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.id = pack.id;
|
||||
|
||||
/**
|
||||
* The stickers in the pack
|
||||
* @type {Collection<Snowflake, Sticker>}
|
||||
*/
|
||||
this.stickers = new Collection(pack.stickers.map(s => [s.id, new Sticker(client, s)]));
|
||||
|
||||
/**
|
||||
* The name of the sticker pack
|
||||
* @type {string}
|
||||
*/
|
||||
this.name = pack.name;
|
||||
|
||||
/**
|
||||
* The id of the pack's SKU
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.skuId = pack.sku_id;
|
||||
|
||||
/**
|
||||
* The id of a sticker in the pack which is shown as the pack's icon
|
||||
* @type {?Snowflake}
|
||||
*/
|
||||
this.coverStickerId = pack.cover_sticker_id ?? null;
|
||||
|
||||
/**
|
||||
* The description of the sticker pack
|
||||
* @type {string}
|
||||
*/
|
||||
this.description = pack.description;
|
||||
|
||||
/**
|
||||
* The id of the sticker pack's banner image
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.bannerId = pack.banner_asset_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* The timestamp the sticker was created at
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get createdTimestamp() {
|
||||
return SnowflakeUtil.deconstruct(this.id).timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* The time the sticker was created at
|
||||
* @type {Date}
|
||||
* @readonly
|
||||
*/
|
||||
get createdAt() {
|
||||
return new Date(this.createdTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* The sticker which is shown as the pack's icon
|
||||
* @type {?Sticker}
|
||||
* @readonly
|
||||
*/
|
||||
get coverSticker() {
|
||||
return this.coverStickerId && this.stickers.get(this.coverStickerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* The URL to this sticker pack's banner.
|
||||
* @param {StaticImageURLOptions} [options={}] Options for the Image URL
|
||||
* @returns {string}
|
||||
*/
|
||||
bannerURL({ format, size } = {}) {
|
||||
return this.client.rest.cdn.StickerPackBanner(this.bannerId, format, size);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StickerPack;
|
||||
|
||||
/**
|
||||
* @external APIStickerPack
|
||||
* @see {@link https://discord.com/developers/docs/resources/sticker#sticker-pack-object}
|
||||
*/
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
'use strict';
|
||||
|
||||
const GuildChannel = require('./GuildChannel');
|
||||
|
||||
/**
|
||||
* Represents a guild store channel on Discord.
|
||||
* @extends {GuildChannel}
|
||||
*/
|
||||
class StoreChannel extends GuildChannel {
|
||||
/**
|
||||
* @param {Guild} guild The guild the store channel is part of
|
||||
* @param {APIChannel} data The data for the store channel
|
||||
* @param {Client} [client] A safety parameter for the client that instantiated this
|
||||
*/
|
||||
constructor(guild, data, client) {
|
||||
super(guild, data, client);
|
||||
|
||||
/**
|
||||
* If the guild considers this channel NSFW
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.nsfw = Boolean(data.nsfw);
|
||||
}
|
||||
|
||||
_patch(data) {
|
||||
super._patch(data);
|
||||
|
||||
if ('nsfw' in data) {
|
||||
this.nsfw = Boolean(data.nsfw);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an invite to this guild channel.
|
||||
* @param {CreateInviteOptions} [options={}] The options for creating the invite
|
||||
* @returns {Promise<Invite>}
|
||||
* @example
|
||||
* // Create an invite to a channel
|
||||
* channel.createInvite()
|
||||
* .then(invite => console.log(`Created an invite with a code of ${invite.code}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
createInvite(options) {
|
||||
return this.guild.invites.create(this.id, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a collection of invites to this guild channel.
|
||||
* Resolves with a collection mapping invites by their codes.
|
||||
* @param {boolean} [cache=true] Whether or not to cache the fetched invites
|
||||
* @returns {Promise<Collection<string, Invite>>}
|
||||
*/
|
||||
fetchInvites(cache = true) {
|
||||
return this.guild.invites.fetch({ channelId: this.id, cache });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StoreChannel;
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const Base = require('./Base');
|
||||
const TeamMember = require('./TeamMember');
|
||||
const SnowflakeUtil = require('../util/SnowflakeUtil');
|
||||
|
||||
/**
|
||||
* Represents a Client OAuth2 Application Team.
|
||||
* @extends {Base}
|
||||
*/
|
||||
class Team extends Base {
|
||||
constructor(client, data) {
|
||||
super(client);
|
||||
this._patch(data);
|
||||
}
|
||||
|
||||
_patch(data) {
|
||||
/**
|
||||
* The Team's id
|
||||
* @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 {?Snowflake}
|
||||
*/
|
||||
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 SnowflakeUtil.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 team's icon.
|
||||
* @param {StaticImageURLOptions} [options={}] Options for the Image URL
|
||||
* @returns {?string}
|
||||
*/
|
||||
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
@@ -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 {MembershipState}
|
||||
*/
|
||||
this.membershipState = MembershipStates[data.membership_state];
|
||||
|
||||
/**
|
||||
* The user for this Team Member
|
||||
* @type {User}
|
||||
*/
|
||||
this.user = this.client.users._add(data.user);
|
||||
}
|
||||
|
||||
/**
|
||||
* The Team Member's id
|
||||
* @type {Snowflake}
|
||||
* @readonly
|
||||
*/
|
||||
get id() {
|
||||
return this.user.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* When concatenated with a string, this automatically returns the team member'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;
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
const BaseGuildTextChannel = require('./BaseGuildTextChannel');
|
||||
|
||||
/**
|
||||
* Represents a guild text channel on Discord.
|
||||
* @extends {BaseGuildTextChannel}
|
||||
*/
|
||||
class TextChannel extends BaseGuildTextChannel {
|
||||
_patch(data) {
|
||||
super._patch(data);
|
||||
|
||||
if ('rate_limit_per_user' in data) {
|
||||
/**
|
||||
* The ratelimit per user for this channel in seconds
|
||||
* @type {number}
|
||||
*/
|
||||
this.rateLimitPerUser = data.rate_limit_per_user;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TextChannel;
|
||||
+479
@@ -0,0 +1,479 @@
|
||||
'use strict';
|
||||
|
||||
const Channel = require('./Channel');
|
||||
const TextBasedChannel = require('./interfaces/TextBasedChannel');
|
||||
const MessageManager = require('../managers/MessageManager');
|
||||
const ThreadMemberManager = require('../managers/ThreadMemberManager');
|
||||
const Permissions = require('../util/Permissions');
|
||||
|
||||
/**
|
||||
* Represents a thread channel on Discord.
|
||||
* @extends {Channel}
|
||||
* @implements {TextBasedChannel}
|
||||
*/
|
||||
class ThreadChannel extends Channel {
|
||||
/**
|
||||
* @param {Guild} guild The guild the thread channel is part of
|
||||
* @param {APIChannel} data The data for the thread channel
|
||||
* @param {Client} [client] A safety parameter for the client that instantiated this
|
||||
* @param {boolean} [fromInteraction=false] Whether the data was from an interaction (partial)
|
||||
*/
|
||||
constructor(guild, data, client, fromInteraction = false) {
|
||||
super(guild?.client ?? client, data, false);
|
||||
|
||||
/**
|
||||
* The guild the thread is in
|
||||
* @type {Guild}
|
||||
*/
|
||||
this.guild = guild;
|
||||
|
||||
/**
|
||||
* The id of the guild the channel is in
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.guildId = guild?.id ?? data.guild_id;
|
||||
|
||||
/**
|
||||
* A manager of the messages sent to this thread
|
||||
* @type {MessageManager}
|
||||
*/
|
||||
this.messages = new MessageManager(this);
|
||||
|
||||
/**
|
||||
* A manager of the members that are part of this thread
|
||||
* @type {ThreadMemberManager}
|
||||
*/
|
||||
this.members = new ThreadMemberManager(this);
|
||||
if (data) this._patch(data, fromInteraction);
|
||||
}
|
||||
|
||||
_patch(data, partial = false) {
|
||||
super._patch(data);
|
||||
|
||||
/**
|
||||
* The name of the thread
|
||||
* @type {string}
|
||||
*/
|
||||
this.name = data.name;
|
||||
|
||||
if ('guild_id' in data) {
|
||||
this.guildId = data.guild_id;
|
||||
}
|
||||
|
||||
if ('parent_id' in data) {
|
||||
/**
|
||||
* The id of the parent channel of this thread
|
||||
* @type {?Snowflake}
|
||||
*/
|
||||
this.parentId = data.parent_id;
|
||||
} else if (!this.parentId) {
|
||||
this.parentId = null;
|
||||
}
|
||||
|
||||
if ('thread_metadata' in data) {
|
||||
/**
|
||||
* Whether the thread is locked
|
||||
* @type {?boolean}
|
||||
*/
|
||||
this.locked = data.thread_metadata.locked ?? false;
|
||||
|
||||
/**
|
||||
* Whether the thread is archived
|
||||
* @type {?boolean}
|
||||
*/
|
||||
this.archived = data.thread_metadata.archived;
|
||||
|
||||
/**
|
||||
* The amount of time (in minutes) after which the thread will automatically archive in case of no recent activity
|
||||
* @type {?number}
|
||||
*/
|
||||
this.autoArchiveDuration = data.thread_metadata.auto_archive_duration;
|
||||
|
||||
/**
|
||||
* The timestamp when the thread's archive status was last changed
|
||||
* <info>If the thread was never archived or unarchived, this is the timestamp at which the thread was
|
||||
* created</info>
|
||||
* @type {?number}
|
||||
*/
|
||||
this.archiveTimestamp = new Date(data.thread_metadata.archive_timestamp).getTime();
|
||||
} else {
|
||||
if (!this.locked) {
|
||||
this.locked = null;
|
||||
}
|
||||
if (!this.archived) {
|
||||
this.archived = null;
|
||||
}
|
||||
if (!this.autoArchiveDuration) {
|
||||
this.autoArchiveDuration = null;
|
||||
}
|
||||
if (!this.archiveTimestamp) {
|
||||
this.archiveTimestamp = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ('owner_id' in data) {
|
||||
/**
|
||||
* The id of the member who created this thread
|
||||
* @type {?Snowflake}
|
||||
*/
|
||||
this.ownerId = data.owner_id;
|
||||
} else if (!this.ownerId) {
|
||||
this.ownerId = null;
|
||||
}
|
||||
|
||||
if ('last_message_id' in data) {
|
||||
/**
|
||||
* The last message id sent in this thread, if one was sent
|
||||
* @type {?Snowflake}
|
||||
*/
|
||||
this.lastMessageId = data.last_message_id;
|
||||
} else if (!this.lastMessageId) {
|
||||
this.lastMessageId = null;
|
||||
}
|
||||
|
||||
if ('last_pin_timestamp' in data) {
|
||||
/**
|
||||
* 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;
|
||||
} else if (!this.lastPinTimestamp) {
|
||||
this.lastPinTimestamp = null;
|
||||
}
|
||||
|
||||
if ('rate_limit_per_user' in data || !partial) {
|
||||
/**
|
||||
* The ratelimit per user for this thread (in seconds)
|
||||
* @type {?number}
|
||||
*/
|
||||
this.rateLimitPerUser = data.rate_limit_per_user ?? 0;
|
||||
} else if (!this.rateLimitPerUser) {
|
||||
this.rateLimitPerUser = null;
|
||||
}
|
||||
|
||||
if ('message_count' in data) {
|
||||
/**
|
||||
* The approximate count of messages in this thread
|
||||
* <info>This stops counting at 50. If you need an approximate value higher than that, use
|
||||
* `ThreadChannel#messages.cache.size`</info>
|
||||
* @type {?number}
|
||||
*/
|
||||
this.messageCount = data.message_count;
|
||||
} else if (!this.messageCount) {
|
||||
this.messageCount = null;
|
||||
}
|
||||
|
||||
if ('member_count' in data) {
|
||||
/**
|
||||
* The approximate count of users in this thread
|
||||
* <info>This stops counting at 50. If you need an approximate value higher than that, use
|
||||
* `ThreadChannel#members.cache.size`</info>
|
||||
* @type {?number}
|
||||
*/
|
||||
this.memberCount = data.member_count;
|
||||
} else if (!this.memberCount) {
|
||||
this.memberCount = null;
|
||||
}
|
||||
|
||||
if (data.member && this.client.user) this.members._add({ user_id: this.client.user.id, ...data.member });
|
||||
if (data.messages) for (const message of data.messages) this.messages._add(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* A collection of associated guild member objects of this thread's members
|
||||
* @type {Collection<Snowflake, GuildMember>}
|
||||
* @readonly
|
||||
*/
|
||||
get guildMembers() {
|
||||
return this.members.cache.mapValues(member => member.guildMember);
|
||||
}
|
||||
|
||||
/**
|
||||
* The time at which this thread's archive status was last changed
|
||||
* <info>If the thread was never archived or unarchived, this is the time at which the thread was created</info>
|
||||
* @type {?Date}
|
||||
* @readonly
|
||||
*/
|
||||
get archivedAt() {
|
||||
if (!this.archiveTimestamp) return null;
|
||||
return new Date(this.archiveTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* The parent channel of this thread
|
||||
* @type {?(NewsChannel|TextChannel)}
|
||||
* @readonly
|
||||
*/
|
||||
get parent() {
|
||||
return this.guild.channels.resolve(this.parentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the client user join the thread.
|
||||
* @returns {Promise<ThreadChannel>}
|
||||
*/
|
||||
async join() {
|
||||
await this.members.add('@me');
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the client user leave the thread.
|
||||
* @returns {Promise<ThreadChannel>}
|
||||
*/
|
||||
async leave() {
|
||||
await this.members.remove('@me');
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the overall set of permissions for a member or role in this thread's parent channel, taking overwrites into
|
||||
* account.
|
||||
* @param {GuildMemberResolvable|RoleResolvable} memberOrRole The member or role to obtain the overall permissions for
|
||||
* @returns {?Readonly<Permissions>}
|
||||
*/
|
||||
permissionsFor(memberOrRole) {
|
||||
return this.parent?.permissionsFor(memberOrRole) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the owner of this thread. If the thread member object isn't needed,
|
||||
* use {@link ThreadChannel#ownerId} instead.
|
||||
* @param {FetchOwnerOptions} [options] The options for fetching the member
|
||||
* @returns {Promise<?ThreadMember>}
|
||||
*/
|
||||
async fetchOwner({ cache = true, force = false } = {}) {
|
||||
if (!force) {
|
||||
const existing = this.members.cache.get(this.ownerId);
|
||||
if (existing) return existing;
|
||||
}
|
||||
|
||||
// We cannot fetch a single thread member, as of this commit's date, Discord API responds with 405
|
||||
const members = await this.members.fetch(cache);
|
||||
return members.get(this.ownerId) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The options used to edit a thread channel
|
||||
* @typedef {Object} ThreadEditData
|
||||
* @property {string} [name] The new name for the thread
|
||||
* @property {boolean} [archived] Whether the thread is archived
|
||||
* @property {ThreadAutoArchiveDuration} [autoArchiveDuration] The amount of time (in minutes) after which the thread
|
||||
* should automatically archive in case of no recent activity
|
||||
* @property {number} [rateLimitPerUser] The ratelimit per user for the thread in seconds
|
||||
* @property {boolean} [locked] Whether the thread is locked
|
||||
*/
|
||||
|
||||
/**
|
||||
* Edits this thread.
|
||||
* @param {ThreadEditData} data The new data for this thread
|
||||
* @param {string} [reason] Reason for editing this thread
|
||||
* @returns {Promise<ThreadChannel>}
|
||||
* @example
|
||||
* // Edit a thread
|
||||
* thread.edit({ name: 'new-thread' })
|
||||
* .then(editedThread => console.log(editedThread))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async edit(data, reason) {
|
||||
let autoArchiveDuration = data.autoArchiveDuration;
|
||||
if (data.autoArchiveDuration === 'MAX') {
|
||||
autoArchiveDuration = 1440;
|
||||
if (this.guild.features.includes('SEVEN_DAY_THREAD_ARCHIVE')) {
|
||||
autoArchiveDuration = 10080;
|
||||
} else if (this.guild.features.includes('THREE_DAY_THREAD_ARCHIVE')) {
|
||||
autoArchiveDuration = 4320;
|
||||
}
|
||||
}
|
||||
const newData = await this.client.api.channels(this.id).patch({
|
||||
data: {
|
||||
name: (data.name ?? this.name).trim(),
|
||||
archived: data.archived,
|
||||
auto_archive_duration: autoArchiveDuration,
|
||||
rate_limit_per_user: data.rateLimitPerUser,
|
||||
locked: data.locked,
|
||||
},
|
||||
reason,
|
||||
});
|
||||
|
||||
return this.client.actions.ChannelUpdate.handle(newData).updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the thread is archived.
|
||||
* @param {boolean} [archived=true] Whether the thread is archived
|
||||
* @param {string} [reason] Reason for archiving or unarchiving
|
||||
* @returns {Promise<ThreadChannel>}
|
||||
* @example
|
||||
* // Archive the thread
|
||||
* thread.setArchived(true)
|
||||
* .then(newThread => console.log(`Thread is now ${newThread.archived ? 'archived' : 'active'}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
setArchived(archived = true, reason) {
|
||||
return this.edit({ archived }, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the duration after which the thread will automatically archive in case of no recent activity.
|
||||
* @param {ThreadAutoArchiveDuration} autoArchiveDuration The amount of time (in minutes) after which the thread
|
||||
* should automatically archive in case of no recent activity
|
||||
* @param {string} [reason] Reason for changing the auto archive duration
|
||||
* @returns {Promise<ThreadChannel>}
|
||||
* @example
|
||||
* // Set the thread's auto archive time to 1 hour
|
||||
* thread.setAutoArchiveDuration(60)
|
||||
* .then(newThread => {
|
||||
* console.log(`Thread will now archive after ${newThread.autoArchiveDuration} minutes of inactivity`);
|
||||
* });
|
||||
* .catch(console.error);
|
||||
*/
|
||||
setAutoArchiveDuration(autoArchiveDuration, reason) {
|
||||
return this.edit({ autoArchiveDuration }, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the thread can be **unarchived** by anyone with `SEND_MESSAGES` permission.
|
||||
* When a thread is locked only members with `MANAGE_THREADS` can unarchive it.
|
||||
* @param {boolean} [locked=true] Whether the thread is locked
|
||||
* @param {string} [reason] Reason for locking or unlocking the thread
|
||||
* @returns {Promise<ThreadChannel>}
|
||||
* @example
|
||||
* // Set the thread to locked
|
||||
* thread.setLocked(true)
|
||||
* .then(newThread => console.log(`Thread is now ${newThread.locked ? 'locked' : 'unlocked'}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
setLocked(locked = true, reason) {
|
||||
return this.edit({ locked }, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new name for this thread.
|
||||
* @param {string} name The new name for the thread
|
||||
* @param {string} [reason] Reason for changing the thread's name
|
||||
* @returns {Promise<ThreadChannel>}
|
||||
* @example
|
||||
* // Change the thread's name
|
||||
* thread.setName('not_general')
|
||||
* .then(newThread => console.log(`Thread's new name is ${newThread.name}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
setName(name, reason) {
|
||||
return this.edit({ name }, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the rate limit per user for this thread.
|
||||
* @param {number} rateLimitPerUser The new ratelimit in seconds
|
||||
* @param {string} [reason] Reason for changing the thread's ratelimits
|
||||
* @returns {Promise<ThreadChannel>}
|
||||
*/
|
||||
setRateLimitPerUser(rateLimitPerUser, reason) {
|
||||
return this.edit({ rateLimitPerUser }, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the client user is a member of the thread.
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get joined() {
|
||||
return this.members.cache.has(this.client.user?.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the thread is editable by the client user (name, archived, autoArchiveDuration)
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get editable() {
|
||||
return (this.ownerId === this.client.user.id && (this.type !== 'private_thread' || this.joined)) || this.manageable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the thread is joinable by the client user
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get joinable() {
|
||||
return (
|
||||
!this.archived &&
|
||||
!this.joined &&
|
||||
this.permissionsFor(this.client.user)?.has(
|
||||
this.type === 'GUILD_PRIVATE_THREAD' ? Permissions.FLAGS.MANAGE_THREADS : Permissions.FLAGS.VIEW_CHANNEL,
|
||||
false,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the thread is manageable by the client user, for deleting or editing rateLimitPerUser or locked.
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get manageable() {
|
||||
return this.permissionsFor(this.client.user)?.has(Permissions.FLAGS.MANAGE_THREADS, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the client user can send messages in this thread
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get sendable() {
|
||||
return (
|
||||
!this.archived &&
|
||||
(this.type !== 'private_thread' || this.joined || this.manageable) &&
|
||||
this.permissionsFor(this.client.user)?.any(
|
||||
[
|
||||
Permissions.FLAGS.SEND_MESSAGES,
|
||||
this.type === 'GUILD_PRIVATE_THREAD'
|
||||
? Permissions.FLAGS.USE_PRIVATE_THREADS
|
||||
: Permissions.FLAGS.USE_PUBLIC_THREADS,
|
||||
],
|
||||
false,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the thread is unarchivable by the client user
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get unarchivable() {
|
||||
return this.archived && (this.locked ? this.manageable : this.sendable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes this thread.
|
||||
* @param {string} [reason] Reason for deleting this thread
|
||||
* @returns {Promise<ThreadChannel>}
|
||||
* @example
|
||||
* // Delete the thread
|
||||
* thread.delete('cleaning out old threads')
|
||||
* .then(deletedThread => console.log(deletedThread))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async delete(reason) {
|
||||
await this.client.api.channels(this.id).delete({ reason });
|
||||
return this;
|
||||
}
|
||||
|
||||
// These are here only for documentation purposes - they are implemented by TextBasedChannel
|
||||
/* eslint-disable no-empty-function */
|
||||
get lastMessage() {}
|
||||
get lastPinAt() {}
|
||||
send() {}
|
||||
sendTyping() {}
|
||||
createMessageCollector() {}
|
||||
awaitMessages() {}
|
||||
createMessageComponentCollector() {}
|
||||
awaitMessageComponent() {}
|
||||
bulkDelete() {}
|
||||
}
|
||||
|
||||
TextBasedChannel.applyToClass(ThreadChannel, true);
|
||||
|
||||
module.exports = ThreadChannel;
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
'use strict';
|
||||
|
||||
const Base = require('./Base');
|
||||
const ThreadMemberFlags = require('../util/ThreadMemberFlags');
|
||||
|
||||
/**
|
||||
* Represents a Member for a Thread.
|
||||
* @extends {Base}
|
||||
*/
|
||||
class ThreadMember extends Base {
|
||||
/**
|
||||
* @param {ThreadChannel} thread The thread that this member is associated with
|
||||
* @param {APIThreadMember} data The data for the thread member
|
||||
*/
|
||||
constructor(thread, data) {
|
||||
super(thread.client);
|
||||
|
||||
/**
|
||||
* The thread that this member is a part of
|
||||
* @type {ThreadChannel}
|
||||
*/
|
||||
this.thread = thread;
|
||||
|
||||
/**
|
||||
* The timestamp the member last joined the thread at
|
||||
* @type {?number}
|
||||
*/
|
||||
this.joinedTimestamp = null;
|
||||
|
||||
/**
|
||||
* The id of the thread member
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.id = data.user_id;
|
||||
|
||||
this._patch(data);
|
||||
}
|
||||
|
||||
_patch(data) {
|
||||
this.joinedTimestamp = new Date(data.join_timestamp).getTime();
|
||||
|
||||
/**
|
||||
* The flags for this thread member
|
||||
* @type {ThreadMemberFlags}
|
||||
*/
|
||||
this.flags = new ThreadMemberFlags(data.flags).freeze();
|
||||
}
|
||||
|
||||
/**
|
||||
* The guild member associated with this thread member
|
||||
* @type {?GuildMember}
|
||||
* @readonly
|
||||
*/
|
||||
get guildMember() {
|
||||
return this.thread.guild.members.resolve(this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* The last time this member joined the thread
|
||||
* @type {?Date}
|
||||
* @readonly
|
||||
*/
|
||||
get joinedAt() {
|
||||
return this.joinedTimestamp ? new Date(this.joinedTimestamp) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The user associated with this thread member
|
||||
* @type {?User}
|
||||
* @readonly
|
||||
*/
|
||||
get user() {
|
||||
return this.client.users.resolve(this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the client user can manage this thread member
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get manageable() {
|
||||
return !this.thread.archived && this.thread.editable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes this member from the thread.
|
||||
* @param {string} [reason] Reason for removing the member
|
||||
* @returns {ThreadMember}
|
||||
*/
|
||||
async remove(reason) {
|
||||
await this.thread.members.remove(this.id, reason);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ThreadMember;
|
||||
|
||||
/**
|
||||
* @external APIThreadMember
|
||||
* @see {@link https://discord.com/developers/docs/resources/channel#thread-member-object}
|
||||
*/
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
'use strict';
|
||||
|
||||
const Base = require('./Base');
|
||||
|
||||
/**
|
||||
* Represents a typing state for a user in a channel.
|
||||
* @extends {Base}
|
||||
*/
|
||||
class Typing extends Base {
|
||||
/**
|
||||
* @param {TextBasedChannels} channel The channel this typing came from
|
||||
* @param {User} user The user that started typing
|
||||
* @param {APITypingStart} data The raw data received
|
||||
*/
|
||||
constructor(channel, user, data) {
|
||||
super(channel.client);
|
||||
|
||||
/**
|
||||
* The channel the status is from
|
||||
* @type {TextBasedChannels}
|
||||
*/
|
||||
this.channel = channel;
|
||||
|
||||
/**
|
||||
* The user who is typing
|
||||
* @type {User}
|
||||
*/
|
||||
this.user = user;
|
||||
|
||||
this._patch(data);
|
||||
}
|
||||
|
||||
_patch(data) {
|
||||
/**
|
||||
* The UNIX timestamp in milliseconds the user started typing at
|
||||
* @type {number}
|
||||
*/
|
||||
this.startedTimestamp = data.timestamp * 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the status is received from a guild.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
inGuild() {
|
||||
return this.guild !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The time the user started typing at
|
||||
* @type {Date}
|
||||
* @readonly
|
||||
*/
|
||||
get startedAt() {
|
||||
return new Date(this.startedTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* The guild the status is from
|
||||
* @type {?Guild}
|
||||
* @readonly
|
||||
*/
|
||||
get guild() {
|
||||
return this.channel.guild ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The member who is typing
|
||||
* @type {?GuildMember}
|
||||
* @readonly
|
||||
*/
|
||||
get member() {
|
||||
return this.guild?.members.resolve(this.user) ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Typing;
|
||||
|
||||
/**
|
||||
* @external APITypingStart
|
||||
* @see {@link https://discord.com/developers/docs/topics/gateway#typing-start-typing-start-event-fields}
|
||||
*/
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
'use strict';
|
||||
|
||||
const Base = require('./Base');
|
||||
const TextBasedChannel = require('./interfaces/TextBasedChannel');
|
||||
const { Error } = require('../errors');
|
||||
const SnowflakeUtil = require('../util/SnowflakeUtil');
|
||||
const UserFlags = require('../util/UserFlags');
|
||||
|
||||
/**
|
||||
* Represents a user on Discord.
|
||||
* @implements {TextBasedChannel}
|
||||
* @extends {Base}
|
||||
*/
|
||||
class User extends Base {
|
||||
/**
|
||||
* @param {Client} client The instantiating client
|
||||
* @param {APIUser} data The data for the user
|
||||
*/
|
||||
constructor(client, data) {
|
||||
super(client);
|
||||
|
||||
/**
|
||||
* The user's id
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.id = data.id;
|
||||
|
||||
this.bot = null;
|
||||
|
||||
this.system = 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) {
|
||||
/**
|
||||
* Whether or not the user is a bot
|
||||
* @type {?boolean}
|
||||
*/
|
||||
this.bot = Boolean(data.bot);
|
||||
} else if (!this.partial && typeof this.bot !== 'boolean') {
|
||||
this.bot = false;
|
||||
}
|
||||
|
||||
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 user avatar's hash
|
||||
* @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);
|
||||
} else if (!this.partial && typeof this.system !== 'boolean') {
|
||||
this.system = false;
|
||||
}
|
||||
|
||||
if ('public_flags' in data) {
|
||||
/**
|
||||
* The flags for this user
|
||||
* @type {?UserFlags}
|
||||
*/
|
||||
this.flags = new UserFlags(data.public_flags);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 SnowflakeUtil.deconstruct(this.id).timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* The time the user was created at
|
||||
* @type {Date}
|
||||
* @readonly
|
||||
*/
|
||||
get createdAt() {
|
||||
return new Date(this.createdTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.channels._add(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
await this.client.api.channels(dmChannel.id).delete();
|
||||
this.client.channels._remove(dmChannel.id);
|
||||
return dmChannel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 API
|
||||
* @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=true] Whether to skip the cache check and request the API
|
||||
* @returns {Promise<User>}
|
||||
*/
|
||||
fetch(force = true) {
|
||||
return this.client.users.fetch(this.id, { 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,
|
||||
},
|
||||
...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;
|
||||
|
||||
/**
|
||||
* @external APIUser
|
||||
* @see {@link https://discord.com/developers/docs/resources/user#user-object}
|
||||
*/
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
'use strict';
|
||||
|
||||
const BaseGuildVoiceChannel = require('./BaseGuildVoiceChannel');
|
||||
const Permissions = require('../util/Permissions');
|
||||
|
||||
/**
|
||||
* Represents a guild voice channel on Discord.
|
||||
* @extends {BaseGuildVoiceChannel}
|
||||
*/
|
||||
class VoiceChannel extends BaseGuildVoiceChannel {
|
||||
/**
|
||||
* 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 (!super.joinable) 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the RTC region of the channel.
|
||||
* @name VoiceChannel#setRTCRegion
|
||||
* @param {?string} region The new region of the channel. Set to `null` to remove a specific region for the channel
|
||||
* @returns {Promise<VoiceChannel>}
|
||||
* @example
|
||||
* // Set the RTC region to europe
|
||||
* voiceChannel.setRTCRegion('europe');
|
||||
* @example
|
||||
* // Remove a fixed region for this channel - let Discord decide automatically
|
||||
* voiceChannel.setRTCRegion(null);
|
||||
*/
|
||||
}
|
||||
|
||||
module.exports = VoiceChannel;
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
'use strict';
|
||||
|
||||
const Util = require('../util/Util');
|
||||
|
||||
/**
|
||||
* Represents a Discord voice region for guilds.
|
||||
*/
|
||||
class VoiceRegion {
|
||||
constructor(data) {
|
||||
/**
|
||||
* The region's id
|
||||
* @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;
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
'use strict';
|
||||
|
||||
const Base = require('./Base');
|
||||
const { Error, TypeError } = require('../errors');
|
||||
|
||||
/**
|
||||
* Represents the voice state for a Guild Member.
|
||||
*/
|
||||
class VoiceState extends Base {
|
||||
/**
|
||||
* @param {Guild} guild The guild the voice state is part of
|
||||
* @param {APIVoiceState} data The data for the voice state
|
||||
*/
|
||||
constructor(guild, data) {
|
||||
super(guild.client);
|
||||
/**
|
||||
* The guild of this voice state
|
||||
* @type {Guild}
|
||||
*/
|
||||
this.guild = guild;
|
||||
/**
|
||||
* The id of the member of this voice state
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.id = data.user_id;
|
||||
this._patch(data);
|
||||
}
|
||||
|
||||
_patch(data) {
|
||||
/**
|
||||
* Whether this member is deafened server-wide
|
||||
* @type {?boolean}
|
||||
*/
|
||||
this.serverDeaf = data.deaf ?? null;
|
||||
/**
|
||||
* Whether this member is muted server-wide
|
||||
* @type {?boolean}
|
||||
*/
|
||||
this.serverMute = data.mute ?? null;
|
||||
/**
|
||||
* Whether this member is self-deafened
|
||||
* @type {?boolean}
|
||||
*/
|
||||
this.selfDeaf = data.self_deaf ?? null;
|
||||
/**
|
||||
* Whether this member is self-muted
|
||||
* @type {?boolean}
|
||||
*/
|
||||
this.selfMute = data.self_mute ?? null;
|
||||
/**
|
||||
* Whether this member's camera is enabled
|
||||
* @type {?boolean}
|
||||
*/
|
||||
this.selfVideo = data.self_video ?? null;
|
||||
/**
|
||||
* The session id for this member's connection
|
||||
* @type {?string}
|
||||
*/
|
||||
this.sessionId = data.session_id ?? null;
|
||||
/**
|
||||
* Whether this member is streaming using "Screen Share"
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.streaming = data.self_stream ?? false;
|
||||
/**
|
||||
* The {@link VoiceChannel} or {@link StageChannel} id the member is in
|
||||
* @type {?Snowflake}
|
||||
*/
|
||||
this.channelId = data.channel_id ?? null;
|
||||
/**
|
||||
* Whether this member is suppressed from speaking. This property is specific to stage channels only.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.suppress = data.suppress;
|
||||
/**
|
||||
* The time at which the member requested to speak. This property is specific to stage channels only.
|
||||
* @type {?number}
|
||||
*/
|
||||
this.requestToSpeakTimestamp = data.request_to_speak_timestamp
|
||||
? new Date(data.request_to_speak_timestamp).getTime()
|
||||
: 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|StageChannel)}
|
||||
* @readonly
|
||||
*/
|
||||
get channel() {
|
||||
return this.guild.channels.cache.get(this.channelId) ?? 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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?.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?.edit({ deaf }, reason) ?? Promise.reject(new Error('VOICE_STATE_UNCACHED_MEMBER'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the member from the channel.
|
||||
* @param {string} [reason] Reason for disconnecting the member from the channel
|
||||
* @returns {Promise<GuildMember>}
|
||||
*/
|
||||
disconnect(reason) {
|
||||
return this.setChannel(null, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the member to a different channel, or disconnects them from the one they're in.
|
||||
* @param {VoiceChannelResolvable|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?.edit({ channel }, reason) ?? Promise.reject(new Error('VOICE_STATE_UNCACHED_MEMBER'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the request to speak in the channel.
|
||||
* Only applicable for stage channels and for the client's own voice state.
|
||||
* @param {boolean} request Whether or not the client is requesting to become a speaker.
|
||||
* @example
|
||||
* // Making the client request to speak in a stage channel (raise its hand)
|
||||
* guild.me.voice.setRequestToSpeak(true);
|
||||
* @example
|
||||
* // Making the client cancel a request to speak
|
||||
* guild.me.voice.setRequestToSpeak(false);
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async setRequestToSpeak(request) {
|
||||
if (this.channel?.type !== 'GUILD_STAGE_VOICE') throw new Error('VOICE_NOT_STAGE_CHANNEL');
|
||||
|
||||
if (this.client.user.id !== this.id) throw new Error('VOICE_STATE_NOT_OWN');
|
||||
|
||||
await this.client.api.guilds(this.guild.id, 'voice-states', '@me').patch({
|
||||
data: {
|
||||
channel_id: this.channelId,
|
||||
request_to_speak_timestamp: request ? new Date().toISOString() : null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppress/unsuppress the user. Only applicable for stage channels.
|
||||
* @param {boolean} suppressed - Whether or not the user should be suppressed.
|
||||
* @example
|
||||
* // Making the client a speaker
|
||||
* guild.me.voice.setSuppressed(false);
|
||||
* @example
|
||||
* // Making the client an audience member
|
||||
* guild.me.voice.setSuppressed(true);
|
||||
* @example
|
||||
* // Inviting another user to speak
|
||||
* voiceState.setSuppressed(false);
|
||||
* @example
|
||||
* // Moving another user to the audience, or cancelling their invite to speak
|
||||
* voiceState.setSuppressed(true);
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async setSuppressed(suppressed) {
|
||||
if (typeof suppressed !== 'boolean') throw new TypeError('VOICE_STATE_INVALID_TYPE', 'suppressed');
|
||||
|
||||
if (this.channel?.type !== 'GUILD_STAGE_VOICE') throw new Error('VOICE_NOT_STAGE_CHANNEL');
|
||||
|
||||
const target = this.client.user.id === this.id ? '@me' : this.id;
|
||||
|
||||
await this.client.api.guilds(this.guild.id, 'voice-states', target).patch({
|
||||
data: {
|
||||
channel_id: this.channelId,
|
||||
suppress: suppressed,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return super.toJSON({
|
||||
id: true,
|
||||
serverDeaf: true,
|
||||
serverMute: true,
|
||||
selfDeaf: true,
|
||||
selfMute: true,
|
||||
sessionId: true,
|
||||
channelId: 'channel',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = VoiceState;
|
||||
|
||||
/**
|
||||
* @external APIVoiceState
|
||||
* @see {@link https://discord.com/developers/docs/resources/voice#voice-state-object}
|
||||
*/
|
||||
+363
@@ -0,0 +1,363 @@
|
||||
'use strict';
|
||||
|
||||
const MessagePayload = require('./MessagePayload');
|
||||
const { Error } = require('../errors');
|
||||
const { WebhookTypes } = require('../util/Constants');
|
||||
const DataResolver = require('../util/DataResolver');
|
||||
const SnowflakeUtil = require('../util/SnowflakeUtil');
|
||||
|
||||
/**
|
||||
* 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, unavailable for follower webhooks and webhooks owned by another application.
|
||||
* @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 webhook's id
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.id = data.id;
|
||||
|
||||
/**
|
||||
* The type of the webhook
|
||||
* @type {WebhookType}
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* The owner of the webhook
|
||||
* @type {?(User|APIUser)}
|
||||
*/
|
||||
this.owner = data.user ? this.client.users?._add(data.user) ?? data.user : null;
|
||||
|
||||
/**
|
||||
* The source guild of the webhook
|
||||
* @type {?(Guild|APIGuild)}
|
||||
*/
|
||||
this.sourceGuild = data.source_guild
|
||||
? this.client.guilds?._add(data.source_guild, false) ?? data.source_guild
|
||||
: null;
|
||||
|
||||
/**
|
||||
* The source channel of the webhook
|
||||
* @type {?(Channel|APIChannel)}
|
||||
*/
|
||||
this.sourceChannel = this.client.channels?.resolve(data.source_channel?.id) ?? data.source_channel ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options that can be passed into send.
|
||||
* @typedef {BaseMessageOptions} WebhookMessageOptions
|
||||
* @property {string} [username=this.name] Username override for the message
|
||||
* @property {string} [avatarURL] Avatar URL override for the message
|
||||
* @property {Snowflake} [threadId] The id of the thread in the channel to send to.
|
||||
* <info>For interaction webhooks, this property is ignored</info>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options that can be passed into editMessage.
|
||||
* @typedef {Object} WebhookEditMessageOptions
|
||||
* @property {MessageEmbed[]|APIEmbed[]} [embeds] See {@link WebhookMessageOptions#embeds}
|
||||
* @property {string} [content] See {@link BaseMessageOptions#content}
|
||||
* @property {FileOptions[]|BufferResolvable[]|MessageAttachment[]} [files] See {@link BaseMessageOptions#files}
|
||||
* @property {MessageMentionOptions} [allowedMentions] See {@link BaseMessageOptions#allowedMentions}
|
||||
* @property {MessageActionRow[]|MessageActionRowOptions[]} [components]
|
||||
* Action rows containing interactive components for the message (buttons, select menus)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sends a message with this webhook.
|
||||
* @param {string|MessagePayload|WebhookMessageOptions} options The options to provide
|
||||
* @returns {Promise<Message|APIMessage>}
|
||||
* @example
|
||||
* // Send a basic message
|
||||
* webhook.send('hello!')
|
||||
* .then(message => console.log(`Sent message: ${message.content}`))
|
||||
* .catch(console.error);
|
||||
* @example
|
||||
* // Send a basic message in a thread
|
||||
* webhook.send({ content: 'hello!', threadId: '836856309672348295' })
|
||||
* .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({
|
||||
* content: '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(options) {
|
||||
if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE');
|
||||
|
||||
let messagePayload;
|
||||
|
||||
if (options instanceof MessagePayload) {
|
||||
messagePayload = options.resolveData();
|
||||
} else {
|
||||
messagePayload = MessagePayload.create(this, options).resolveData();
|
||||
}
|
||||
|
||||
const { data, files } = await messagePayload.resolveFiles();
|
||||
const d = await this.client.api.webhooks(this.id, this.token).post({
|
||||
data,
|
||||
files,
|
||||
query: { thread_id: messagePayload.options.threadId, wait: true },
|
||||
auth: false,
|
||||
});
|
||||
return this.client.channels?.cache.get(d.channel_id)?.messages._add(d, false) ?? d;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
* @see {@link https://api.slack.com/messaging/webhooks}
|
||||
*/
|
||||
async sendSlackMessage(body) {
|
||||
if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE');
|
||||
|
||||
const data = await this.client.api.webhooks(this.id, this.token).slack.post({
|
||||
query: { wait: true },
|
||||
auth: false,
|
||||
data: body,
|
||||
});
|
||||
return data.toString() === 'ok';
|
||||
}
|
||||
|
||||
/**
|
||||
* Options used to edit a {@link Webhook}.
|
||||
* @typedef {Object} WebhookEditData
|
||||
* @property {string} [name=this.name] The new name for the webhook
|
||||
* @property {BufferResolvable} [avatar] The new avatar for the webhook
|
||||
* @property {GuildTextChannelResolvable} [channel] The new channel for the webhook
|
||||
*/
|
||||
|
||||
/**
|
||||
* Edits this webhook.
|
||||
* @param {WebhookEditData} options Options for editing the webhook
|
||||
* @param {string} [reason] Reason for editing the 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?.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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a message that was sent by this webhook.
|
||||
* @param {Snowflake|'@original'} message The id of the message to fetch
|
||||
* @param {boolean} [cache=true] Whether to cache the message
|
||||
* @returns {Promise<Message|APIMessage>} Returns the raw message data if the webhook was instantiated as a
|
||||
* {@link WebhookClient} or if the channel is uncached, otherwise a {@link Message} will be returned
|
||||
*/
|
||||
async fetchMessage(message, cache = true) {
|
||||
if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE');
|
||||
|
||||
const data = await this.client.api.webhooks(this.id, this.token).messages(message).get();
|
||||
return this.client.channels?.cache.get(data.channel_id)?.messages._add(data, cache) ?? data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits a message that was sent by this webhook.
|
||||
* @param {MessageResolvable|'@original'} message The message to edit
|
||||
* @param {string|MessagePayload|WebhookEditMessageOptions} options The options to provide
|
||||
* @returns {Promise<Message|APIMessage>} Returns the raw message data if the webhook was instantiated as a
|
||||
* {@link WebhookClient} or if the channel is uncached, otherwise a {@link Message} will be returned
|
||||
*/
|
||||
async editMessage(message, options) {
|
||||
if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE');
|
||||
|
||||
let messagePayload;
|
||||
|
||||
if (options instanceof MessagePayload) messagePayload = options;
|
||||
else messagePayload = MessagePayload.create(this, options);
|
||||
|
||||
const { data, files } = await messagePayload.resolveData().resolveFiles();
|
||||
|
||||
const d = await this.client.api
|
||||
.webhooks(this.id, this.token)
|
||||
.messages(typeof message === 'string' ? message : message.id)
|
||||
.patch({ data, files });
|
||||
|
||||
const messageManager = this.client.channels?.cache.get(d.channel_id)?.messages;
|
||||
if (!messageManager) return d;
|
||||
|
||||
const existing = messageManager.cache.get(d.id);
|
||||
if (!existing) return messageManager._add(d);
|
||||
|
||||
const clone = existing._clone();
|
||||
clone._patch(d);
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the webhook.
|
||||
* @param {string} [reason] Reason for deleting this webhook
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async delete(reason) {
|
||||
await this.client.api.webhooks(this.id, this.token).delete({ reason });
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a message that was sent by this webhook.
|
||||
* @param {MessageResolvable|'@original'} message The message to delete
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async deleteMessage(message) {
|
||||
if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE');
|
||||
|
||||
await this.client.api
|
||||
.webhooks(this.id, this.token)
|
||||
.messages(typeof message === 'string' ? message : message.id)
|
||||
.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* The timestamp the webhook was created at
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get createdTimestamp() {
|
||||
return SnowflakeUtil.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 {StaticImageURLOptions} [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, ignore = []) {
|
||||
for (const prop of [
|
||||
'send',
|
||||
'sendSlackMessage',
|
||||
'fetchMessage',
|
||||
'edit',
|
||||
'editMessage',
|
||||
'delete',
|
||||
'deleteMessage',
|
||||
'createdTimestamp',
|
||||
'createdAt',
|
||||
'url',
|
||||
]) {
|
||||
if (ignore.includes(prop)) continue;
|
||||
Object.defineProperty(structure.prototype, prop, Object.getOwnPropertyDescriptor(Webhook.prototype, prop));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Webhook;
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
'use strict';
|
||||
|
||||
const Base = require('./Base');
|
||||
const Emoji = require('./Emoji');
|
||||
|
||||
/**
|
||||
* Represents a channel link in a guild's welcome screen.
|
||||
* @extends {Base}
|
||||
*/
|
||||
class WelcomeChannel extends Base {
|
||||
constructor(guild, data) {
|
||||
super(guild.client);
|
||||
|
||||
/**
|
||||
* The guild for this welcome channel
|
||||
* @type {Guild|InviteGuild}
|
||||
*/
|
||||
this.guild = guild;
|
||||
|
||||
/**
|
||||
* The description of this welcome channel
|
||||
* @type {string}
|
||||
*/
|
||||
this.description = data.description;
|
||||
|
||||
/**
|
||||
* The raw emoji data
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
this._emoji = {
|
||||
name: data.emoji_name,
|
||||
id: data.emoji_id,
|
||||
};
|
||||
|
||||
/**
|
||||
* The id of this welcome channel
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.channelId = data.channel_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* The channel of this welcome channel
|
||||
* @type {?(TextChannel|NewsChannel)}
|
||||
*/
|
||||
get channel() {
|
||||
return this.client.channels.resolve(this.channelId);
|
||||
}
|
||||
|
||||
/**
|
||||
* The emoji of this welcome channel
|
||||
* @type {GuildEmoji|Emoji}
|
||||
*/
|
||||
get emoji() {
|
||||
return this.client.emojis.resolve(this._emoji.id) ?? new Emoji(this.client, this._emoji);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WelcomeChannel;
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const Base = require('./Base');
|
||||
const WelcomeChannel = require('./WelcomeChannel');
|
||||
|
||||
/**
|
||||
* Represents a welcome screen.
|
||||
* @extends {Base}
|
||||
*/
|
||||
class WelcomeScreen extends Base {
|
||||
constructor(guild, data) {
|
||||
super(guild.client);
|
||||
|
||||
/**
|
||||
* The guild for this welcome screen
|
||||
* @type {Guild}
|
||||
*/
|
||||
this.guild = guild;
|
||||
|
||||
/**
|
||||
* The description of this welcome screen
|
||||
* @type {?string}
|
||||
*/
|
||||
this.description = data.description ?? null;
|
||||
|
||||
/**
|
||||
* Collection of welcome channels belonging to this welcome screen
|
||||
* @type {Collection<Snowflake, WelcomeChannel>}
|
||||
*/
|
||||
this.welcomeChannels = new Collection();
|
||||
|
||||
for (const channel of data.welcome_channels) {
|
||||
const welcomeChannel = new WelcomeChannel(this.guild, channel);
|
||||
this.welcomeChannels.set(welcomeChannel.channelId, welcomeChannel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the welcome screen is enabled on the guild or not
|
||||
* @type {boolean}
|
||||
*/
|
||||
get enabled() {
|
||||
return this.guild.features.includes('WELCOME_SCREEN_ENABLED');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WelcomeScreen;
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const Base = require('./Base');
|
||||
const WidgetMember = require('./WidgetMember');
|
||||
|
||||
/**
|
||||
* Represents a Widget.
|
||||
*/
|
||||
class Widget extends Base {
|
||||
/**
|
||||
* @param {Client} client The instantiating client
|
||||
* @param {Object} data The raw data
|
||||
*/
|
||||
constructor(client, data) {
|
||||
super(client);
|
||||
this._patch(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a channel in a Widget
|
||||
* @typedef {Object} WidgetChannel
|
||||
* @property {Snowflake} id Id of the channel
|
||||
* @property {string} name Name of the channel
|
||||
* @property {number} position Position of the channel
|
||||
*/
|
||||
|
||||
/**
|
||||
* Builds the widget with the provided data.
|
||||
* @param {*} data The raw data of the widget
|
||||
* @private
|
||||
*/
|
||||
_patch(data) {
|
||||
/**
|
||||
* The id of the guild.
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.id = data.id;
|
||||
|
||||
/**
|
||||
* The name of the guild.
|
||||
* @type {string}
|
||||
*/
|
||||
this.name = data.name;
|
||||
|
||||
/**
|
||||
* The invite of the guild.
|
||||
* @type {?string}
|
||||
*/
|
||||
this.instantInvite = data.instant_invite;
|
||||
|
||||
/**
|
||||
* The list of channels in the guild.
|
||||
* @type {Collection<Snowflake, WidgetChannel>}
|
||||
*/
|
||||
this.channels = new Collection();
|
||||
for (const channel of data.channels) {
|
||||
this.channels.set(channel.id, channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of members in the guild.
|
||||
* These strings are just arbitrary numbers, they aren't Snowflakes.
|
||||
* @type {Collection<string, WidgetMember>}
|
||||
*/
|
||||
this.members = new Collection();
|
||||
for (const member of data.members) {
|
||||
this.members.set(member.id, new WidgetMember(this.client, member));
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of the members online.
|
||||
* @type {number}
|
||||
*/
|
||||
this.presenceCount = data.presence_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the Widget.
|
||||
* @returns {Promise<Widget>}
|
||||
*/
|
||||
async fetch() {
|
||||
const data = await this.client.api.guilds(this.id, 'widget.json').get();
|
||||
this._patch(data);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Widget;
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
'use strict';
|
||||
|
||||
const Base = require('./Base');
|
||||
|
||||
/**
|
||||
* Represents a WidgetMember.
|
||||
*/
|
||||
class WidgetMember extends Base {
|
||||
/**
|
||||
* Activity sent in a {@link WidgetMember}.
|
||||
* @typedef {Object} WidgetActivity
|
||||
* @property {string} name The name of the activity
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Client} client The instantiating client
|
||||
* @param {Object} data The raw data
|
||||
*/
|
||||
constructor(client, data) {
|
||||
super(client);
|
||||
|
||||
/**
|
||||
* The id of the user. It's an arbitrary number.
|
||||
* @type {string}
|
||||
*/
|
||||
this.id = data.id;
|
||||
|
||||
/**
|
||||
* The username of the member.
|
||||
* @type {string}
|
||||
*/
|
||||
this.username = data.username;
|
||||
|
||||
/**
|
||||
* The discriminator of the member.
|
||||
* @type {string}
|
||||
*/
|
||||
this.discriminator = data.discriminator;
|
||||
|
||||
/**
|
||||
* The avatar of the member.
|
||||
* @type {?string}
|
||||
*/
|
||||
this.avatar = data.avatar;
|
||||
|
||||
/**
|
||||
* The status of the member.
|
||||
* @type {PresenceStatus}
|
||||
*/
|
||||
this.status = data.status;
|
||||
|
||||
/**
|
||||
* IIf the member is server deafened
|
||||
* @type {?boolean}
|
||||
*/
|
||||
this.deaf = data.deaf ?? null;
|
||||
|
||||
/**
|
||||
* If the member is server muted
|
||||
* @type {?boolean}
|
||||
*/
|
||||
this.mute = data.mute ?? null;
|
||||
|
||||
/**
|
||||
* If the member is self deafened
|
||||
* @type {?boolean}
|
||||
*/
|
||||
this.selfDeaf = data.self_deaf ?? null;
|
||||
|
||||
/**
|
||||
* If the member is self muted
|
||||
* @type {?boolean}
|
||||
*/
|
||||
this.selfMute = data.self_mute ?? null;
|
||||
|
||||
/**
|
||||
* If the member is suppressed
|
||||
* @type {?boolean}
|
||||
*/
|
||||
this.suppress = data.suppress ?? null;
|
||||
|
||||
/**
|
||||
* The id of the voice channel the member is in, if any
|
||||
* @type {?Snowflake}
|
||||
*/
|
||||
this.channelId = data.channel_id ?? null;
|
||||
|
||||
/**
|
||||
* The avatar URL of the member.
|
||||
* @type {string}
|
||||
*/
|
||||
this.avatarURL = data.avatar_url;
|
||||
|
||||
/**
|
||||
* The activity of the member.
|
||||
* @type {?WidgetActivity}
|
||||
*/
|
||||
this.activity = data.activity ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WidgetMember;
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
'use strict';
|
||||
|
||||
const { ClientApplicationAssetTypes, Endpoints } = require('../../util/Constants');
|
||||
const SnowflakeUtil = require('../../util/SnowflakeUtil');
|
||||
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 application's id
|
||||
* @type {Snowflake}
|
||||
*/
|
||||
this.id = data.id;
|
||||
|
||||
/**
|
||||
* The name of the application
|
||||
* @type {?string}
|
||||
*/
|
||||
this.name = data.name ?? this.name ?? null;
|
||||
|
||||
/**
|
||||
* The application's description
|
||||
* @type {?string}
|
||||
*/
|
||||
this.description = data.description ?? this.description ?? null;
|
||||
|
||||
/**
|
||||
* The application's icon hash
|
||||
* @type {?string}
|
||||
*/
|
||||
this.icon = data.icon ?? this.icon ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The timestamp the application was created at
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get createdTimestamp() {
|
||||
return SnowflakeUtil.deconstruct(this.id).timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* The time the application was created at
|
||||
* @type {Date}
|
||||
* @readonly
|
||||
*/
|
||||
get createdAt() {
|
||||
return new Date(this.createdTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* A link to the application's icon.
|
||||
* @param {StaticImageURLOptions} [options={}] Options for the Image URL
|
||||
* @returns {?string}
|
||||
*/
|
||||
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 {StaticImageURLOptions} [options={}] Options for the Image URL
|
||||
* @returns {?string}
|
||||
*/
|
||||
coverURL({ 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's id
|
||||
* @property {string} name The asset's name
|
||||
* @property {string} type The asset's type
|
||||
*/
|
||||
|
||||
/**
|
||||
* Gets the application's rich presence assets.
|
||||
* @returns {Promise<Array<ApplicationAsset>>}
|
||||
*/
|
||||
async fetchAssets() {
|
||||
const assets = await this.client.api.oauth2.applications(this.id).assets.get();
|
||||
return 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;
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const { TypeError } = require('../../errors');
|
||||
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 {CollectorFilter} [filter] The filter applied to this collector
|
||||
* @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, 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}
|
||||
* @returns {boolean|Promise<boolean>}
|
||||
*/
|
||||
this.filter = options.filter ?? (() => true);
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
if (typeof this.filter !== 'function') {
|
||||
throw new TypeError('INVALID_TYPE', 'options.filter', 'function');
|
||||
}
|
||||
|
||||
this.handleCollect = this.handleCollect.bind(this);
|
||||
this.handleDispose = this.handleDispose.bind(this);
|
||||
|
||||
if (options.time) this._timeout = setTimeout(() => this.stop('time'), options.time).unref();
|
||||
if (options.idle) this._idletimeout = setTimeout(() => this.stop('idle'), options.idle).unref();
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this to handle an event as a collectable element. Accepts any event data as parameters.
|
||||
* @param {...*} args The arguments emitted by the listener
|
||||
* @returns {Promise<void>}
|
||||
* @emits Collector#collect
|
||||
*/
|
||||
async handleCollect(...args) {
|
||||
const collect = await 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) {
|
||||
clearTimeout(this._idletimeout);
|
||||
this._idletimeout = setTimeout(() => this.stop('idle'), this.options.idle).unref();
|
||||
}
|
||||
}
|
||||
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
|
||||
* @returns {Promise<void>}
|
||||
* @emits Collector#dispose
|
||||
*/
|
||||
async handleDispose(...args) {
|
||||
if (!this.options.dispose) return;
|
||||
|
||||
const dispose = this.dispose(...args);
|
||||
if (!dispose || !(await 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) {
|
||||
clearTimeout(this._timeout);
|
||||
this._timeout = null;
|
||||
}
|
||||
if (this._idletimeout) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Options used to reset timeout and idle timer of a {@link Collector}.
|
||||
* @typedef {Object} CollectorResetTimerOptions
|
||||
* @property {number} [time] How long to run the collector for (in milliseconds)
|
||||
* @property {number} [idle] How long to wait to stop the collector after inactivity (in milliseconds)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resets the collector's timeout and idle timer.
|
||||
* @param {CollectorResetTimerOptions} [options] Options for reseting
|
||||
*/
|
||||
resetTimer({ time, idle } = {}) {
|
||||
if (this._timeout) {
|
||||
clearTimeout(this._timeout);
|
||||
this._timeout = setTimeout(() => this.stop('time'), time ?? this.options.time).unref();
|
||||
}
|
||||
if (this._idletimeout) {
|
||||
clearTimeout(this._idletimeout);
|
||||
this._idletimeout = setTimeout(() => this.stop('idle'), idle ?? this.options.idle).unref();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the collector should end, and if so, ends it.
|
||||
* @returns {boolean} Whether the collector ended or not
|
||||
*/
|
||||
checkEnd() {
|
||||
const reason = this.endReason;
|
||||
if (reason) this.stop(reason);
|
||||
return Boolean(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 */
|
||||
/**
|
||||
* The reason this collector has ended with, or null if it hasn't ended yet
|
||||
* @type {?string}
|
||||
* @readonly
|
||||
* @abstract
|
||||
*/
|
||||
get endReason() {}
|
||||
|
||||
/**
|
||||
* 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 {?(*|Promise<?*>)} 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() {}
|
||||
/* eslint-enable no-empty-function */
|
||||
}
|
||||
|
||||
module.exports = Collector;
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
'use strict';
|
||||
|
||||
const { Error } = require('../../errors');
|
||||
const { InteractionResponseTypes } = require('../../util/Constants');
|
||||
const MessageFlags = require('../../util/MessageFlags');
|
||||
const MessagePayload = require('../MessagePayload');
|
||||
|
||||
/**
|
||||
* Interface for classes that support shared interaction response types.
|
||||
* @interface
|
||||
*/
|
||||
class InteractionResponses {
|
||||
/**
|
||||
* Options for deferring the reply to an {@link Interaction}.
|
||||
* @typedef {Object} InteractionDeferReplyOptions
|
||||
* @property {boolean} [ephemeral] Whether the reply should be ephemeral
|
||||
* @property {boolean} [fetchReply] Whether to fetch the reply
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for deferring and updating the reply to a {@link ButtonInteraction}.
|
||||
* @typedef {Object} InteractionDeferUpdateOptions
|
||||
* @property {boolean} [fetchReply] Whether to fetch the reply
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for a reply to an {@link Interaction}.
|
||||
* @typedef {BaseMessageOptions} InteractionReplyOptions
|
||||
* @property {boolean} [ephemeral] Whether the reply should be ephemeral
|
||||
* @property {boolean} [fetchReply] Whether to fetch the reply
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for updating the message received from a {@link ButtonInteraction}.
|
||||
* @typedef {MessageEditOptions} InteractionUpdateOptions
|
||||
* @property {boolean} [fetchReply] Whether to fetch the reply
|
||||
*/
|
||||
|
||||
/**
|
||||
* Defers the reply to this interaction.
|
||||
* @param {InteractionDeferReplyOptions} [options] Options for deferring the reply to this interaction
|
||||
* @returns {Promise<Message|APIMessage|void>}
|
||||
* @example
|
||||
* // Defer the reply to this interaction
|
||||
* interaction.deferReply()
|
||||
* .then(console.log)
|
||||
* .catch(console.error)
|
||||
* @example
|
||||
* // Defer to send an ephemeral reply later
|
||||
* interaction.deferReply({ ephemeral: true })
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async deferReply(options = {}) {
|
||||
if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED');
|
||||
if (options.fetchReply && options.ephemeral) throw new Error('INTERACTION_FETCH_EPHEMERAL');
|
||||
this.ephemeral = options.ephemeral ?? false;
|
||||
await this.client.api.interactions(this.id, this.token).callback.post({
|
||||
data: {
|
||||
type: InteractionResponseTypes.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
flags: options.ephemeral ? MessageFlags.FLAGS.EPHEMERAL : undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
this.deferred = true;
|
||||
|
||||
return options.fetchReply ? this.fetchReply() : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a reply to this interaction.
|
||||
* @param {string|MessagePayload|InteractionReplyOptions} options The options for the reply
|
||||
* @returns {Promise<Message|APIMessage|void>}
|
||||
* @example
|
||||
* // Reply to the interaction with an embed
|
||||
* const embed = new MessageEmbed().setDescription('Pong!');
|
||||
*
|
||||
* interaction.reply({ embeds: [embed] })
|
||||
* .then(() => console.log('Reply sent.'))
|
||||
* .catch(console.error);
|
||||
* @example
|
||||
* // Create an ephemeral reply
|
||||
* interaction.reply({ content: 'Pong!', ephemeral: true })
|
||||
* .then(() => console.log('Reply sent.'))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async reply(options) {
|
||||
if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED');
|
||||
if (options.fetchReply && options.ephemeral) throw new Error('INTERACTION_FETCH_EPHEMERAL');
|
||||
this.ephemeral = options.ephemeral ?? false;
|
||||
|
||||
let messagePayload;
|
||||
if (options instanceof MessagePayload) messagePayload = options;
|
||||
else messagePayload = MessagePayload.create(this, options);
|
||||
|
||||
const { data, files } = await messagePayload.resolveData().resolveFiles();
|
||||
|
||||
await this.client.api.interactions(this.id, this.token).callback.post({
|
||||
data: {
|
||||
type: InteractionResponseTypes.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data,
|
||||
},
|
||||
files,
|
||||
});
|
||||
this.replied = true;
|
||||
|
||||
return options.fetchReply ? this.fetchReply() : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the initial reply to this interaction.
|
||||
* @see Webhook#fetchMessage
|
||||
* @returns {Promise<Message|APIMessage>}
|
||||
* @example
|
||||
* // Fetch the reply to this interaction
|
||||
* interaction.fetchReply()
|
||||
* .then(reply => console.log(`Replied with ${reply.content}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
fetchReply() {
|
||||
if (this.ephemeral) throw new Error('INTERACTION_EPHEMERAL_REPLIED');
|
||||
return this.webhook.fetchMessage('@original');
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the initial reply to this interaction.
|
||||
* @see Webhook#editMessage
|
||||
* @param {string|MessagePayload|WebhookEditMessageOptions} options The new options for the message
|
||||
* @returns {Promise<Message|APIMessage>}
|
||||
* @example
|
||||
* // Edit the reply to this interaction
|
||||
* interaction.editReply('New content')
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async editReply(options) {
|
||||
if (!this.deferred && !this.replied) throw new Error('INTERACTION_NOT_REPLIED');
|
||||
const message = await this.webhook.editMessage('@original', options);
|
||||
this.replied = true;
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the initial reply to this interaction.
|
||||
* @see Webhook#deleteMessage
|
||||
* @returns {Promise<void>}
|
||||
* @example
|
||||
* // Delete the reply to this interaction
|
||||
* interaction.deleteReply()
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async deleteReply() {
|
||||
if (this.ephemeral) throw new Error('INTERACTION_EPHEMERAL_REPLIED');
|
||||
await this.webhook.deleteMessage('@original');
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a follow-up message to this interaction.
|
||||
* @param {string|MessagePayload|InteractionReplyOptions} options The options for the reply
|
||||
* @returns {Promise<Message|APIMessage>}
|
||||
*/
|
||||
followUp(options) {
|
||||
return this.webhook.send(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Defers an update to the message to which the component was attached.
|
||||
* @param {InteractionDeferUpdateOptions} [options] Options for deferring the update to this interaction
|
||||
* @returns {Promise<Message|APIMessage|void>}
|
||||
* @example
|
||||
* // Defer updating and reset the component's loading state
|
||||
* interaction.deferUpdate()
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async deferUpdate(options = {}) {
|
||||
if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED');
|
||||
if (options.fetchReply && new MessageFlags(this.message.flags).has(MessageFlags.FLAGS.EPHEMERAL)) {
|
||||
throw new Error('INTERACTION_FETCH_EPHEMERAL');
|
||||
}
|
||||
await this.client.api.interactions(this.id, this.token).callback.post({
|
||||
data: {
|
||||
type: InteractionResponseTypes.DEFERRED_MESSAGE_UPDATE,
|
||||
},
|
||||
});
|
||||
this.deferred = true;
|
||||
|
||||
return options.fetchReply ? this.fetchReply() : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the original message of the component on which the interaction was received on.
|
||||
* @param {string|MessagePayload|InteractionUpdateOptions} options The options for the updated message
|
||||
* @returns {Promise<Message|APIMessage|void>}
|
||||
* @example
|
||||
* // Remove the components from the message
|
||||
* interaction.update({
|
||||
* content: "A component interaction was received",
|
||||
* components: []
|
||||
* })
|
||||
* .then(console.log)
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async update(options) {
|
||||
if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED');
|
||||
if (options.fetchReply && new MessageFlags(this.message.flags).has(MessageFlags.FLAGS.EPHEMERAL)) {
|
||||
throw new Error('INTERACTION_FETCH_EPHEMERAL');
|
||||
}
|
||||
|
||||
let messagePayload;
|
||||
if (options instanceof MessagePayload) messagePayload = options;
|
||||
else messagePayload = MessagePayload.create(this, options);
|
||||
|
||||
const { data, files } = await messagePayload.resolveData().resolveFiles();
|
||||
|
||||
await this.client.api.interactions(this.id, this.token).callback.post({
|
||||
data: {
|
||||
type: InteractionResponseTypes.UPDATE_MESSAGE,
|
||||
data,
|
||||
},
|
||||
files,
|
||||
});
|
||||
this.replied = true;
|
||||
|
||||
return options.fetchReply ? this.fetchReply() : undefined;
|
||||
}
|
||||
|
||||
static applyToClass(structure, ignore = []) {
|
||||
const props = [
|
||||
'deferReply',
|
||||
'reply',
|
||||
'fetchReply',
|
||||
'editReply',
|
||||
'deleteReply',
|
||||
'followUp',
|
||||
'deferUpdate',
|
||||
'update',
|
||||
];
|
||||
|
||||
for (const prop of props) {
|
||||
if (ignore.includes(prop)) continue;
|
||||
Object.defineProperty(
|
||||
structure.prototype,
|
||||
prop,
|
||||
Object.getOwnPropertyDescriptor(InteractionResponses.prototype, prop),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = InteractionResponses;
|
||||
+362
@@ -0,0 +1,362 @@
|
||||
'use strict';
|
||||
|
||||
/* eslint-disable import/order */
|
||||
const MessageCollector = require('../MessageCollector');
|
||||
const MessagePayload = require('../MessagePayload');
|
||||
const SnowflakeUtil = require('../../util/SnowflakeUtil');
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const { InteractionTypes } = require('../../util/Constants');
|
||||
const { TypeError, Error } = require('../../errors');
|
||||
const InteractionCollector = require('../InteractionCollector');
|
||||
|
||||
/**
|
||||
* 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 channel's last message id, 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.resolve(this.lastMessageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base options provided when sending.
|
||||
* @typedef {Object} BaseMessageOptions
|
||||
* @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[]|APIEmbed[]} [embeds] The embeds 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
|
||||
* (see [here](https://discord.com/developers/docs/resources/channel#allowed-mentions-object) for more details)
|
||||
* @property {FileOptions[]|BufferResolvable[]|MessageAttachment[]} [files] Files to send with the message
|
||||
* @property {MessageActionRow[]|MessageActionRowOptions[]} [components]
|
||||
* Action rows containing interactive components for the message (buttons, select menus)
|
||||
* @property {StickerResolvable[]} [stickers=[]] Stickers to send in the message
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options provided when sending or editing a message.
|
||||
* @typedef {BaseMessageOptions} MessageOptions
|
||||
* @property {ReplyOptions} [reply] The options for replying to a message
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @property {boolean} [repliedUser=true] Whether the author of the Message being replied to should be pinged
|
||||
*/
|
||||
|
||||
/**
|
||||
* Types of mentions to enable in MessageMentionOptions.
|
||||
* - `roles`
|
||||
* - `users`
|
||||
* - `everyone`
|
||||
* @typedef {string} MessageMentionTypes
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} FileOptions
|
||||
* @property {BufferResolvable} attachment File to attach
|
||||
* @property {string} [name='file.jpg'] Filename of the attachment
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for sending a message with a reply.
|
||||
* @typedef {Object} ReplyOptions
|
||||
* @param {MessageResolvable} messageReference The message to reply to (must be in the same channel and not system)
|
||||
* @param {boolean} [failIfNotExists=true] Whether to error if the referenced message
|
||||
* does not exist (creates a standard message in this case when false)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sends a message to this channel.
|
||||
* @param {string|MessagePayload|MessageOptions} options The options to provide
|
||||
* @returns {Promise<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({
|
||||
* content: '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(options) {
|
||||
const User = require('../User');
|
||||
const GuildMember = require('../GuildMember');
|
||||
|
||||
if (this instanceof User || this instanceof GuildMember) {
|
||||
const dm = await this.createDM();
|
||||
return dm.send(options);
|
||||
}
|
||||
|
||||
let messagePayload;
|
||||
|
||||
if (options instanceof MessagePayload) {
|
||||
messagePayload = options.resolveData();
|
||||
} else {
|
||||
messagePayload = MessagePayload.create(this, options).resolveData();
|
||||
}
|
||||
|
||||
const { data, files } = await messagePayload.resolveFiles();
|
||||
const d = await this.client.api.channels[this.id].messages.post({ data, files });
|
||||
|
||||
const existing = this.messages.cache.get(d.id);
|
||||
if (existing) {
|
||||
const clone = existing._clone();
|
||||
clone._patch(d);
|
||||
return clone;
|
||||
}
|
||||
return this.messages._add(d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a typing indicator in the channel.
|
||||
* @returns {Promise<void>} Resolves upon the typing status being sent
|
||||
* @example
|
||||
* // Start typing in a channel
|
||||
* channel.sendTyping();
|
||||
*/
|
||||
async sendTyping() {
|
||||
await this.client.api.channels(this.id).typing.post();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Message Collector.
|
||||
* @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(options = {}) {
|
||||
return new MessageCollector(this, 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 {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(options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const collector = this.createMessageCollector(options);
|
||||
collector.once('end', (collection, reason) => {
|
||||
if (options.errors?.includes(reason)) {
|
||||
reject(collection);
|
||||
} else {
|
||||
resolve(collection);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a button interaction collector.
|
||||
* @param {MessageComponentCollectorOptions} [options={}] Options to send to the collector
|
||||
* @returns {InteractionCollector}
|
||||
* @example
|
||||
* // Create a button interaction collector
|
||||
* const filter = (interaction) => interaction.customId === 'button' && interaction.user.id === 'someId';
|
||||
* const collector = channel.createMessageComponentCollector({ filter, time: 15000 });
|
||||
* collector.on('collect', i => console.log(`Collected ${i.customId}`));
|
||||
* collector.on('end', collected => console.log(`Collected ${collected.size} items`));
|
||||
*/
|
||||
createMessageComponentCollector(options = {}) {
|
||||
return new InteractionCollector(this.client, {
|
||||
...options,
|
||||
interactionType: InteractionTypes.MESSAGE_COMPONENT,
|
||||
channel: this,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects a single component interaction that passes the filter.
|
||||
* The Promise will reject if the time expires.
|
||||
* @param {AwaitMessageComponentOptions} [options={}] Options to pass to the internal collector
|
||||
* @returns {Promise<MessageComponentInteraction>}
|
||||
* @example
|
||||
* // Collect a message component interaction
|
||||
* const filter = (interaction) => interaction.customId === 'button' && interaction.user.id === 'someId';
|
||||
* channel.awaitMessageComponent({ filter, time: 15000 })
|
||||
* .then(interaction => console.log(`${interaction.customId} was clicked!`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
awaitMessageComponent(options = {}) {
|
||||
const _options = { ...options, max: 1 };
|
||||
return new Promise((resolve, reject) => {
|
||||
const collector = this.createMessageComponentCollector(_options);
|
||||
collector.once('end', (interactions, reason) => {
|
||||
const interaction = interactions.first();
|
||||
if (interaction) resolve(interaction);
|
||||
else reject(new Error('INTERACTION_COLLECTOR_ERROR', reason));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>>} Returns the 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.keys()] : messages.map(m => m.id ?? m);
|
||||
if (filterOld) {
|
||||
messageIds = messageIds.filter(id => Date.now() - SnowflakeUtil.deconstruct(id).timestamp < 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',
|
||||
'sendTyping',
|
||||
'createMessageCollector',
|
||||
'awaitMessages',
|
||||
'createMessageComponentCollector',
|
||||
'awaitMessageComponent',
|
||||
);
|
||||
}
|
||||
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');
|
||||
Reference in new issue
Block a user