Initial commit

This commit is contained in:
March 7th
2022-03-19 17:37:45 +07:00
commit ac49705f3e
282 changed files with 39756 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
'use strict';
const { ActionRow: BuildersActionRow } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
class ActionRow extends BuildersActionRow {
constructor(data) {
super(Transformers.toSnakeCase(data));
}
}
module.exports = ActionRow;
+95
View File
@@ -0,0 +1,95 @@
'use strict';
const BaseGuild = require('./BaseGuild');
/**
* 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) {
if ('features' in data) this.features = data.features;
if ('splash' in data) {
/**
* The hash of the guild invite splash image
* @type {?string}
*/
this.splash = data.splash;
}
if ('banner' in data) {
/**
* The hash of the guild banner
* @type {?string}
*/
this.banner = data.banner;
}
if ('description' in data) {
/**
* The description of the guild, if any
* @type {?string}
*/
this.description = data.description;
}
if ('verification_level' in data) {
/**
* The verification level of the guild
* @type {GuildVerificationLevel}
*/
this.verificationLevel = data.verification_level;
}
if ('vanity_url_code' in data) {
/**
* 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 {GuildNSFWLevel}
*/
this.nsfwLevel = data.nsfw_level;
}
if ('premium_subscription_count' in data) {
/**
* The total number of boosts for this server
* @type {?number}
*/
this.premiumSubscriptionCount = data.premium_subscription_count;
}
}
/**
* The URL to this guild's banner.
* @param {ImageURLOptions} [options={}] Options for the image URL
* @returns {?string}
*/
bannerURL(options = {}) {
return this.banner && this.client.rest.cdn.banner(this.id, this.banner, options);
}
/**
* The URL to this guild's invite splash image.
* @param {ImageURLOptions} [options={}] Options for the image URL
* @returns {?string}
*/
splashURL(options = {}) {
return this.splash && this.client.rest.cdn.splash(this.id, this.splash, options);
}
}
module.exports = AnonymousGuild;
+415
View File
@@ -0,0 +1,415 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { ApplicationCommandOptionType } = require('discord-api-types/v9');
const Base = require('./Base');
const ApplicationCommandPermissionsManager = require('../managers/ApplicationCommandPermissionsManager');
/**
* 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 = data.type;
this._patch(data);
}
_patch(data) {
if ('name' in data) {
/**
* The name of this command
* @type {string}
*/
this.name = data.name;
}
if ('description' in data) {
/**
* The description of this command
* @type {string}
*/
this.description = data.description;
}
if ('options' in data) {
/**
* The options of this command
* @type {ApplicationCommandOption[]}
*/
this.options = data.options.map(o => this.constructor.transformOption(o, true));
} else {
this.options ??= [];
}
if ('default_permission' in data) {
/**
* Whether the command is enabled by default when the app is added to a guild
* @type {boolean}
*/
this.defaultPermission = data.default_permission;
}
if ('version' in data) {
/**
* Autoincrementing version identifier updated during substantial record changes
* @type {Snowflake}
*/
this.version = data.version;
}
}
/**
* The timestamp the command was created at
* @type {number}
* @readonly
*/
get createdTimestamp() {
return DiscordSnowflake.timestampFrom(this.id);
}
/**
* 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, must be in all lowercase if type is
* {@link ApplicationCommandType.ChatInput}
* @property {string} description The description of the command, if type is {@link ApplicationCommandType.ChatInput}
* @property {ApplicationCommandType} [type=ApplicationCommandType.ChatInput] The type of the command
* @property {ApplicationCommandOptionData[]} [options] Options for the command
* @property {boolean} [defaultPermission=true] Whether the command is enabled by default when the app is added to a
* guild
*/
/**
* An option for an application command or subcommand.
* <info>In addition to the listed properties, when used as a parameter,
* API style `snake_case` properties can be used for compatibility with generators like `@discordjs/builders`.</info>
* <warn>Note that providing a value for the `camelCase` counterpart for any `snake_case` property
* will discard the provided `snake_case` property.</warn>
* @typedef {Object} ApplicationCommandOptionData
* @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} [autocomplete] Whether the autocomplete interaction is enabled for a
* {@link ApplicationCommandOptionType.String}, {@link ApplicationCommandOptionType.Integer} or
* {@link ApplicationCommandOptionType.Number} 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)
* @property {ChannelType[]} [channelTypes] When the option type is channel,
* the allowed types of channels that can be selected
* @property {number} [minValue] The minimum value for an {@link ApplicationCommandOptionType.Integer} or
* {@link ApplicationCommandOptionType.Number} option
* @property {number} [maxValue] The maximum value for an {@link ApplicationCommandOptionType.Integer} or
* {@link ApplicationCommandOptionType.Number} option
*/
/**
* 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);
}
/**
* Edits the name of this ApplicationCommand
* @param {string} name The new name of the command
* @returns {Promise<ApplicationCommand>}
*/
setName(name) {
return this.edit({ name });
}
/**
* Edits the description of this ApplicationCommand
* @param {string} description The new description of the command
* @returns {Promise<ApplicationCommand>}
*/
setDescription(description) {
return this.edit({ description });
}
/**
* Edits the default permission of this ApplicationCommand
* @param {boolean} [defaultPermission=true] The default permission for this command
* @returns {Promise<ApplicationCommand>}
*/
setDefaultPermission(defaultPermission = true) {
return this.edit({ defaultPermission });
}
/**
* Edits the options of this ApplicationCommand
* @param {ApplicationCommandOptionData[]} options The options to set for this command
* @returns {Promise<ApplicationCommand>}
*/
setOptions(options) {
return this.edit({ options });
}
/**
* 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);
}
/**
* Whether this command equals another command. It compares all properties, so for most operations
* it is advisable to just compare `command.id === command2.id` as it is much faster and is often
* what most users need.
* @param {ApplicationCommand|ApplicationCommandData|APIApplicationCommand} command The command to compare with
* @param {boolean} [enforceOptionOrder=false] Whether to strictly check that options and choices are in the same
* order in the array <info>The client may not always respect this ordering!</info>
* @returns {boolean}
*/
equals(command, enforceOptionOrder = false) {
// If given an id, check if the id matches
if (command.id && this.id !== command.id) return false;
// Check top level parameters
if (
command.name !== this.name ||
('description' in command && command.description !== this.description) ||
('version' in command && command.version !== this.version) ||
('autocomplete' in command && command.autocomplete !== this.autocomplete) ||
(command.type && command.type !== this.type) ||
// Future proof for options being nullable
// TODO: remove ?? 0 on each when nullable
(command.options?.length ?? 0) !== (this.options?.length ?? 0) ||
(command.defaultPermission ?? command.default_permission ?? true) !== this.defaultPermission
) {
return false;
}
if (command.options) {
return this.constructor.optionsEqual(this.options, command.options, enforceOptionOrder);
}
return true;
}
/**
* Recursively checks that all options for an {@link ApplicationCommand} are equal to the provided options.
* In most cases it is better to compare using {@link ApplicationCommand#equals}
* @param {ApplicationCommandOptionData[]} existing The options on the existing command,
* should be {@link ApplicationCommand#options}
* @param {ApplicationCommandOptionData[]|APIApplicationCommandOption[]} options The options to compare against
* @param {boolean} [enforceOptionOrder=false] Whether to strictly check that options and choices are in the same
* order in the array <info>The client may not always respect this ordering!</info>
* @returns {boolean}
*/
static optionsEqual(existing, options, enforceOptionOrder = false) {
if (existing.length !== options.length) return false;
if (enforceOptionOrder) {
return existing.every((option, index) => this._optionEquals(option, options[index], enforceOptionOrder));
}
const newOptions = new Map(options.map(option => [option.name, option]));
for (const option of existing) {
const foundOption = newOptions.get(option.name);
if (!foundOption || !this._optionEquals(option, foundOption)) return false;
}
return true;
}
/**
* Checks that an option for an {@link ApplicationCommand} is equal to the provided option
* In most cases it is better to compare using {@link ApplicationCommand#equals}
* @param {ApplicationCommandOptionData} existing The option on the existing command,
* should be from {@link ApplicationCommand#options}
* @param {ApplicationCommandOptionData|APIApplicationCommandOption} option The option to compare against
* @param {boolean} [enforceOptionOrder=false] Whether to strictly check that options or choices are in the same
* order in their array <info>The client may not always respect this ordering!</info>
* @returns {boolean}
* @private
*/
static _optionEquals(existing, option, enforceOptionOrder = false) {
if (
option.name !== existing.name ||
option.type !== existing.type ||
option.description !== existing.description ||
option.autocomplete !== existing.autocomplete ||
(option.required ??
([ApplicationCommandOptionType.Subcommand, ApplicationCommandOptionType.SubcommandGroup].includes(option.type)
? undefined
: false)) !== existing.required ||
option.choices?.length !== existing.choices?.length ||
option.options?.length !== existing.options?.length ||
(option.channelTypes ?? option.channel_types)?.length !== existing.channelTypes?.length ||
(option.minValue ?? option.min_value) !== existing.minValue ||
(option.maxValue ?? option.max_value) !== existing.maxValue
) {
return false;
}
if (existing.choices) {
if (
enforceOptionOrder &&
!existing.choices.every(
(choice, index) => choice.name === option.choices[index].name && choice.value === option.choices[index].value,
)
) {
return false;
}
if (!enforceOptionOrder) {
const newChoices = new Map(option.choices.map(choice => [choice.name, choice]));
for (const choice of existing.choices) {
const foundChoice = newChoices.get(choice.name);
if (!foundChoice || foundChoice.value !== choice.value) return false;
}
}
}
if (existing.channelTypes) {
const newTypes = option.channelTypes ?? option.channel_types;
for (const type of existing.channelTypes) {
if (!newTypes.includes(type)) return false;
}
}
if (existing.options) {
return this.optionsEqual(existing.options, option.options, enforceOptionOrder);
}
return true;
}
/**
* 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 {boolean} [autocomplete] Whether the autocomplete interaction is enabled for a
* {@link ApplicationCommandOptionType.String}, {@link ApplicationCommandOptionType.Integer} or
* {@link ApplicationCommandOptionType.Number} option
* @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)
* @property {ChannelType[]} [channelTypes] When the option type is channel,
* the allowed types of channels that can be selected
* @property {number} [minValue] The minimum value for an {@link ApplicationCommandOptionType.Integer} or
* {@link ApplicationCommandOptionType.Number} option
* @property {number} [maxValue] The maximum value for an {@link ApplicationCommandOptionType.Integer} or
* {@link ApplicationCommandOptionType.Number} option
*/
/**
* 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 channelTypesKey = received ? 'channelTypes' : 'channel_types';
const minValueKey = received ? 'minValue' : 'min_value';
const maxValueKey = received ? 'maxValue' : 'max_value';
return {
type: option.type,
name: option.name,
description: option.description,
required:
option.required ??
(option.type === ApplicationCommandOptionType.Subcommand ||
option.type === ApplicationCommandOptionType.SubcommandGroup
? undefined
: false),
autocomplete: option.autocomplete,
choices: option.choices,
options: option.options?.map(o => this.transformOption(o, received)),
[channelTypesKey]: option.channelTypes ?? option.channel_types,
[minValueKey]: option.minValue ?? option.min_value,
[maxValueKey]: option.maxValue ?? option.max_value,
};
}
}
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}
*/
+92
View File
@@ -0,0 +1,92 @@
'use strict';
const { InteractionResponseType, Routes } = require('discord-api-types/v9');
const CommandInteractionOptionResolver = require('./CommandInteractionOptionResolver');
const Interaction = require('./Interaction');
/**
* Represents an autocomplete interaction.
* @extends {Interaction}
*/
class AutocompleteInteraction extends Interaction {
constructor(client, data) {
super(client, data);
/**
* The id of the channel this interaction was sent in
* @type {Snowflake}
* @name AutocompleteInteraction#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;
/**
* The invoked application command's type
* @type {ApplicationCommandType.ChatInput}
*/
this.commandType = data.data.type;
/**
* Whether this interaction has already received a response
* @type {boolean}
*/
this.responded = false;
/**
* The options passed to the command
* @type {CommandInteractionOptionResolver}
*/
this.options = new CommandInteractionOptionResolver(this.client, data.data.options ?? []);
}
/**
* 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;
}
/**
* Sends results for the autocomplete of this interaction.
* @param {ApplicationCommandOptionChoice[]} options The options for the autocomplete
* @returns {Promise<void>}
* @example
* // respond to autocomplete interaction
* interaction.respond([
* {
* name: 'Option 1',
* value: 'option1',
* },
* ])
* .then(console.log)
* .catch(console.error);
*/
async respond(options) {
if (this.responded) throw new Error('INTERACTION_ALREADY_REPLIED');
await this.client.api.interactions(this.id, this.token).callback.post({
body: {
type: InteractionResponseType.ApplicationCommandAutocompleteResult,
data: {
choices: options,
},
},
auth: false,
})
this.responded = true;
}
}
module.exports = AutocompleteInteraction;
+43
View File
@@ -0,0 +1,43 @@
'use strict';
const Util = require('../util/Util');
/**
* Represents a data model that is identifiable by a Snowflake (i.e. Discord API data models).
* @abstract
*/
class Base {
constructor(client) {
/**
* The client that instantiated this
* @name Base#client
* @type {Client}
* @readonly
*/
Object.defineProperty(this, 'client', { value: client });
}
_clone() {
return Object.assign(Object.create(this), this);
}
_patch(data) {
return data;
}
_update(data) {
const clone = this._clone();
this._patch(data);
return clone;
}
toJSON(...props) {
return Util.flatten(this, ...props);
}
valueOf() {
return this.id;
}
}
module.exports = Base;
+118
View File
@@ -0,0 +1,118 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { Routes } = require('discord-api-types/v9');
const Base = require('./Base');
/**
* 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 {GuildFeature[]}
*/
this.features = data.features;
}
/**
* The timestamp this guild was created at
* @type {number}
* @readonly
*/
get createdTimestamp() {
return DiscordSnowflake.timestampFrom(this.id);
}
/**
* 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(options = {}) {
return this.icon && this.client.rest.cdn.icon(this.id, this.icon, options);
}
/**
* Fetches this guild.
* @returns {Promise<Guild>}
*/
async fetch() {
const data = await this.client.api.guilds(this.id).get({
query: new URLSearchParams({ 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
View File
@@ -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 ('name' in data) this.name = data.name;
if ('require_colons' in data) {
/**
* Whether or not this emoji requires colons surrounding it
* @type {?boolean}
*/
this.requiresColons = data.require_colons;
}
if ('managed' in data) {
/**
* Whether this emoji is managed by an external service
* @type {?boolean}
*/
this.managed = data.managed;
}
if ('available' in data) {
/**
* Whether this emoji is available
* @type {?boolean}
*/
this.available = data.available;
}
}
}
module.exports = BaseGuildEmoji;
+229
View File
@@ -0,0 +1,229 @@
'use strict';
const GuildChannel = require('./GuildChannel');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const MessageManager = require('../managers/MessageManager');
const ThreadManager = require('../managers/ThreadManager');
/**
* Represents a text-based guild channel on Discord.
* @extends {GuildChannel}
* @implements {TextBasedChannel}
*/
class BaseGuildTextChannel extends GuildChannel {
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 ? Date.parse(data.last_pin_timestamp) : 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=true] Whether the channel should be considered NSFW
* @param {string} [reason] Reason for changing the channel's NSFW flag
* @returns {Promise<TextChannel>}
*/
setNSFW(nsfw = true, 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);
*/
fetchWebhooks() {
return this.guild.channels.fetchWebhooks(this.id);
}
/**
* Options used to create a {@link Webhook} in a {@link TextChannel} or a {@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)
*/
createWebhook(name, options = {}) {
return this.guild.channels.createWebhook(this.id, name, 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);
}
/**
* 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 create an invite to a guild channel.
* @typedef {Object} CreateInviteOptions
* @property {boolean} [temporary] 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] How long the invite should last (in seconds, 0 for forever)
* @property {number} [maxUses] Maximum number of uses
* @property {boolean} [unique] 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 {@link InviteTargetType.Stream}, the user must be streaming in the channel
* @property {ApplicationResolvable} [targetApplication] The embedded application to open for this invite,
* required if `targetType` is {@link InviteTargetType.Stream}, the application must have the
* {@link InviteTargetType.EmbeddedApplication} flag
* @property {InviteTargetType} [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;
+123
View File
@@ -0,0 +1,123 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { PermissionFlagsBits } = require('discord-api-types/v9');
const GuildChannel = require('./GuildChannel');
/**
* Represents a voice-based guild channel on Discord.
* @extends {GuildChannel}
*/
class BaseGuildVoiceChannel extends GuildChannel {
_patch(data) {
super._patch(data);
if ('rtc_region' in data) {
/**
* The RTC region for this voice-based channel. This region is automatically selected if `null`.
* @type {?string}
*/
this.rtcRegion = data.rtc_region;
}
if ('bitrate' in data) {
/**
* The bitrate of this voice-based channel
* @type {number}
*/
this.bitrate = data.bitrate;
}
if ('user_limit' in data) {
/**
* 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;
const permissions = this.permissionsFor(this.client.user);
if (!permissions) return false;
// This flag allows joining even if timed out
if (permissions.has(PermissionFlagsBits.Administrator, false)) return true;
return (
this.guild.me.communicationDisabledUntilTimestamp < Date.now() &&
permissions.has(PermissionFlagsBits.Connect, false)
);
}
/**
* 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;
+12
View File
@@ -0,0 +1,12 @@
'use strict';
const { ButtonComponent: BuildersButtonComponent } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
class ButtonComponent extends BuildersButtonComponent {
constructor(data) {
super(Transformers.toSnakeCase(data));
}
}
module.exports = ButtonComponent;
+11
View File
@@ -0,0 +1,11 @@
'use strict';
const MessageComponentInteraction = require('./MessageComponentInteraction');
/**
* Represents a button interaction.
* @extends {MessageComponentInteraction}
*/
class ButtonInteraction extends MessageComponentInteraction {}
module.exports = ButtonInteraction;
+32
View File
@@ -0,0 +1,32 @@
'use strict';
const GuildChannel = require('./GuildChannel');
const CategoryChannelChildManager = require('../managers/CategoryChannelChildManager');
/**
* Represents a guild category channel on Discord.
* @extends {GuildChannel}
*/
class CategoryChannel extends GuildChannel {
/**
* A manager of the channels belonging to this category
* @type {CategoryChannelChildManager}
* @readonly
*/
get children() {
return new CategoryChannelChildManager(this);
}
/**
* 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;
+280
View File
@@ -0,0 +1,280 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { ChannelType, Routes } = require('discord-api-types/v9');
const Base = require('./Base');
const { ThreadChannelTypes } = require('../util/Constants');
let CategoryChannel;
let DMChannel;
let NewsChannel;
let StageChannel;
let StoreChannel;
let TextChannel;
let ThreadChannel;
let VoiceChannel;
/**
* Represents any channel on Discord.
* @extends {Base}
* @abstract
*/
class Channel extends Base {
constructor(client, data, immediatePatch = true) {
super(client);
/**
* The type of the channel
* @type {ChannelType}
*/
this.type = data.type;
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 DiscordSnowflake.timestampFrom(this.id);
}
/**
* The time the channel was created at
* @type {Date}
* @readonly
*/
get createdAt() {
return new Date(this.createdTimestamp);
}
/**
* The URL to the channel
* @type {string}
* @readonly
*/
get url() {
return `https://discord.com/channels/${this.isDMBased() ? '@me' : this.guildId}/${this.id}`;
}
/**
* 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);
*/
async delete() {
await this.client.api.channels(this.id).delete();
return 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 a {@link TextChannel}.
* @returns {boolean}
*/
isText() {
return this.type === ChannelType.GuildText;
}
/**
* Indicates whether this channel is a {@link DMChannel}.
* @returns {boolean}
*/
isDM() {
return this.type === ChannelType.DM;
}
/**
* Indicates whether this channel is a {@link VoiceChannel}.
* @returns {boolean}
*/
isVoice() {
return this.type === ChannelType.GuildVoice;
}
/**
* Indicates whether this channel is a {@link PartialGroupDMChannel}.
* @returns {boolean}
*/
isGroupDM() {
return this.type === ChannelType.GroupDM;
}
/**
* Indicates whether this channel is a {@link CategoryChannel}.
* @returns {boolean}
*/
isCategory() {
return this.type === ChannelType.GuildCategory;
}
/**
* Indicates whether this channel is a {@link NewsChannel}.
* @returns {boolean}
*/
isNews() {
return this.type === ChannelType.GuildNews;
}
/**
* Indicates whether this channel is a {@link StoreChannel}.
* @returns {boolean}
*/
isStore() {
return this.type === ChannelType.GuildStore;
}
/**
* Indicates whether this channel is a {@link ThreadChannel}.
* @returns {boolean}
*/
isThread() {
return ThreadChannelTypes.includes(this.type);
}
/**
* Indicates whether this channel is a {@link StageChannel}.
* @returns {boolean}
*/
isStage() {
return this.type === ChannelType.GuildStageVoice;
}
/**
* Indicates whether this channel is {@link TextBasedChannels text-based}.
* @returns {boolean}
*/
isTextBased() {
return 'messages' in this;
}
/**
* Indicates whether this channel is DM-based (either a {@link DMChannel} or a {@link PartialGroupDMChannel}).
* @returns {boolean}
*/
isDMBased() {
return [ChannelType.DM, ChannelType.GroupDM].includes(this.type);
}
/**
* Indicates whether this channel is {@link BaseGuildVoiceChannel voice-based}.
* @returns {boolean}
*/
isVoiceBased() {
return 'bitrate' in this;
}
static create(client, data, guild, { allowUnknownGuild, fromInteraction } = {}) {
CategoryChannel ??= require('./CategoryChannel');
DMChannel ??= require('./DMChannel');
NewsChannel ??= require('./NewsChannel');
StageChannel ??= require('./StageChannel');
StoreChannel ??= require('./StoreChannel');
TextChannel ??= require('./TextChannel');
ThreadChannel ??= require('./ThreadChannel');
VoiceChannel ??= require('./VoiceChannel');
let channel;
if (!data.guild_id && !guild) {
if ((data.recipients && data.type !== ChannelType.GroupDM) || data.type === ChannelType.DM) {
channel = new DMChannel(client, data);
} else if (data.type === ChannelType.GroupDM) {
const PartialGroupDMChannel = require('./PartialGroupDMChannel');
channel = new PartialGroupDMChannel(client, data);
}
} else {
guild ??= client.guilds.cache.get(data.guild_id);
if (guild || allowUnknownGuild) {
switch (data.type) {
case ChannelType.GuildText: {
channel = new TextChannel(guild, data, client);
break;
}
case ChannelType.GuildVoice: {
channel = new VoiceChannel(guild, data, client);
break;
}
case ChannelType.GuildCategory: {
channel = new CategoryChannel(guild, data, client);
break;
}
case ChannelType.GuildNews: {
channel = new NewsChannel(guild, data, client);
break;
}
case ChannelType.GuildStore: {
channel = new StoreChannel(guild, data, client);
break;
}
case ChannelType.GuildStageVoice: {
channel = new StageChannel(guild, data, client);
break;
}
case ChannelType.GuildNewsThread:
case ChannelType.GuildPublicThread:
case ChannelType.GuildPrivateThread: {
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);
}
}
exports.Channel = Channel;
/**
* @external APIChannel
* @see {@link https://discord.com/developers/docs/resources/channel#channel-object}
*/
@@ -0,0 +1,41 @@
'use strict';
const CommandInteraction = require('./CommandInteraction');
const CommandInteractionOptionResolver = require('./CommandInteractionOptionResolver');
/**
* Represents a command interaction.
* @extends {CommandInteraction}
*/
class ChatInputCommandInteraction extends CommandInteraction {
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 ?? {}),
);
}
/**
* Returns a string representation of the command interaction.
* This can then be copied by a user and executed again in a new command while keeping the option order.
* @returns {string}
*/
toString() {
const properties = [
this.commandName,
this.options._group,
this.options._subcommand,
...this.options._hoistedOptions.map(o => `${o.name}:${o.value}`),
];
return `/${properties.filter(Boolean).join(' ')}`;
}
}
module.exports = ChatInputCommandInteraction;
+109
View File
@@ -0,0 +1,109 @@
'use strict';
const Team = require('./Team');
const { Error } = require('../errors/DJSError');
const Application = require('./interfaces/Application');
const ApplicationFlagsBitField = require('../util/ApplicationFlagsBitField');
const ApplicationCommandManager = require('../managers/ApplicationCommandManager');
/**
* 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);
if(!data) return;
if ('flags' in data) {
/**
* The flags this application has
* @type {ApplicationFlagsBitField}
*/
this.flags = new ApplicationFlagsBitField(data.flags).freeze();
}
if ('cover_image' in data) {
/**
* The hash of the application's cover image
* @type {?string}
*/
this.cover = data.cover_image;
} else {
this.cover ??= null;
}
if ('rpc_origins' in data) {
/**
* The application's RPC origins, if enabled
* @type {string[]}
*/
this.rpcOrigins = data.rpc_origins;
} else {
this.rpcOrigins ??= [];
}
if ('bot_require_code_grant' in data) {
/**
* If this application's bot requires a code grant when using the OAuth2 flow
* @type {?boolean}
*/
this.botRequireCodeGrant = data.bot_require_code_grant;
} else {
this.botRequireCodeGrant ??= null;
}
if ('bot_public' in data) {
/**
* If this application's bot is public
* @type {?boolean}
*/
this.botPublic = data.bot_public;
} else {
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() {
if(!this.client.bot) throw new Error("INVALID_USER_METHOD");
const app = await this.client.api.oauth2.applications('@me').get();
this._patch(app);
return this;
}
}
module.exports = ClientApplication;
+80
View File
@@ -0,0 +1,80 @@
'use strict';
const { GatewayOpcodes } = require('discord-api-types/v9');
const { Presence } = require('./Presence');
const { TypeError } = require('../errors');
/**
* Represents the client's presence.
* @extends {Presence}
*/
class ClientPresence extends Presence {
constructor(client, data = {}) {
super(client, Object.assign(data, { status: data.status ?? 'online', user: { id: null } }));
}
/**
* Sets the client's presence
* @param {PresenceData} presence The data to set the presence to
* @returns {ClientPresence}
*/
set(presence) {
const packet = this._parse(presence);
this._patch(packet);
if (typeof presence.shardId === 'undefined') {
this.client.ws.broadcast({ op: GatewayOpcodes.PresenceUpdate, d: packet });
} else if (Array.isArray(presence.shardId)) {
for (const shardId of presence.shardId) {
this.client.ws.shards.get(shardId).send({ op: GatewayOpcodes.PresenceUpdate, d: packet });
}
} else {
this.client.ws.shards.get(presence.shardId).send({ op: GatewayOpcodes.PresenceUpdate, d: packet });
}
return this;
}
/**
* Parses presence data into a packet ready to be sent to Discord
* @param {PresenceData} presence The data to parse
* @returns {APIPresence}
* @private
*/
_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');
activity.type ??= 0;
data.activities.push({
type: 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: a.type,
url: a.url ?? undefined,
})),
);
}
return data;
}
}
module.exports = ClientPresence;
/* eslint-disable max-len */
/**
* @external APIPresence
* @see {@link https://discord.com/developers/docs/rich-presence/how-to#updating-presence-update-presence-payload-fields}
*/
+182
View File
@@ -0,0 +1,182 @@
'use strict';
const { Routes } = require('discord-api-types/v9');
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 {
this.mfaEnabled ??= null;
}
if ('token' in data) this.client.token = data.token;
}
/**
* Represents the client user's presence
* @type {ClientPresence}
* @readonly
*/
get presence() {
return this.client.presence;
}
/**
* Data used to edit the logged in client
* @typedef {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
* @returns {Promise<ClientUser>}
*/
async edit(data) {
if (typeof data.avatar !== 'undefined') data.avatar = await DataResolver.resolveImage(data.avatar);
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);
*/
setAvatar(avatar) {
return this.edit({ 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 {ClientPresence}
* @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 {ClientPresence}
* @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 {ClientPresence}
* @example
* // Set the client user's activity
* client.user.setActivity('discord.js', { type: ActivityType.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=true] Whether or not the user is AFK
* @param {number|number[]} [shardId] Shard Id(s) to have the AFK flag set on
* @returns {ClientPresence}
*/
setAFK(afk = true, shardId) {
return this.setPresence({ afk, shardId });
}
}
module.exports = ClientUser;
+216
View File
@@ -0,0 +1,216 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const Interaction = require('./Interaction');
const InteractionWebhook = require('./InteractionWebhook');
const MessageAttachment = require('./MessageAttachment');
const InteractionResponses = require('./interfaces/InteractionResponses');
/**
* Represents a command interaction.
* @extends {Interaction}
* @implements {InteractionResponses}
* @abstract
*/
class CommandInteraction extends Interaction {
constructor(client, data) {
super(client, data);
/**
* The id of the channel this interaction was sent in
* @type {Snowflake}
* @name CommandInteraction#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;
/**
* The invoked application command's type
* @type {ApplicationCommandType}
*/
this.commandType = data.data.type;
/**
* 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<Snowflake, User>} [users] The resolved users
* @property {Collection<Snowflake, GuildMember|APIGuildMember>} [members] The resolved guild members
* @property {Collection<Snowflake, Role|APIRole>} [roles] The resolved roles
* @property {Collection<Snowflake, Channel|APIChannel>} [channels] The resolved channels
* @property {Collection<Snowflake, Message|APIMessage>} [messages] The resolved messages
* @property {Collection<Snowflake, MessageAttachment>} [attachments] The resolved attachments
*/
/**
* Transforms the resolved received from the API.
* @param {APIInteractionDataResolved} resolved The received resolved objects
* @returns {CommandInteractionResolvedData}
* @private
*/
transformResolved({ members, users, channels, roles, messages, attachments }) {
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);
}
}
if (attachments) {
result.attachments = new Collection();
for (const attachment of Object.values(attachments)) {
const patched = new MessageAttachment(attachment.url, attachment.filename, attachment);
result.attachments.set(attachment.id, patched);
}
}
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 {boolean} [autocomplete] Whether the autocomplete interaction is enabled for a
* {@link ApplicationCommandOptionType.String}, {@link ApplicationCommandOptionType.Integer} or
* {@link ApplicationCommandOptionType.Number} 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|ThreadChannel|APIChannel} [channel] The resolved channel
* @property {Role|APIRole} [role] The resolved role
* @property {MessageAttachment} [attachment] The resolved attachment
*/
/**
* 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: 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;
const attachment = resolved.attachments?.[option.value];
if (attachment) result.attachment = new MessageAttachment(attachment.url, attachment.filename, attachment);
}
return result;
}
// These are here only for documentation purposes - they are implemented by InteractionResponses
/* eslint-disable no-empty-function */
deferReply() {}
reply() {}
fetchReply() {}
editReply() {}
deleteReply() {}
followUp() {}
}
InteractionResponses.applyToClass(CommandInteraction, ['deferUpdate', 'update']);
module.exports = CommandInteraction;
/* eslint-disable max-len */
/**
* @external APIInteractionDataResolved
* @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-resolved-data-structure}
*/
@@ -0,0 +1,272 @@
'use strict';
const { ApplicationCommandOptionType } = require('discord-api-types/v9');
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 === ApplicationCommandOptionType.SubcommandGroup) {
this._group = this._hoistedOptions[0].name;
this._hoistedOptions = this._hoistedOptions[0].options ?? [];
}
// Hoist subcommand if present
if (this._hoistedOptions[0]?.type === ApplicationCommandOptionType.Subcommand) {
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=false] 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 = false) {
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, ApplicationCommandOptionType.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|ThreadChannel|APIChannel)}
* The value of the option, or null if not set and not required.
*/
getChannel(name, required = false) {
const option = this._getTypedOption(name, ApplicationCommandOptionType.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, ApplicationCommandOptionType.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, ApplicationCommandOptionType.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, ApplicationCommandOptionType.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, ApplicationCommandOptionType.User, ['user'], required);
return option?.user ?? null;
}
/**
* Gets a member option.
* @param {string} name The name of the option.
* @returns {?(GuildMember|APIGuildMember)}
* The value of the option, or null if the user is not present in the guild or the option is not set.
*/
getMember(name) {
const option = this._getTypedOption(name, ApplicationCommandOptionType.User, ['member'], false);
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, ApplicationCommandOptionType.Role, ['role'], required);
return option?.role ?? null;
}
/**
* Gets an attachment 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 {?MessageAttachment} The value of the option, or null if not set and not required.
*/
getAttachment(name, required = false) {
const option = this._getTypedOption(name, ApplicationCommandOptionType.Attachment, ['attachment'], required);
return option?.attachment ?? 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,
ApplicationCommandOptionType.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;
}
/**
* Gets the focused option.
* @param {boolean} [getFull=false] Whether to get the full option object
* @returns {string|number|ApplicationCommandOptionChoice}
* The value of the option, or the whole option if getFull is true
*/
getFocused(getFull = false) {
const focusedOption = this._hoistedOptions.find(option => option.focused);
if (!focusedOption) throw new TypeError('AUTOCOMPLETE_INTERACTION_OPTION_NO_FOCUSED_OPTION');
return getFull ? focusedOption : focusedOption.value;
}
}
module.exports = CommandInteractionOptionResolver;
@@ -0,0 +1,59 @@
'use strict';
const { ApplicationCommandOptionType } = require('discord-api-types/v9');
const CommandInteraction = require('./CommandInteraction');
const CommandInteractionOptionResolver = require('./CommandInteractionOptionResolver');
/**
* Represents a context menu interaction.
* @extends {CommandInteraction}
*/
class ContextMenuCommandInteraction extends CommandInteraction {
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;
}
/**
* 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: ApplicationCommandOptionType.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 = ContextMenuCommandInteraction;
+102
View File
@@ -0,0 +1,102 @@
'use strict';
const { ChannelType } = require('discord-api-types/v9');
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 {
constructor(client, data) {
super(client, data);
// Override the channel type so partials have a known type
this.type = ChannelType.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]);
}
if ('last_message_id' in data) {
/**
* The channel's last message id, 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 = Date.parse(data.last_pin_timestamp);
} else {
this.lastPinTimestamp ??= 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;
+12
View File
@@ -0,0 +1,12 @@
'use strict';
const { Embed: BuildersEmbed } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
class Embed extends BuildersEmbed {
constructor(data) {
super(Transformers.toSnakeCase(data));
}
}
module.exports = Embed;
+108
View File
@@ -0,0 +1,108 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const Base = require('./Base');
/**
* 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;
}
/**
* 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 it's 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 && DiscordSnowflake.timestampFrom(this.id);
}
/**
* 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,
});
}
}
exports.Emoji = Emoji;
/**
* @external APIEmoji
* @see {@link https://discord.com/developers/docs/resources/emoji#emoji-object}
*/
File diff suppressed because it is too large Load Diff
+528
View File
@@ -0,0 +1,528 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { OverwriteType, AuditLogEvent } = require('discord-api-types/v9');
const { GuildScheduledEvent } = require('./GuildScheduledEvent');
const Integration = require('./Integration');
const Invite = require('./Invite');
const { StageInstance } = require('./StageInstance');
const { Sticker } = require('./Sticker');
const Webhook = require('./Webhook');
const Partials = require('../util/Partials');
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
* * StageInstance
* * Sticker
* * Thread
* * GuildScheduledEvent
* @typedef {string} AuditLogTargetType
*/
/**
* Key mirror of all available audit log targets.
* @name GuildAuditLogs.Targets
* @type {Object<string, string>}
*/
const Targets = {
All: 'All',
Guild: 'Guild',
GuildScheduledEvent: 'GuildScheduledEvent',
Channel: 'Channel',
User: 'User',
Role: 'Role',
Invite: 'Invite',
Webhook: 'Webhook',
Emoji: 'Emoji',
Message: 'Message',
Integration: 'Integration',
StageInstance: 'StageInstance',
Sticker: 'Sticker',
Thread: 'Thread',
Unknown: 'Unknown',
};
/**
* 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|string, 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 guild scheduled event
* * 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|
* GuildScheduledEvent)} 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.StageInstance;
if (target < 100) return Targets.Sticker;
if (target < 110) return Targets.GuildScheduledEvent;
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 (
[
AuditLogEvent.ChannelCreate,
AuditLogEvent.ChannelOverwriteCreate,
AuditLogEvent.MemberBanRemove,
AuditLogEvent.BotAdd,
AuditLogEvent.RoleCreate,
AuditLogEvent.InviteCreate,
AuditLogEvent.WebhookCreate,
AuditLogEvent.EmojiCreate,
AuditLogEvent.MessagePin,
AuditLogEvent.IntegrationCreate,
AuditLogEvent.StageInstanceCreate,
AuditLogEvent.StickerCreate,
AuditLogEvent.GuildScheduledEventCreate,
AuditLogEvent.ThreadCreate,
].includes(action)
) {
return 'Create';
}
if (
[
AuditLogEvent.ChannelDelete,
AuditLogEvent.ChannelOverwriteDelete,
AuditLogEvent.MemberKick,
AuditLogEvent.MemberPrune,
AuditLogEvent.MemberBanAdd,
AuditLogEvent.MemberDisconnect,
AuditLogEvent.RoleDelete,
AuditLogEvent.InviteDelete,
AuditLogEvent.WebhookDelete,
AuditLogEvent.EmojiDelete,
AuditLogEvent.MessageDelete,
AuditLogEvent.MessageBulkDelete,
AuditLogEvent.MessageUnpin,
AuditLogEvent.IntegrationDelete,
AuditLogEvent.StageInstanceDelete,
AuditLogEvent.StickerDelete,
AuditLogEvent.GuildScheduledEventDelete,
AuditLogEvent.ThreadDelete,
].includes(action)
) {
return 'Delete';
}
if (
[
AuditLogEvent.GuildUpdate,
AuditLogEvent.ChannelUpdate,
AuditLogEvent.ChannelOverwriteUpdate,
AuditLogEvent.MemberUpdate,
AuditLogEvent.MemberRoleUpdate,
AuditLogEvent.MemberMove,
AuditLogEvent.RoleUpdate,
AuditLogEvent.InviteUpdate,
AuditLogEvent.WebhookUpdate,
AuditLogEvent.EmojiUpdate,
AuditLogEvent.IntegrationUpdate,
AuditLogEvent.StageInstanceUpdate,
AuditLogEvent.StickerUpdate,
AuditLogEvent.GuildScheduledEventUpdate,
AuditLogEvent.ThreadUpdate,
].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(AuditLogEvent).find(k => AuditLogEvent[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(Partials.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 AuditLogEvent.MemberPrune:
this.extra = {
removed: Number(data.options.members_removed),
days: Number(data.options.delete_member_days),
};
break;
case AuditLogEvent.MemberMove:
case AuditLogEvent.MessageDelete:
case AuditLogEvent.MessageBulkDelete:
this.extra = {
channel: guild.channels.cache.get(data.options.channel_id) ?? { id: data.options.channel_id },
count: Number(data.options.count),
};
break;
case AuditLogEvent.MessagePin:
case AuditLogEvent.MessageUnpin:
this.extra = {
channel: guild.client.channels.cache.get(data.options.channel_id) ?? { id: data.options.channel_id },
messageId: data.options.message_id,
};
break;
case AuditLogEvent.MemberDisconnect:
this.extra = {
count: Number(data.options.count),
};
break;
case AuditLogEvent.ChannelOverwriteCreate:
case AuditLogEvent.ChannelOverwriteUpdate:
case AuditLogEvent.ChannelOverwriteDelete:
switch (data.options.type) {
case OverwriteType.Role:
this.extra = guild.roles.cache.get(data.options.id) ?? {
id: data.options.id,
name: data.options.role_name,
type: OverwriteType.Role,
};
break;
case OverwriteType.Member:
this.extra = guild.members.cache.get(data.options.id) ?? {
id: data.options.id,
type: OverwriteType.Member,
};
break;
default:
break;
}
break;
case AuditLogEvent.StageInstanceCreate:
case AuditLogEvent.StageInstanceDelete:
case AuditLogEvent.StageInstanceUpdate:
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;
// MemberDisconnect and similar types do not provide a target_id.
} else if (targetType === Targets.User && data.target_id) {
this.target = guild.client.options.partials.includes(Partials.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) {
let change = this.changes.find(c => c.key === 'code');
change = change.new ?? change.old;
this.target =
guild.invites.cache.get(change) ??
new Invite(
guild.client,
this.changes.reduce(
(o, c) => {
o[c.key] = c.new ?? c.old;
return o;
},
{ guild },
),
);
} else if (targetType === Targets.Message) {
// Discord sends a channel id for the MessageBulkDelete action type.
this.target =
data.action_type === AuditLogEvent.MessageBulkDelete
? 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.StageInstance) {
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 (targetType === Targets.GuildScheduledEvent) {
this.target =
guild.scheduledEvents.cache.get(data.target_id) ??
new GuildScheduledEvent(
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 (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 DiscordSnowflake.timestampFrom(this.id);
}
/**
* 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.Targets = Targets;
GuildAuditLogs.Entry = GuildAuditLogsEntry;
module.exports = GuildAuditLogs;
+59
View File
@@ -0,0 +1,59 @@
'use strict';
const Base = require('./Base');
/**
* Represents a ban in a guild on Discord.
* @extends {Base}
*/
class GuildBan extends Base {
constructor(client, data, guild) {
super(client);
/**
* The guild in which the ban is
* @type {Guild}
*/
this.guild = guild;
this._patch(data);
}
_patch(data) {
if ('user' in 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;
+456
View File
@@ -0,0 +1,456 @@
'use strict';
const { PermissionFlagsBits } = require('discord-api-types/v9');
const { Channel } = require('./Channel');
const { Error } = require('../errors');
const PermissionOverwriteManager = require('../managers/PermissionOverwriteManager');
const { VoiceBasedChannelTypes } = require('../util/Constants');
const PermissionsBitField = require('../util/PermissionsBitField');
/**
* 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 {
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 === PermissionsBitField.defaultBit &&
parentVal.allow.bitfield === PermissionsBitField.defaultBit) ||
(!parentVal &&
channelVal.deny.bitfield === PermissionsBitField.defaultBit &&
channelVal.allow.bitfield === PermissionsBitField.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
* @param {boolean} [checkAdmin=true] Whether having `ADMINISTRATOR` will return all permissions
* @returns {?Readonly<PermissionsBitField>}
*/
permissionsFor(memberOrRole, checkAdmin = true) {
const member = this.guild.members.resolve(memberOrRole);
if (member) return this.memberPermissions(member, checkAdmin);
const role = this.guild.roles.resolve(memberOrRole);
return role && this.rolePermissions(role, checkAdmin);
}
overwritesFor(member, verified = false, roles = null) {
if (!verified) member = this.guild.members.resolve(member);
if (!member) return [];
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
* @param {boolean} checkAdmin=true Whether having `ADMINISTRATOR` will return all permissions
* @returns {Readonly<PermissionsBitField>}
* @private
*/
memberPermissions(member, checkAdmin) {
if (checkAdmin && member.id === this.guild.ownerId) {
return new PermissionsBitField(PermissionsBitField.All).freeze();
}
const roles = member.roles.cache;
const permissions = new PermissionsBitField(roles.map(role => role.permissions));
if (checkAdmin && permissions.has(PermissionFlagsBits.Administrator)) {
return new PermissionsBitField(PermissionsBitField.All).freeze();
}
const overwrites = this.overwritesFor(member, true, roles);
return permissions
.remove(overwrites.everyone?.deny ?? PermissionsBitField.defaultBit)
.add(overwrites.everyone?.allow ?? PermissionsBitField.defaultBit)
.remove(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.deny) : PermissionsBitField.defaultBit)
.add(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.allow) : PermissionsBitField.defaultBit)
.remove(overwrites.member?.deny ?? PermissionsBitField.defaultBit)
.add(overwrites.member?.allow ?? PermissionsBitField.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
* @param {boolean} checkAdmin Whether having `ADMINISTRATOR` will return all permissions
* @returns {Readonly<PermissionsBitField>}
* @private
*/
rolePermissions(role, checkAdmin) {
if (checkAdmin && role.permissions.has(PermissionFlagsBits.Administrator)) {
return new PermissionsBitField(PermissionsBitField.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 ?? PermissionsBitField.defaultBit)
.add(everyoneOverwrites?.allow ?? PermissionsBitField.defaultBit)
.remove(roleOverwrites?.deny ?? PermissionsBitField.defaultBit)
.add(roleOverwrites?.allow ?? PermissionsBitField.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(PermissionFlagsBits.ViewChannel, false));
}
/**
* Edits the channel.
* @param {ChannelData} data The new data for the channel
* @param {string} [reason] Reason for editing this channel
* @returns {Promise<GuildChannel>}
* @example
* // Edit a channel
* channel.edit({ name: 'new-channel' })
* .then(console.log)
* .catch(console.error);
*/
edit(data, reason) {
return this.guild.channels.edit(this, data, reason);
}
/**
* 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 the 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 the position of a channel.
* @typedef {Object} SetChannelPositionOptions
* @property {boolean} [relative=false] Whether or not to change the position relative to its current value
* @property {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);
*/
setPosition(position, options = {}) {
return this.guild.channels.setPosition(this, position, options);
}
/**
* 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.rawPosition,
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.manageable && 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;
const permissions = this.permissionsFor(this.client.user);
if (!permissions) return false;
// This flag allows managing even if timed out
if (permissions.has(PermissionFlagsBits.Administrator, false)) return true;
if (this.guild.me.communicationDisabledUntilTimestamp > Date.now()) return false;
const bitfield = VoiceBasedChannelTypes.includes(this.type)
? PermissionFlagsBits.ManageChannels | PermissionFlagsBits.Connect
: PermissionFlagsBits.ViewChannel | PermissionFlagsBits.ManageChannels;
return permissions.has(bitfield, 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(PermissionFlagsBits.ViewChannel, 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.guild.channels.delete(this.id, reason);
return this;
}
}
module.exports = GuildChannel;
+148
View File
@@ -0,0 +1,148 @@
'use strict';
const { PermissionFlagsBits } = require('discord-api-types/v9');
const BaseGuildEmoji = require('./BaseGuildEmoji');
const { Error } = require('../errors');
const GuildEmojiRoleManager = require('../managers/GuildEmojiRoleManager');
/**
* Represents a custom emoji.
* @extends {BaseGuildEmoji}
*/
class GuildEmoji extends BaseGuildEmoji {
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 });
this._patch(data);
}
/**
* 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(PermissionFlagsBits.ManageEmojisAndStickers);
}
/**
* A manager for roles this emoji is active for.
* @type {GuildEmojiRoleManager}
* @readonly
*/
get roles() {
return new GuildEmojiRoleManager(this);
}
/**
* Fetches the author for this emoji
* @returns {Promise<User>}
*/
fetchAuthor() {
return this.guild.emojis.fetchAuthor(this);
}
/**
* Data for editing an emoji.
* @typedef {Object} GuildEmojiEditData
* @property {string} [name] The name of the emoji
* @property {Collection<Snowflake, Role>|RoleResolvable[]} [roles] Roles to restrict emoji to
*/
/**
* Edits the emoji.
* @param {GuildEmojiEditData} data The new data for the emoji
* @param {string} [reason] Reason for editing this emoji
* @returns {Promise<GuildEmoji>}
* @example
* // Edit an emoji
* emoji.edit({ name: 'newemoji' })
* .then(e => console.log(`Edited emoji ${e}`))
* .catch(console.error);
*/
edit(data, reason) {
return this.guild.emojis.edit(this.id, data, reason);
}
/**
* 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.guild.emojis.delete(this.id, 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;
+458
View File
@@ -0,0 +1,458 @@
'use strict';
const { PermissionFlagsBits } = require('discord-api-types/v9');
const Base = require('./Base');
const VoiceState = require('./VoiceState');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const { Error } = require('../errors');
const GuildMemberRoleManager = require('../managers/GuildMemberRoleManager');
const PermissionsBitField = require('../util/PermissionsBitField');
/**
* Represents a member of a guild on Discord.
* @implements {TextBasedChannel}
* @extends {Base}
*/
class GuildMember extends Base {
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 last timestamp this member started boosting the guild
* @type {?number}
*/
this.premiumSinceTimestamp = null;
/**
* 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 = null;
/**
* The timestamp this member's timeout will be removed
* @type {?number}
*/
this.communicationDisabledUntilTimestamp = null;
this._roles = [];
if (data) this._patch(data);
}
_patch(data) {
if ('user' in data) {
/**
* The user that this guild member instance represents
* @type {?User}
*/
this.user = this.client.users._add(data.user, true);
}
if ('nick' in data) this.nickname = data.nick;
if ('avatar' in data) {
/**
* The guild member's avatar hash
* @type {?string}
*/
this.avatar = data.avatar;
} else if (typeof this.avatar !== 'string') {
this.avatar = null;
}
if ('joined_at' in data) this.joinedTimestamp = Date.parse(data.joined_at);
if ('premium_since' in data) {
this.premiumSinceTimestamp = data.premium_since ? Date.parse(data.premium_since) : null;
}
if ('roles' in data) this._roles = data.roles;
if ('pending' in data) {
this.pending = data.pending;
} else if (!this.partial) {
// See https://github.com/discordjs/discord.js/issues/6546 for more info.
this.pending ??= false;
}
if ('communication_disabled_until' in data) {
this.communicationDisabledUntilTimestamp =
data.communication_disabled_until && Date.parse(data.communication_disabled_until);
}
}
_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 === null;
}
/**
* 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 });
}
/**
* A link to the member's guild avatar.
* @param {ImageURLOptions} [options={}] Options for the image URL
* @returns {?string}
*/
avatarURL(options = {}) {
return this.avatar && this.client.rest.cdn.guildMemberAvatar(this.guild.id, this.id, this.avatar, options);
}
/**
* A link to the member's guild avatar if they have one.
* Otherwise, a link to their {@link User#displayAvatarURL} will be returned.
* @param {ImageURLOptions} [options={}] Options for the Image URL
* @returns {string}
*/
displayAvatarURL(options) {
return this.avatarURL(options) ?? this.user.displayAvatarURL(options);
}
/**
* The time this member joined the guild
* @type {?Date}
* @readonly
*/
get joinedAt() {
return this.joinedTimestamp && new Date(this.joinedTimestamp);
}
/**
* The time this member's timeout will be removed
* @type {?Date}
* @readonly
*/
get communicationDisabledUntil() {
return this.communicationDisabledUntilTimestamp && new Date(this.communicationDisabledUntilTimestamp);
}
/**
* The last time this member started boosting the guild
* @type {?Date}
* @readonly
*/
get premiumSince() {
return this.premiumSinceTimestamp && new Date(this.premiumSinceTimestamp);
}
/**
* 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<PermissionsBitField>}
* @readonly
*/
get permissions() {
if (this.user.id === this.guild.ownerId) return new PermissionsBitField(PermissionsBitField.All).freeze();
return new PermissionsBitField(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() {
if (!this.guild.me) throw new Error('GUILD_UNCACHED_ME');
return this.manageable && this.guild.me.permissions.has(PermissionFlagsBits.KickMembers);
}
/**
* Whether this member is bannable by the client user
* @type {boolean}
* @readonly
*/
get bannable() {
if (!this.guild.me) throw new Error('GUILD_UNCACHED_ME');
return this.manageable && this.guild.me.permissions.has(PermissionFlagsBits.BanMembers);
}
/**
* Whether this member is moderatable by the client user
* @type {boolean}
* @readonly
*/
get moderatable() {
return (
!this.permissions.has(PermissionFlagsBits.Administrator) &&
this.manageable &&
(this.guild.me?.permissions.has(PermissionFlagsBits.ModerateMembers) ?? false)
);
}
/**
* Whether this member is currently timed out
* @returns {boolean}
*/
isCommunicationDisabled() {
return this.communicationDisabledUntilTimestamp > Date.now();
}
/**
* 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<PermissionsBitField>}
*/
permissionsIn(channel) {
channel = this.guild.channels.resolve(channel);
if (!channel) throw new Error('GUILD_CHANNEL_RESOLVE');
return channel.permissionsFor(this);
}
/**
* 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.
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<DMChannel>}
*/
createDM(force = false) {
return this.user.createDM(force);
}
/**
* 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({ deleteMessageDays: 7, reason: 'They deserved it' })
* .then(console.log)
* .catch(console.error);
*/
ban(options) {
return this.guild.members.ban(this, options);
}
/**
* Times this guild member out.
* @param {DateResolvable|null} communicationDisabledUntil The date or timestamp
* for the member's communication to be disabled until. Provide `null` to remove the timeout.
* @param {string} [reason] The reason for this timeout.
* @returns {Promise<GuildMember>}
* @example
* // Time a guild member out for 5 minutes
* guildMember.disableCommunicationUntil(Date.now() + (5 * 60 * 1000), 'They deserved it')
* .then(console.log)
* .catch(console.error);
*/
disableCommunicationUntil(communicationDisabledUntil, reason) {
return this.edit({ communicationDisabledUntil }, reason);
}
/**
* Times this guild member out.
* @param {number|null} timeout The time in milliseconds
* for the member's communication to be disabled until. Provide `null` to remove the timeout.
* @param {string} [reason] The reason for this timeout.
* @returns {Promise<GuildMember>}
* @example
* // Time a guild member out for 5 minutes
* guildMember.timeout(5 * 60 * 1000, 'They deserved it')
* .then(console.log)
* .catch(console.error);
*/
timeout(timeout, reason) {
return this.disableCommunicationUntil(timeout && Date.now() + timeout, reason);
}
/**
* 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.avatar === member.avatar &&
this.pending === member.pending &&
this.communicationDisabledUntilTimestamp === member.communicationDisabledUntilTimestamp &&
(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() {
const json = super.toJSON({
guild: 'guildId',
user: 'userId',
displayName: true,
roles: true,
});
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(GuildMember);
exports.GuildMember = GuildMember;
/**
* @external APIGuildMember
* @see {@link https://discord.com/developers/docs/resources/guild#guild-member-object}
*/
+193
View File
@@ -0,0 +1,193 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { Routes } = require('discord-api-types/v9');
const Base = require('./Base');
const GuildPreviewEmoji = require('./GuildPreviewEmoji');
const { Sticker } = require('./Sticker');
/**
* 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);
}
_patch(data) {
/**
* The id of this guild
* @type {string}
*/
this.id = data.id;
if ('name' in data) {
/**
* The name of this guild
* @type {string}
*/
this.name = data.name;
}
if ('icon' in data) {
/**
* The icon of this guild
* @type {?string}
*/
this.icon = data.icon;
}
if ('splash' in data) {
/**
* The splash icon of this guild
* @type {?string}
*/
this.splash = data.splash;
}
if ('discovery_splash' in data) {
/**
* The discovery splash icon of this guild
* @type {?string}
*/
this.discoverySplash = data.discovery_splash;
}
if ('features' in data) {
/**
* An array of enabled guild features
* @type {GuildFeature[]}
*/
this.features = data.features;
}
if ('approximate_member_count' in data) {
/**
* The approximate count of members in this guild
* @type {number}
*/
this.approximateMemberCount = data.approximate_member_count;
}
if ('approximate_presence_count' in data) {
/**
* The approximate count of online members in this guild
* @type {number}
*/
this.approximatePresenceCount = data.approximate_presence_count;
}
if ('description' in data) {
/**
* The description for this guild
* @type {?string}
*/
this.description = data.description;
} else {
this.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));
}
/**
* Collection of stickers belonging to this guild
* @type {Collection<Snowflake, Sticker>}
*/
this.stickers = data.stickers.reduce(
(stickers, sticker) => stickers.set(sticker.id, new Sticker(this.client, sticker)),
new Collection(),
);
}
/**
* The timestamp this guild was created at
* @type {number}
* @readonly
*/
get createdTimestamp() {
return DiscordSnowflake.timestampFrom(this.id);
}
/**
* 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 {ImageURLOptions} [options={}] Options for the image URL
* @returns {?string}
*/
splashURL(options = {}) {
return this.splash && this.client.rest.cdn.splash(this.id, this.splash, options);
}
/**
* The URL to this guild's discovery splash.
* @param {ImageURLOptions} [options={}] Options for the image URL
* @returns {?string}
*/
discoverySplashURL(options = {}) {
return this.discoverySplash && this.client.rest.cdn.discoverySplash(this.id, this.discoverySplash, options);
}
/**
* The URL to this guild's icon.
* @param {ImageURLOptions} [options={}] Options for the image URL
* @returns {?string}
*/
iconURL(options = {}) {
return this.icon && this.client.rest.cdn.icon(this.id, this.icon, options);
}
/**
* 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
View File
@@ -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;
+434
View File
@@ -0,0 +1,434 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { GuildScheduledEventStatus, GuildScheduledEventEntityType, RouteBases } = require('discord-api-types/v9');
const Base = require('./Base');
const { Error } = require('../errors');
/**
* Represents a scheduled event in a {@link Guild}.
* @extends {Base}
*/
class GuildScheduledEvent extends Base {
constructor(client, data) {
super(client);
/**
* The id of the guild scheduled event
* @type {Snowflake}
*/
this.id = data.id;
/**
* The id of the guild this guild scheduled event belongs to
* @type {Snowflake}
*/
this.guildId = data.guild_id;
this._patch(data);
}
_patch(data) {
if ('channel_id' in data) {
/**
* The channel id in which the scheduled event will be hosted,
* or `null` if entity type is {@link GuildScheduledEventEntityType.External}
* @type {?Snowflake}
*/
this.channelId = data.channel_id;
} else {
this.channelId ??= null;
}
if ('creator_id' in data) {
/**
* The id of the user that created this guild scheduled event
* @type {?Snowflake}
*/
this.creatorId = data.creator_id;
} else {
this.creatorId ??= null;
}
/**
* The name of the guild scheduled event
* @type {string}
*/
this.name = data.name;
if ('description' in data) {
/**
* The description of the guild scheduled event
* @type {?string}
*/
this.description = data.description;
} else {
this.description ??= null;
}
/**
* The timestamp the guild scheduled event will start at
* <info>This can be potentially `null` only when it's an {@link AuditLogEntryTarget}</info>
* @type {?number}
*/
this.scheduledStartTimestamp = data.scheduled_start_time ? Date.parse(data.scheduled_start_time) : null;
/**
* The timestamp the guild scheduled event will end at,
* or `null` if the event does not have a scheduled time to end
* @type {?number}
*/
this.scheduledEndTimestamp = data.scheduled_end_time ? Date.parse(data.scheduled_end_time) : null;
/**
* The privacy level of the guild scheduled event
* @type {GuildScheduledEventPrivacyLevel}
*/
this.privacyLevel = data.privacy_level;
/**
* The status of the guild scheduled event
* @type {GuildScheduledEventStatus}
*/
this.status = data.status;
/**
* The type of hosting entity associated with the scheduled event
* @type {GuildScheduledEventEntityType}
*/
this.entityType = data.entity_type;
if ('entity_id' in data) {
/**
* The id of the hosting entity associated with the scheduled event
* @type {?Snowflake}
*/
this.entityId = data.entity_id;
} else {
this.entityId ??= null;
}
if ('user_count' in data) {
/**
* The number of users who are subscribed to this guild scheduled event
* @type {?number}
*/
this.userCount = data.user_count;
} else {
this.userCount ??= null;
}
if ('creator' in data) {
/**
* The user that created this guild scheduled event
* @type {?User}
*/
this.creator = this.client.users._add(data.creator);
} else {
this.creator ??= this.client.users.resolve(this.creatorId);
}
/* eslint-disable max-len */
/**
* Represents the additional metadata for a {@link GuildScheduledEvent}
* @see {@link https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-entity-metadata}
* @typedef {Object} GuildScheduledEventEntityMetadata
* @property {?string} location The location of the guild scheduled event
*/
/* eslint-enable max-len */
if ('entity_metadata' in data) {
if (data.entity_metadata) {
/**
* Additional metadata
* @type {?GuildScheduledEventEntityMetadata}
*/
this.entityMetadata = {
location: data.entity_metadata.location ?? this.entityMetadata?.location ?? null,
};
} else {
this.entityMetadata = null;
}
} else {
this.entityMetadata ??= null;
}
/**
* The cover image hash for this scheduled event
* @type {?string}
*/
this.image = data.image ?? null;
}
/**
* The URL of this scheduled event's cover image
* @param {BaseImageURLOptions} [options={}] Options for image URL
* @returns {?string}
*/
coverImageURL(options = {}) {
return this.image && this.client.rest.cdn.guildScheduledEventCover(this.id, this.image, options);
}
/**
* The timestamp the guild scheduled event was created at
* @type {number}
* @readonly
*/
get createdTimestamp() {
return DiscordSnowflake.timestampFrom(this.id);
}
/**
* The time the guild scheduled event was created at
* @type {Date}
* @readonly
*/
get createdAt() {
return new Date(this.createdTimestamp);
}
/**
* The time the guild scheduled event will start at
* @type {Date}
* @readonly
*/
get scheduledStartAt() {
return new Date(this.scheduledStartTimestamp);
}
/**
* The time the guild scheduled event will end at,
* or `null` if the event does not have a scheduled time to end
* @type {?Date}
* @readonly
*/
get scheduledEndAt() {
return this.scheduledEndTimestamp && new Date(this.scheduledEndTimestamp);
}
/**
* The channel associated with this scheduled event
* @type {?(VoiceChannel|StageChannel)}
* @readonly
*/
get channel() {
return this.client.channels.resolve(this.channelId);
}
/**
* The guild this scheduled event belongs to
* @type {?Guild}
* @readonly
*/
get guild() {
return this.client.guilds.resolve(this.guildId);
}
/**
* The URL to the guild scheduled event
* @type {string}
* @readonly
*/
get url() {
return `${RouteBases.scheduledEvent}/${this.guildId}/${this.id}`;
}
/**
* Options used to create an invite URL to a {@link GuildScheduledEvent}
* @typedef {CreateInviteOptions} CreateGuildScheduledEventInviteURLOptions
* @property {GuildInvitableChannelResolvable} [channel] The channel to create the invite in.
* <warn>This is required when the `entityType` of `GuildScheduledEvent` is
* {@link GuildScheduledEventEntityType.External}, gets ignored otherwise</warn>
*/
/**
* Creates an invite URL to this guild scheduled event.
* @param {CreateGuildScheduledEventInviteURLOptions} [options] The options to create the invite
* @returns {Promise<string>}
*/
async createInviteURL(options) {
let channelId = this.channelId;
if (this.entityType === GuildScheduledEventEntityType.External) {
if (!options?.channel) throw new Error('INVITE_OPTIONS_MISSING_CHANNEL');
channelId = this.guild.channels.resolveId(options.channel);
if (!channelId) throw new Error('GUILD_CHANNEL_RESOLVE');
}
const invite = await this.guild.invites.create(channelId, options);
return `${RouteBases.invite}/${invite.code}?event=${this.id}`;
}
/**
* Edits this guild scheduled event.
* @param {GuildScheduledEventEditOptions} options The options to edit the guild scheduled event
* @returns {Promise<GuildScheduledEvent>}
* @example
* // Edit a guild scheduled event
* guildScheduledEvent.edit({ name: 'Party' })
* .then(guildScheduledEvent => console.log(guildScheduledEvent))
* .catch(console.error);
*/
edit(options) {
return this.guild.scheduledEvents.edit(this.id, options);
}
/**
* Deletes this guild scheduled event.
* @returns {Promise<GuildScheduledEvent>}
* @example
* // Delete a guild scheduled event
* guildScheduledEvent.delete()
* .then(guildScheduledEvent => console.log(guildScheduledEvent))
* .catch(console.error);
*/
async delete() {
await this.guild.scheduledEvents.delete(this.id);
return this;
}
/**
* Sets a new name for the guild scheduled event.
* @param {string} name The new name of the guild scheduled event
* @param {string} [reason] The reason for changing the name
* @returns {Promise<GuildScheduledEvent>}
* @example
* // Set name of a guild scheduled event
* guildScheduledEvent.setName('Birthday Party')
* .then(guildScheduledEvent => console.log(`Set the name to: ${guildScheduledEvent.name}`))
* .catch(console.error);
*/
setName(name, reason) {
return this.edit({ name, reason });
}
/**
* Sets a new time to schedule the event at.
* @param {DateResolvable} scheduledStartTime The time to schedule the event at
* @param {string} [reason] The reason for changing the scheduled start time
* @returns {Promise<GuildScheduledEvent>}
* @example
* // Set start time of a guild scheduled event
* guildScheduledEvent.setScheduledStartTime('2022-09-24T00:00:00+05:30')
* .then(guildScheduledEvent => console.log(`Set the start time to: ${guildScheduledEvent.scheduledStartTime}`))
* .catch(console.error);
*/
setScheduledStartTime(scheduledStartTime, reason) {
return this.edit({ scheduledStartTime, reason });
}
// TODO: scheduledEndTime gets reset on passing null but it hasn't been documented
/**
* Sets a new time to end the event at.
* @param {DateResolvable} scheduledEndTime The time to end the event at
* @param {string} [reason] The reason for changing the scheduled end time
* @returns {Promise<GuildScheduledEvent>}
* @example
* // Set end time of a guild scheduled event
* guildScheduledEvent.setScheduledEndTime('2022-09-25T00:00:00+05:30')
* .then(guildScheduledEvent => console.log(`Set the end time to: ${guildScheduledEvent.scheduledEndTime}`))
* .catch(console.error);
*/
setScheduledEndTime(scheduledEndTime, reason) {
return this.edit({ scheduledEndTime, reason });
}
/**
* Sets the new description of the guild scheduled event.
* @param {string} description The description of the guild scheduled event
* @param {string} [reason] The reason for changing the description
* @returns {Promise<GuildScheduledEvent>}
* @example
* // Set description of a guild scheduled event
* guildScheduledEvent.setDescription('A virtual birthday party')
* .then(guildScheduledEvent => console.log(`Set the description to: ${guildScheduledEvent.description}`))
* .catch(console.error);
*/
setDescription(description, reason) {
return this.edit({ description, reason });
}
/**
* Sets the new status of the guild scheduled event.
* <info>If you're working with TypeScript, use this method in conjunction with status type-guards
* like {@link GuildScheduledEvent#isScheduled} to get only valid status as suggestion</info>
* @param {GuildScheduledEventStatus|number} status The status of the guild scheduled event
* @param {string} [reason] The reason for changing the status
* @returns {Promise<GuildScheduledEvent>}
* @example
* // Set status of a guild scheduled event
* guildScheduledEvent.setStatus(GuildScheduledEventStatus.Active)
* .then(guildScheduledEvent => console.log(`Set the status to: ${guildScheduledEvent.status}`))
* .catch(console.error);
*/
setStatus(status, reason) {
return this.edit({ status, reason });
}
/**
* Sets the new location of the guild scheduled event.
* @param {string} location The location of the guild scheduled event
* @param {string} [reason] The reason for changing the location
* @returns {Promise<GuildScheduledEvent>}
* @example
* // Set location of a guild scheduled event
* guildScheduledEvent.setLocation('Earth')
* .then(guildScheduledEvent => console.log(`Set the location to: ${guildScheduledEvent.entityMetadata.location}`))
* .catch(console.error);
*/
setLocation(location, reason) {
return this.edit({ entityMetadata: { location }, reason });
}
/**
* Fetches subscribers of this guild scheduled event.
* @param {FetchGuildScheduledEventSubscribersOptions} [options] Options for fetching the subscribers
* @returns {Promise<Collection<Snowflake, GuildScheduledEventUser>>}
*/
fetchSubscribers(options) {
return this.guild.scheduledEvents.fetchSubscribers(this.id, options);
}
/**
* When concatenated with a string, this automatically concatenates the event's URL instead of the object.
* @returns {string}
* @example
* // Logs: Event: https://discord.com/events/412345678901234567/499876543211234567
* console.log(`Event: ${guildScheduledEvent}`);
*/
toString() {
return this.url;
}
/**
* Indicates whether this guild scheduled event has an {@link GuildScheduledEventStatus.Active} status.
* @returns {boolean}
*/
isActive() {
return this.status === GuildScheduledEventStatus.Active;
}
/**
* Indicates whether this guild scheduled event has a {@link GuildScheduledEventStatus.Canceled} status.
* @returns {boolean}
*/
isCanceled() {
return this.status === GuildScheduledEventStatus.Canceled;
}
/**
* Indicates whether this guild scheduled event has a {@link GuildScheduledEventStatus.Completed} status.
* @returns {boolean}
*/
isCompleted() {
return this.status === GuildScheduledEventStatus.Completed;
}
/**
* Indicates whether this guild scheduled event has a {@link GuildScheduledEventStatus.Scheduled} status.
* @returns {boolean}
*/
isScheduled() {
return this.status === GuildScheduledEventStatus.Scheduled;
}
}
exports.GuildScheduledEvent = GuildScheduledEvent;
+237
View File
@@ -0,0 +1,237 @@
'use strict';
const { setTimeout, clearTimeout } = require('node:timers');
const { RouteBases, Routes } = require('discord-api-types/v9');
const Base = require('./Base');
const DataResolver = require('../util/DataResolver');
const Events = require('../util/Events');
/**
* Represents the template for a guild.
* @extends {Base}
*/
class GuildTemplate extends Base {
constructor(client, data) {
super(client);
this._patch(data);
}
_patch(data) {
if ('code' in data) {
/**
* The unique code of this template
* @type {string}
*/
this.code = data.code;
}
if ('name' in data) {
/**
* The name of this template
* @type {string}
*/
this.name = data.name;
}
if ('description' in data) {
/**
* The description of this template
* @type {?string}
*/
this.description = data.description;
}
if ('usage_count' in data) {
/**
* The amount of times this template has been used
* @type {number}
*/
this.usageCount = data.usage_count;
}
if ('creator_id' in data) {
/**
* The id of the user that created this template
* @type {Snowflake}
*/
this.creatorId = data.creator_id;
}
if ('creator' in data) {
/**
* The user that created this template
* @type {User}
*/
this.creator = this.client.users._add(data.creator);
}
if ('created_at' in data) {
/**
* The timestamp of when this template was created at
* @type {number}
*/
this.createdTimestamp = Date.parse(data.created_at);
}
if ('updated_at' in data) {
/**
* The timestamp of when this template was last synced to the guild
* @type {number}
*/
this.updatedTimestamp = Date.parse(data.updated_at);
}
if ('source_guild_id' in data) {
/**
* The id of the guild that this template belongs to
* @type {Snowflake}
*/
this.guildId = data.source_guild_id;
}
if ('serialized_source_guild' in data) {
/**
* 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 on 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.rest.post(Routes.template(this.code), {
body: {
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.GuildCreate, handleGuild);
client.decrementMaxListeners();
resolve(guild);
};
const handleGuild = guild => {
if (guild.id === data.id) {
clearTimeout(timeout);
resolveGuild(guild);
}
};
client.incrementMaxListeners();
client.on(Events.GuildCreate, handleGuild);
const timeout = setTimeout(() => resolveGuild(client.guilds._add(data)), 10_000).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({ body: { 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 time when this template was created at
* @type {Date}
* @readonly
*/
get createdAt() {
return new Date(this.createdTimestamp);
}
/**
* The time when this template was last synced to the guild
* @type {Date}
* @readonly
*/
get updatedAt() {
return new Date(this.updatedTimestamp);
}
/**
* 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 `${RouteBases.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;
+208
View File
@@ -0,0 +1,208 @@
'use strict';
const { Routes } = require('discord-api-types/v9');
const Base = require('./Base');
const IntegrationApplication = require('./IntegrationApplication');
/**
* The information account for an integration
* @typedef {Object} IntegrationAccount
* @property {Snowflake|string} id The id of the account
* @property {string} name The name of the account
*/
/**
* The type of an {@link Integration}. This can be:
* * `twitch`
* * `youtube`
* * `discord`
* @typedef {string} IntegrationType
*/
/**
* 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|string}
*/
this.id = data.id;
/**
* The integration name
* @type {string}
*/
this.name = data.name;
/**
* The integration type
* @type {IntegrationType}
*/
this.type = data.type;
/**
* Whether this integration is enabled
* @type {boolean}
*/
this.enabled = data.enabled;
if ('syncing' in data) {
/**
* Whether this integration is syncing
* @type {?boolean}
*/
this.syncing = data.syncing;
} else {
this.syncing ??= null;
}
/**
* The role that this integration uses for subscribers
* @type {?Role}
*/
this.role = this.guild.roles.resolve(data.role_id);
if ('enable_emoticons' in data) {
/**
* Whether emoticons should be synced for this integration (twitch only currently)
* @type {?boolean}
*/
this.enableEmoticons = data.enable_emoticons;
} else {
this.enableEmoticons ??= null;
}
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;
if ('synced_at' in data) {
/**
* The timestamp at which this integration was last synced at
* @type {?number}
*/
this.syncedTimestamp = Date.parse(data.synced_at);
} else {
this.syncedTimestamp ??= null;
}
if ('subscriber_count' in data) {
/**
* How many subscribers this integration has
* @type {?number}
*/
this.subscriberCount = data.subscriber_count;
} else {
this.subscriberCount ??= null;
}
if ('revoked' in data) {
/**
* Whether this integration has been revoked
* @type {?boolean}
*/
this.revoked = data.revoked;
} else {
this.revoked ??= null;
}
this._patch(data);
}
/**
* The date at which this integration was last synced at
* @type {?Date}
* @readonly
*/
get syncedAt() {
return this.syncedTimestamp && new Date(this.syncedTimestamp);
}
/**
* 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) {
if ('expire_behavior' in data) {
/**
* The behavior of expiring subscribers
* @type {?IntegrationExpireBehavior}
*/
this.expireBehavior = data.expire_behavior;
} else {
this.expireBehavior ??= null;
}
if ('expire_grace_period' in data) {
/**
* The grace period (in days) before expiring subscribers
* @type {?number}
*/
this.expireGracePeriod = data.expire_grace_period;
} else {
this.expireGracePeriod ??= null;
}
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 {
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;
+95
View File
@@ -0,0 +1,95 @@
'use strict';
const Application = require('./interfaces/Application');
/**
* Represents an Integration's OAuth2 Application.
* @extends {Application}
*/
class IntegrationApplication extends Application {
_patch(data) {
super._patch(data);
if ('bot' in data) {
/**
* The bot user for this application
* @type {?User}
*/
this.bot = this.client.users._add(data.bot);
} else {
this.bot ??= null;
}
if ('terms_of_service_url' in data) {
/**
* The URL of the application's terms of service
* @type {?string}
*/
this.termsOfServiceURL = data.terms_of_service_url;
} else {
this.termsOfServiceURL ??= null;
}
if ('privacy_policy_url' in data) {
/**
* The URL of the application's privacy policy
* @type {?string}
*/
this.privacyPolicyURL = data.privacy_policy_url;
} else {
this.privacyPolicyURL ??= null;
}
if ('rpc_origins' in data) {
/**
* The Array of RPC origin URLs
* @type {string[]}
*/
this.rpcOrigins = data.rpc_origins;
} else {
this.rpcOrigins ??= [];
}
if ('summary' in data) {
/**
* The application's summary
* @type {?string}
*/
this.summary = data.summary;
} else {
this.summary ??= null;
}
if ('hook' in data) {
/**
* Whether the application can be default hooked by the client
* @type {?boolean}
*/
this.hook = data.hook;
} else {
this.hook ??= null;
}
if ('cover_image' in data) {
/**
* The hash of the application's cover image
* @type {?string}
*/
this.cover = data.cover_image;
} else {
this.cover ??= null;
}
if ('verify_key' in data) {
/**
* The hex-encoded key for verification in interactions and the GameSDK's GetTicket
* @type {?string}
*/
this.verifyKey = data.verify_key;
} else {
this.verifyKey ??= null;
}
}
}
module.exports = IntegrationApplication;
+235
View File
@@ -0,0 +1,235 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { InteractionType, ApplicationCommandType, ComponentType } = require('discord-api-types/v9');
const Base = require('./Base');
const PermissionsBitField = require('../util/PermissionsBitField');
/**
* Represents an interaction.
* @extends {Base}
*/
class Interaction extends Base {
constructor(client, data) {
super(client);
/**
* The interaction's type
* @type {InteractionType}
*/
this.type = 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 permissions of the member, if one exists, in the channel this interaction was executed in
* @type {?Readonly<PermissionsBitField>}
*/
this.memberPermissions = data.member?.permissions
? new PermissionsBitField(data.member.permissions).freeze()
: null;
/**
* The locale of the user who invoked this interaction
* @type {string}
* @see {@link https://discord.com/developers/docs/reference#locales}
*/
this.locale = data.locale;
/**
* The preferred locale from the guild this interaction was sent in
* @type {?string}
*/
this.guildLocale = data.guild_locale ?? null;
}
/**
* The timestamp the interaction was created at
* @type {number}
* @readonly
*/
get createdTimestamp() {
return DiscordSnowflake.timestampFrom(this.id);
}
/**
* 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 or not this interaction is both cached and received from a guild.
* @returns {boolean}
*/
inCachedGuild() {
return Boolean(this.guild && this.member);
}
/**
* Indicates whether or not this interaction is received from an uncached guild.
* @returns {boolean}
*/
inRawGuild() {
return Boolean(this.guildId && !this.guild && this.member);
}
/**
* Indicates whether this interaction is a {@link CommandInteraction}.
* @returns {boolean}
*/
isCommand() {
return this.type === InteractionType.ApplicationCommand;
}
/**
* Indicates whether this interaction is a {@link ChatInputCommandInteraction}.
* @returns {boolean}
*/
isChatInputCommand() {
return this.isCommand() && this.commandType === ApplicationCommandType.ChatInput;
}
/**
* Indicates whether this interaction is a {@link ContextMenuCommandInteraction}
* @returns {boolean}
*/
isContextMenuCommand() {
return this.isCommand() && [ApplicationCommandType.User, ApplicationCommandType.Message].includes(this.commandType);
}
/**
* Indicates whether this interaction is a {@link UserContextMenuCommandInteraction}
* @returns {boolean}
*/
isUserContextMenuCommand() {
return this.isContextMenuCommand() && this.commandType === ApplicationCommandType.User;
}
/**
* Indicates whether this interaction is a {@link MessageContextMenuCommandInteraction}
* @returns {boolean}
*/
isMessageContextMenuCommand() {
return this.isContextMenuCommand() && this.commandType === ApplicationCommandType.Message;
}
/**
* Indicates whether this interaction is an {@link AutocompleteInteraction}
* @returns {boolean}
*/
isAutocomplete() {
return this.type === InteractionType.ApplicationCommandAutocomplete;
}
/**
* Indicates whether this interaction is a {@link MessageComponentInteraction}.
* @returns {boolean}
*/
isMessageComponent() {
return this.type === InteractionType.MessageComponent;
}
/**
* Indicates whether this interaction is a {@link ButtonInteraction}.
* @returns {boolean}
*/
isButton() {
return this.isMessageComponent() && this.componentType === ComponentType.Button;
}
/**
* Indicates whether this interaction is a {@link SelectMenuInteraction}.
* @returns {boolean}
*/
isSelectMenu() {
return this.isMessageComponent() && this.componentType === ComponentType.SelectMenu;
}
/**
* Indicates whether this interaction can be replied to.
* @returns {boolean}
*/
isRepliable() {
return ![InteractionType.Ping, InteractionType.ApplicationCommandAutocomplete].includes(this.type);
}
}
module.exports = Interaction;
+241
View File
@@ -0,0 +1,241 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const Collector = require('./interfaces/Collector');
const Events = require('../util/Events');
/**
* @typedef {CollectorOptions} InteractionCollectorOptions
* @property {TextBasedChannelResolvable} [channel] The channel to listen to interactions from
* @property {ComponentType} [componentType] The type of component to listen for
* @property {GuildResolvable} [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#event:messageDelete messageDelete} or
* {@link Client#event:messageDeleteBulk messageDeleteBulk}),
* channel ({@link Client#event:channelDelete channelDelete}), or
* guild ({@link Client#event:guildDelete guildDelete}) is deleted.
* <info>Interaction collectors that do not specify `time` or `idle` may be prone to always running.
* Ensure your interaction collectors end via either of these options or manual cancellation.</info>
* @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 type of interaction to collect
* @type {?InteractionType}
*/
this.interactionType = options.interactionType ?? null;
/**
* The type of component to collect
* @type {?ComponentType}
*/
this.componentType = options.componentType ?? null;
/**
* The users that have interacted with 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();
const bulkDeleteListener = messages => {
if (messages.has(this.messageId)) this.stop('messageDelete');
};
if (this.messageId) {
this._handleMessageDeletion = this._handleMessageDeletion.bind(this);
this.client.on(Events.MessageDelete, this._handleMessageDeletion);
this.client.on(Events.MessageBulkDelete, bulkDeleteListener);
}
if (this.channelId) {
this._handleChannelDeletion = this._handleChannelDeletion.bind(this);
this._handleThreadDeletion = this._handleThreadDeletion.bind(this);
this.client.on(Events.ChannelDelete, this._handleChannelDeletion);
this.client.on(Events.ThreadDelete, this._handleThreadDeletion);
}
if (this.guildId) {
this._handleGuildDeletion = this._handleGuildDeletion.bind(this);
this.client.on(Events.GuildDelete, this._handleGuildDeletion);
}
this.client.on(Events.InteractionCreate, this.handleCollect);
this.once('end', () => {
this.client.removeListener(Events.InteractionCreate, this.handleCollect);
this.client.removeListener(Events.MessageDelete, this._handleMessageDeletion);
this.client.removeListener(Events.MessageBulkDelete, bulkDeleteListener);
this.client.removeListener(Events.ChannelDelete, this._handleChannelDeletion);
this.client.removeListener(Events.ThreadDelete, this._handleThreadDeletion);
this.client.removeListener(Events.GuildDelete, 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 || channel.threads?.cache.has(this.channelId)) {
this.stop('channelDelete');
}
}
/**
* Handles checking if the thread has been deleted, and if so, stops the collector with the reason 'threadDelete'.
* @private
* @param {ThreadChannel} thread The thread that was deleted
* @returns {void}
*/
_handleThreadDeletion(thread) {
if (thread.id === this.channelId) {
this.stop('threadDelete');
}
}
/**
* 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
View File
@@ -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;
+317
View File
@@ -0,0 +1,317 @@
'use strict';
const { RouteBases, Routes, PermissionFlagsBits } = require('discord-api-types/v9');
const Base = require('./Base');
const { GuildScheduledEvent } = require('./GuildScheduledEvent');
const IntegrationApplication = require('./IntegrationApplication');
const InviteStageInstance = require('./InviteStageInstance');
const { Error } = require('../errors');
/**
* Represents an invitation to a guild channel.
* @extends {Base}
*/
class Invite extends Base {
constructor(client, data) {
super(client);
this._patch(data);
}
_patch(data) {
const InviteGuild = require('./InviteGuild');
/**
* The guild the invite is for including welcome screen data if present
* @type {?(Guild|InviteGuild)}
*/
this.guild ??= null;
if (data.guild) {
this.guild = this.client.guilds.resolve(data.guild.id) ?? new InviteGuild(this.client, data.guild);
}
if ('code' in data) {
/**
* The code for this invite
* @type {string}
*/
this.code = data.code;
}
if ('approximate_presence_count' in data) {
/**
* The approximate number of online members of the guild this invite is for
* <info>This is only available when the invite was fetched through {@link Client#fetchInvite}.</info>
* @type {?number}
*/
this.presenceCount = data.approximate_presence_count;
} else {
this.presenceCount ??= null;
}
if ('approximate_member_count' in data) {
/**
* The approximate total number of members of the guild this invite is for
* <info>This is only available when the invite was fetched through {@link Client#fetchInvite}.</info>
* @type {?number}
*/
this.memberCount = data.approximate_member_count;
} else {
this.memberCount ??= null;
}
if ('temporary' in data) {
/**
* Whether or not this invite only grants temporary membership
* <info>This is only available when the invite was fetched through {@link GuildInviteManager#fetch}
* or created through {@link GuildInviteManager#create}.</info>
* @type {?boolean}
*/
this.temporary = data.temporary ?? null;
} else {
this.temporary ??= null;
}
if ('max_age' in data) {
/**
* The maximum age of the invite, in seconds, 0 if never expires
* <info>This is only available when the invite was fetched through {@link GuildInviteManager#fetch}
* or created through {@link GuildInviteManager#create}.</info>
* @type {?number}
*/
this.maxAge = data.max_age;
} else {
this.maxAge ??= null;
}
if ('uses' in data) {
/**
* How many times this invite has been used
* <info>This is only available when the invite was fetched through {@link GuildInviteManager#fetch}
* or created through {@link GuildInviteManager#create}.</info>
* @type {?number}
*/
this.uses = data.uses;
} else {
this.uses ??= null;
}
if ('max_uses' in data) {
/**
* The maximum uses of this invite
* <info>This is only available when the invite was fetched through {@link GuildInviteManager#fetch}
* or created through {@link GuildInviteManager#create}.</info>
* @type {?number}
*/
this.maxUses = data.max_uses;
} else {
this.maxUses ??= null;
}
if ('inviter_id' in data) {
/**
* The user's id who created this invite
* @type {?Snowflake}
*/
this.inviterId = data.inviter_id;
} else {
this.inviterId ??= null;
}
if ('inviter' in data) {
this.client.users._add(data.inviter);
this.inviterId = data.inviter.id;
}
if ('target_user' in data) {
/**
* The user whose stream to display for this voice channel stream invite
* @type {?User}
*/
this.targetUser = this.client.users._add(data.target_user);
} else {
this.targetUser ??= null;
}
if ('target_application' in data) {
/**
* The embedded application to open for this voice channel embedded application invite
* @type {?IntegrationApplication}
*/
this.targetApplication = new IntegrationApplication(this.client, data.target_application);
} else {
this.targetApplication ??= null;
}
if ('target_type' in data) {
/**
* The target type
* @type {?InviteTargetType}
*/
this.targetType = data.target_type;
} else {
this.targetType ??= null;
}
if ('channel_id' in data) {
/**
* The id of the channel this invite is for
* @type {?Snowflake}
*/
this.channelId = data.channel_id;
}
if ('channel' in data) {
/**
* The channel this invite is for
* @type {?Channel}
*/
this.channel =
this.client.channels._add(data.channel, this.guild, { cache: false }) ??
this.client.channels.resolve(this.channelId);
this.channelId ??= data.channel.id;
}
if ('created_at' in data) {
/**
* The timestamp this invite was created at
* @type {?number}
*/
this.createdTimestamp = Date.parse(data.created_at);
} else {
this.createdTimestamp ??= null;
}
if ('expires_at' in data) this._expiresTimestamp = Date.parse(data.expires_at);
else this._expiresTimestamp ??= null;
if ('stage_instance' in data) {
/**
* The stage instance data if there is a public {@link StageInstance} in the stage channel this invite is for
* @type {?InviteStageInstance}
* @deprecated
*/
this.stageInstance = new InviteStageInstance(this.client, data.stage_instance, this.channel.id, this.guild.id);
} else {
this.stageInstance ??= null;
}
if ('guild_scheduled_event' in data) {
/**
* The guild scheduled event data if there is a {@link GuildScheduledEvent} in the channel this invite is for
* @type {?GuildScheduledEvent}
*/
this.guildScheduledEvent = new GuildScheduledEvent(this.client, data.guild_scheduled_event);
} else {
this.guildScheduledEvent ??= null;
}
}
/**
* The time the invite was created at
* @type {?Date}
* @readonly
*/
get createdAt() {
return this.createdTimestamp && new Date(this.createdTimestamp);
}
/**
* 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 Boolean(
this.channel?.permissionsFor(this.client.user).has(PermissionFlagsBits.ManageChannels, false) ||
guild.me.permissions.has(PermissionFlagsBits.ManageGuild),
);
}
/**
* The timestamp the invite will expire at
* @type {?number}
* @readonly
*/
get expiresTimestamp() {
return (
this._expiresTimestamp ??
(this.createdTimestamp && this.maxAge ? this.createdTimestamp + this.maxAge * 1_000 : null)
);
}
/**
* The time the invite will expire at
* @type {?Date}
* @readonly
*/
get expiresAt() {
return this.expiresTimestamp && new Date(this.expiresTimestamp);
}
/**
* The user who created this invite
* @type {?User}
* @readonly
*/
get inviter() {
return this.inviterId && this.client.users.resolve(this.inviterId);
}
/**
* The URL to the invite
* @type {string}
* @readonly
*/
get url() {
return `${RouteBases.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
View File
@@ -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;
+87
View File
@@ -0,0 +1,87 @@
'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}
* @deprecated
*/
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) {
if ('topic' in data) {
/**
* The topic of the stage instance
* @type {string}
*/
this.topic = data.topic;
}
if ('participant_count' in data) {
/**
* The number of users in the stage channel
* @type {number}
*/
this.participantCount = data.participant_count;
}
if ('speaker_count' in data) {
/**
* 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;
+948
View File
@@ -0,0 +1,948 @@
'use strict';
const { createComponent, Embed } = require('@discordjs/builders');
const { Collection } = require('@discordjs/collection');
const { DiscordSnowflake } = require('@sapphire/snowflake');
const {
InteractionType,
ChannelType,
MessageType,
MessageFlags,
PermissionFlagsBits,
} = require('discord-api-types/v9');
const Base = require('./Base');
const ClientApplication = require('./ClientApplication');
const InteractionCollector = require('./InteractionCollector');
const MessageAttachment = require('./MessageAttachment');
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 { NonSystemMessageTypes } = require('../util/Constants');
const MessageFlagsBitField = require('../util/MessageFlagsBitField');
const PermissionsBitField = require('../util/PermissionsBitField');
const Util = require('../util/Util');
/**
* Represents a message on Discord.
* @extends {Base}
*/
class Message extends Base {
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;
this._patch(data);
}
_patch(data) {
/**
* The message's id
* @type {Snowflake}
*/
this.id = data.id;
/**
* The timestamp the message was sent at
* @type {number}
*/
this.createdTimestamp = DiscordSnowflake.timestampFrom(this.id);
if ('type' in data) {
/**
* The type of the message
* @type {?MessageType}
*/
this.type = data.type;
/**
* Whether or not this message was sent by Discord, not actually a user (e.g. pin notifications)
* @type {?boolean}
*/
this.system = !NonSystemMessageTypes.includes(this.type);
} else {
this.system ??= null;
this.type ??= null;
}
if ('content' in data) {
/**
* The content of the message
* @type {?string}
*/
this.content = data.content;
} else {
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 {
this.author ??= null;
}
if ('pinned' in data) {
/**
* Whether or not this message is pinned
* @type {?boolean}
*/
this.pinned = Boolean(data.pinned);
} else {
this.pinned ??= null;
}
if ('tts' in data) {
/**
* Whether or not the message was Text-To-Speech
* @type {?boolean}
*/
this.tts = data.tts;
} else {
this.tts ??= null;
}
if ('nonce' in data) {
/**
* A random number or string used for checking message delivery
* <warn>This is only received after the message was sent successfully, and
* lost if re-fetched</warn>
* @type {?string}
*/
this.nonce = data.nonce;
} else {
this.nonce ??= null;
}
if ('embeds' in data) {
/**
* A list of embeds in the message - e.g. YouTube Player
* @type {Embed[]}
*/
this.embeds = data.embeds.map(e => new Embed(e));
} else {
this.embeds = this.embeds?.slice() ?? [];
}
if ('components' in data) {
/**
* A list of MessageActionRows in the message
* @type {ActionRow[]}
*/
this.components = data.components.map(c => createComponent(c));
} else {
this.components = this.components?.slice() ?? [];
}
if ('attachments' in data) {
/**
* 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.attachments);
}
if ('sticker_items' in data || 'stickers' in data) {
/**
* 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);
}
// Discord sends null if the message has not been edited
if (data.edited_timestamp) {
/**
* The timestamp the message was last edited at (if applicable)
* @type {?number}
*/
this.editedTimestamp = Date.parse(data.edited_timestamp);
} else {
this.editedTimestamp ??= null;
}
if ('reactions' in data) {
/**
* 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);
}
}
} else {
this.reactions ??= new ReactionManager(this);
}
if (!this.mentions) {
/**
* 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) {
/**
* The id of the webhook that sent the message, if applicable
* @type {?Snowflake}
*/
this.webhookId = data.webhook_id;
} else {
this.webhookId ??= null;
}
if ('application' in data) {
/**
* Supplemental application information for group activities
* @type {?ClientApplication}
*/
this.groupActivityApplication = new ClientApplication(this.client, data.application);
} else {
this.groupActivityApplication ??= null;
}
if ('application_id' in data) {
/**
* The id of the application of the interaction that sent this message, if any
* @type {?Snowflake}
*/
this.applicationId = data.application_id;
} else {
this.applicationId ??= null;
}
if ('activity' in data) {
/**
* Group activity
* @type {?MessageActivity}
*/
this.activity = {
partyId: data.activity.party_id,
type: data.activity.type,
};
} else {
this.activity ??= 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) {
/**
* Flags that are applied to the message
* @type {Readonly<MessageFlagsBitField>}
*/
this.flags = new MessageFlagsBitField(data.flags).freeze();
} else {
this.flags = new MessageFlagsBitField(this.flags).freeze();
}
/**
* Reference data sent in a message that contains ids identifying the referenced message.
* This can be present in the following types of message:
* * Crossposted messages (`MessageFlags.Crossposted`)
* * {@link MessageType.ChannelFollowAdd}
* * {@link MessageType.ChannelPinnedMessage}
* * {@link MessageType.Reply}
* * {@link MessageType.ThreadStarterMessage}
* @see {@link https://discord.com/developers/docs/resources/channel#message-types}
* @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) {
/**
* Message reference data
* @type {?MessageReference}
*/
this.reference = {
channelId: data.message_reference.channel_id,
guildId: data.message_reference.guild_id,
messageId: data.message_reference.message_id,
};
} else {
this.reference ??= 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: data.interaction.type,
commandName: data.interaction.name,
user: this.client.users._add(data.interaction.user),
};
} else {
this.interaction ??= null;
}
}
/**
* 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);
}
/**
* 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.HasThread);
}
/**
* 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: 15_000 });
* 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 | Snowflake, MessageReaction>>}
* @example
* // Create a reaction collector
* const filter = (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someId'
* message.awaitReactions({ filter, time: 15_000 })
* .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 {ComponentType} [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: 15_000 });
* 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: InteractionType.MessageComponent,
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 {ComponentType} [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: 15_000 })
* .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() {
const precheck = Boolean(this.author.id === this.client.user.id && (!this.guild || this.channel?.viewable));
// Regardless of permissions thread messages cannot be edited if
// the thread is locked.
if (this.channel?.isThread()) {
return precheck && !this.channel.locked;
}
return precheck;
}
/**
* Whether the message is deletable by the client user
* @type {boolean}
* @readonly
*/
get deletable() {
if (!this.guild) {
return this.author.id === this.client.user.id;
}
// DMChannel does not have viewable property, so check viewable after proved that message is on a guild.
if (!this.channel?.viewable) {
return false;
}
const permissions = this.channel?.permissionsFor(this.client.user);
if (!permissions) return false;
// This flag allows deleting even if timed out
if (permissions.has(PermissionFlagsBits.Administrator, false)) return true;
return Boolean(
this.author.id === this.client.user.id ||
(permissions.has(PermissionFlagsBits.ManageMessages, false) &&
this.guild.me.communicationDisabledUntilTimestamp < Date.now()),
);
}
/**
* Whether the message is pinnable by the client user
* @type {boolean}
* @readonly
*/
get pinnable() {
const { channel } = this;
return Boolean(
!this.system &&
(!this.guild ||
(channel?.viewable &&
channel?.permissionsFor(this.client.user)?.has(PermissionFlagsBits.ManageMessages, 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() {
const bitfield =
PermissionFlagsBits.SendMessages |
(this.author.id === this.client.user.id ? PermissionsBitField.defaultBit : PermissionFlagsBits.ManageMessages);
const { channel } = this;
return Boolean(
channel?.type === ChannelType.GuildNews &&
!this.flags.has(MessageFlags.Crossposted) &&
this.type === MessageType.Default &&
channel.viewable &&
channel.permissionsFor(this.client.user)?.has(bitfield, false),
);
}
/**
* Options that can be passed into {@link Message#edit}.
* @typedef {Object} MessageEditOptions
* @property {?string} [content] Content to be edited
* @property {Embed[]|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 `MessageFlags.SuppressEmbeds` 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 {ActionRow[]|ActionRowOptions[]} [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) {
if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED'));
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 === ChannelType.GuildNews) {
* message.crosspost()
* .then(() => console.log('Crossposted message'))
* .catch(console.error);
* }
*/
crosspost() {
if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED'));
return this.channel.messages.crosspost(this.id);
}
/**
* Pins this message to the channel's pinned messages.
* @param {string} [reason] Reason for pinning
* @returns {Promise<Message>}
* @example
* // Pin a message
* message.pin()
* .then(console.log)
* .catch(console.error)
*/
async pin(reason) {
if (!this.channel) throw new Error('CHANNEL_NOT_CACHED');
await this.channel.messages.pin(this.id, reason);
return this;
}
/**
* Unpins this message from the channel's pinned messages.
* @param {string} [reason] Reason for unpinning
* @returns {Promise<Message>}
* @example
* // Unpin a message
* message.unpin()
* .then(console.log)
* .catch(console.error)
*/
async unpin(reason) {
if (!this.channel) throw new Error('CHANNEL_NOT_CACHED');
await this.channel.messages.unpin(this.id, reason);
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) {
if (!this.channel) throw new Error('CHANNEL_NOT_CACHED');
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.resolvePartialEmoji(emoji),
},
true,
).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() {
if (!this.channel) throw new Error('CHANNEL_NOT_CACHED');
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=this.client.options.failIfNotExists] Whether to error if the referenced
* message does not exist (creates a standard message in this case when false)
* @property {StickerResolvable[]} [stickers=[]] Stickers to send in the message
*/
/**
* Send an inline reply to this message.
* @param {string|MessagePayload|ReplyMessageOptions} options The options to provide
* @returns {Promise<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) {
if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED'));
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);
}
/**
* A number that is allowed to be the duration (in minutes) of inactivity after which a thread is automatically
* archived. This can be:
* * `60` (1 hour)
* * `1440` (1 day)
* * `4320` (3 days) <warn>This is only available when the guild has the `THREE_DAY_THREAD_ARCHIVE` feature.</warn>
* * `10080` (7 days) <warn>This is only available when the guild has the `SEVEN_DAY_THREAD_ARCHIVE` feature.</warn>
* * `'MAX'` Based on the guild's features
* @typedef {number|string} ThreadAutoArchiveDuration
*/
/**
* Options for starting a thread on a message.
* @typedef {Object} StartThreadOptions
* @property {string} name The name of the new thread
* @property {ThreadAutoArchiveDuration} [autoArchiveDuration=this.channel.defaultAutoArchiveDuration] 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
* @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the thread in seconds
*/
/**
* 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 (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED'));
if (![ChannelType.GuildText, ChannelType.GuildNews].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) {
if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED'));
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'));
if (this.webhookId === this.applicationId) return Promise.reject(new Error('WEBHOOK_APPLICATION'));
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 MessageFlagsBitField(this.flags.bitfield);
if (suppress) {
flags.add(MessageFlags.SuppressEmbeds);
} else {
flags.remove(MessageFlags.SuppressEmbeds);
}
return this.edit({ flags });
}
/**
* Removes the attachments from this message.
* @returns {Promise<Message>}
*/
removeAttachments() {
return this.edit({ attachments: [] });
}
/**
* Resolves a component by a custom id.
* @param {string} customId The custom id to resolve against
* @returns {?MessageActionRowComponent}
*/
resolveComponent(customId) {
return this.components.flatMap(row => row.components).find(component => component.customId === customId) ?? null;
}
/**
* 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 === Date.parse(rawData.timestamp) &&
this.editedTimestamp === Date.parse(rawData.edited_timestamp);
}
return equal;
}
/**
* Whether this message is from a guild.
* @returns {boolean}
*/
inGuild() {
return Boolean(this.guildId);
}
/**
* 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,
});
}
}
exports.Message = Message;
+171
View File
@@ -0,0 +1,171 @@
'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 description of this attachment.
* @param {string} description The description of the file
* @returns {MessageAttachment} This attachment
*/
setDescription(description) {
this.description = description;
return this;
}
/**
* 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;
}
/**
* Sets whether this attachment is a spoiler
* @param {boolean} [spoiler=true] Whether the attachment should be marked as a spoiler
* @returns {MessageAttachment} This attachment
*/
setSpoiler(spoiler = true) {
if (spoiler === this.spoiler) return this;
if (!spoiler) {
while (this.spoiler) {
this.name = this.name.slice('SPOILER_'.length);
}
return this;
}
this.name = `SPOILER_${this.name}`;
return this;
}
_patch(data) {
/**
* The attachment's id
* @type {Snowflake}
*/
this.id = data.id;
if ('size' in data) {
/**
* The size of this attachment in bytes
* @type {number}
*/
this.size = data.size;
}
if ('url' in data) {
/**
* The URL to this attachment
* @type {string}
*/
this.url = data.url;
}
if ('proxy_url' in data) {
/**
* The Proxy URL to this attachment
* @type {string}
*/
this.proxyURL = data.proxy_url;
}
if ('height' in data) {
/**
* The height of this attachment (if an image or video)
* @type {?number}
*/
this.height = data.height;
} else {
this.height ??= null;
}
if ('width' in data) {
/**
* The width of this attachment (if an image or video)
* @type {?number}
*/
this.width = data.width;
} else {
this.width ??= null;
}
if ('content_type' in data) {
/**
* The media type of this attachment
* @type {?string}
*/
this.contentType = data.content_type;
} else {
this.contentType ??= null;
}
if ('description' in data) {
/**
* The description (alt text) of this attachment
* @type {?string}
*/
this.description = data.description;
} else {
this.description ??= null;
}
/**
* Whether this attachment is ephemeral
* @type {boolean}
*/
this.ephemeral = data.ephemeral ?? false;
}
/**
* Whether or not this attachment has been marked as a spoiler
* @type {boolean}
* @readonly
*/
get spoiler() {
return Util.basename(this.url ?? this.name).startsWith('SPOILER_');
}
toJSON() {
return Util.flatten(this);
}
}
module.exports = MessageAttachment;
/**
* @external APIAttachment
* @see {@link https://discord.com/developers/docs/resources/channel#attachment-object}
*/
+146
View File
@@ -0,0 +1,146 @@
'use strict';
const Collector = require('./interfaces/Collector');
const Events = require('../util/Events');
/**
* @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 ({@link Client#event:channelDelete channelDelete}),
* thread ({@link Client#event:threadDelete threadDelete}), or
* guild ({@link Client#event:guildDelete guildDelete}) is 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._handleThreadDeletion = this._handleThreadDeletion.bind(this);
this._handleGuildDeletion = this._handleGuildDeletion.bind(this);
this.client.incrementMaxListeners();
this.client.on(Events.MessageCreate, this.handleCollect);
this.client.on(Events.MessageDelete, this.handleDispose);
this.client.on(Events.MessageBulkDelete, bulkDeleteListener);
this.client.on(Events.ChannelDelete, this._handleChannelDeletion);
this.client.on(Events.ThreadDelete, this._handleThreadDeletion);
this.client.on(Events.GuildDelete, this._handleGuildDeletion);
this.once('end', () => {
this.client.removeListener(Events.MessageCreate, this.handleCollect);
this.client.removeListener(Events.MessageDelete, this.handleDispose);
this.client.removeListener(Events.MessageBulkDelete, bulkDeleteListener);
this.client.removeListener(Events.ChannelDelete, this._handleChannelDeletion);
this.client.removeListener(Events.ThreadDelete, this._handleThreadDeletion);
this.client.removeListener(Events.GuildDelete, 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 || channel.id === this.channel.parentId) {
this.stop('channelDelete');
}
}
/**
* Handles checking if the thread has been deleted, and if so, stops the collector with the reason 'threadDelete'.
* @private
* @param {ThreadChannel} thread The thread that was deleted
* @returns {void}
*/
_handleThreadDeletion(thread) {
if (thread.id === this.channel.id) {
this.stop('threadDelete');
}
}
/**
* 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;
@@ -0,0 +1,132 @@
'use strict';
const Interaction = require('./Interaction');
const InteractionWebhook = require('./InteractionWebhook');
const InteractionResponses = require('./interfaces/InteractionResponses');
/**
* Represents a message component interaction.
* @extends {Interaction}
* @implements {InteractionResponses}
*/
class MessageComponentInteraction extends Interaction {
constructor(client, data) {
super(client, data);
/**
* The id of the channel this interaction was sent in
* @type {Snowflake}
* @name MessageComponentInteraction#channelId
*/
/**
* 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 {ComponentType}
*/
this.componentType = 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);
}
// 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}
*/
/**
* @external ButtonComponent
* @see {@link https://discord.js.org/#/docs/builders/main/class/ButtonComponent}
*/
/**
* @external SelectMenuComponent
* @see {@link https://discord.js.org/#/docs/builders/main/class/SelectMenuComponent}
*/
/**
* @external SelectMenuOption
* @see {@link https://discord.js.org/#/docs/builders/main/class/SelectMenuComponent}
*/
/**
* @external ActionRow
* @see {@link https://discord.js.org/#/docs/builders/main/class/ActionRow}
*/
/**
* @external Embed
* @see {@link https://discord.js.org/#/docs/builders/main/class/Embed}
*/
@@ -0,0 +1,20 @@
'use strict';
const ContextMenuCommandInteraction = require('./ContextMenuCommandInteraction');
/**
* Represents a message context menu interaction.
* @extends {ContextMenuCommandInteraction}
*/
class MessageContextMenuCommandInteraction extends ContextMenuCommandInteraction {
/**
* The message this interaction was sent from
* @type {Message|APIMessage}
* @readonly
*/
get targetMessage() {
return this.options.getMessage('message');
}
}
module.exports = MessageContextMenuCommandInteraction;
+239
View File
@@ -0,0 +1,239 @@
'use strict';
const { Collection } = require('@discordjs/collection');
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();
for (const d of crosspostedChannels) {
this.crosspostedChannels.set(d.id, {
channelId: d.id,
guildId: d.guild_id,
type: d.type,
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} [ignoreRepliedUser=false] Whether to ignore replied user mention to an user
* @property {boolean} [ignoreEveryone=false] Whether to ignore `@everyone`/`@here` mentions
*/
/**
* Checks if a user, guild member, thread member, role, or channel is mentioned.
* Takes into account user mentions, role mentions, channel mentions,
* replied user mention, 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, ignoreRepliedUser = false, ignoreEveryone = false } = {}) {
const user = this.client.users.resolve(data);
const role = this.guild?.roles.resolve(data);
const channel = this.client.channels.resolve(data);
if (!ignoreRepliedUser && this.users.has(this.repliedUser?.id) && this.repliedUser?.id === user?.id) return true;
if (!ignoreDirect) {
if (this.users.has(user?.id)) return true;
if (this.roles.has(role?.id)) return true;
if (this.channels.has(channel?.id)) return true;
}
if (user && !ignoreEveryone && this.everyone) return true;
if (!ignoreRoles) {
const member = this.guild?.members.resolve(data);
if (member) {
for (const mentionedRole of this.roles.values()) if (member.roles.cache.has(mentionedRole.id)) return true;
}
}
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;
+292
View File
@@ -0,0 +1,292 @@
'use strict';
const { Buffer } = require('node:buffer');
const { isJSONEncodable } = require('@discordjs/builders');
const { MessageFlags } = require('discord-api-types/v9');
const { RangeError } = require('../errors');
const DataResolver = require('../util/DataResolver');
const MessageFlagsBitField = require('../util/MessageFlagsBitField');
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;
/**
* Body sendable to the API
* @type {?APIMessage}
*/
this.body = null;
/**
* Files sendable to the API
* @type {?RawFile[]}
*/
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;
}
/**
* Whether 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 the body.
* @returns {MessagePayload}
*/
resolveBody() {
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 =>
isJSONEncodable(c) ? c.toJSON() : this.target.client.options.jsonTransformer(c),
);
let username;
let avatarURL;
if (isWebhook) {
username = this.options.username ?? this.target.name;
if (this.options.avatarURL) avatarURL = this.options.avatarURL;
}
let flags;
if (
typeof this.options.flags !== 'undefined' ||
(this.isMessage && typeof this.options.reply === 'undefined') ||
this.isMessageManager
) {
flags =
// eslint-disable-next-line eqeqeq
this.options.flags != null
? new MessageFlagsBitField(this.options.flags).bitfield
: this.target.flags?.bitfield;
}
if (isInteraction && this.options.ephemeral) {
flags |= MessageFlags.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,
};
}
}
const attachments = this.options.files?.map((file, index) => ({
id: index.toString(),
description: file.description,
}));
if (Array.isArray(this.options.attachments)) {
this.options.attachments.push(...(attachments ?? []));
} else {
this.options.attachments = attachments;
}
this.body = {
content,
tts,
nonce,
embeds: this.options.embeds?.map(embed =>
isJSONEncodable(embed) ? embed.toJSON() : this.target.client.options.jsonTransformer(embed),
),
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 {Promise<RawFile>}
*/
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 data = await DataResolver.resolveFile(attachment);
return { data, name };
}
/**
* 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}
*/
/**
* @external RawFile
* @see {@link https://discord.js.org/#/docs/rest/main/typedef/RawFile}
*/
+128
View File
@@ -0,0 +1,128 @@
'use strict';
const { Routes } = require('discord-api-types/v9');
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 {
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.me ? [client.user] : []);
this._emoji = new ReactionEmoji(this, data.emoji);
this._patch(data);
}
_patch(data) {
if ('count' in data) {
/**
* 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++;
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;
+32
View File
@@ -0,0 +1,32 @@
'use strict';
const { Routes } = require('discord-api-types/v9');
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 {TextChannelResolvable} channel The channel where the webhook should be created
* @param {string} [reason] Reason for creating the webhook
* @returns {Promise<NewsChannel>}
* @example
* if (channel.type === ChannelType.GuildNews) {
* 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({ body: { webhook_channel_id: channelId }, reason });
return this;
}
}
module.exports = NewsChannel;
+28
View File
@@ -0,0 +1,28 @@
'use strict';
const BaseGuild = require('./BaseGuild');
const PermissionsBitField = require('../util/PermissionsBitField');
/**
* 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<PermissionsBitField>}
*/
this.permissions = new PermissionsBitField(BigInt(data.permissions)).freeze();
}
}
module.exports = OAuth2Guild;
+57
View File
@@ -0,0 +1,57 @@
'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;
/**
* Recipient data received in a {@link PartialGroupDMChannel}.
* @typedef {Object} PartialRecipient
* @property {string} username The username of the recipient
*/
/**
* The recipients of this Group DM Channel.
* @type {PartialRecipient[]}
*/
this.recipients = data.recipients;
}
/**
* The URL to this channel's icon.
* @param {ImageURLOptions} [options={}] Options for the image URL
* @returns {?string}
*/
iconURL(options = {}) {
return this.icon && this.client.rest.cdn.channelIcon(this.id, this.icon, options);
}
delete() {
return Promise.reject(new Error('DELETE_GROUP_DM_CHANNEL'));
}
fetch() {
return Promise.reject(new Error('FETCH_GROUP_DM_CHANNEL'));
}
}
module.exports = PartialGroupDMChannel;
+196
View File
@@ -0,0 +1,196 @@
'use strict';
const { OverwriteType } = require('discord-api-types/v9');
const Base = require('./Base');
const { Role } = require('./Role');
const { TypeError } = require('../errors');
const PermissionsBitField = require('../util/PermissionsBitField');
/**
* 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;
if ('type' in data) {
/**
* The type of this overwrite
* @type {OverwriteType}
*/
this.type = data.type;
}
if ('deny' in data) {
/**
* The permissions that are denied for the user or role.
* @type {Readonly<PermissionsBitField>}
*/
this.deny = new PermissionsBitField(BigInt(data.deny)).freeze();
}
if ('allow' in data) {
/**
* The permissions that are allowed for the user or role.
* @type {Readonly<PermissionsBitField>}
*/
this.allow = new PermissionsBitField(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: 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: this.type,
allow: this.allow,
deny: this.deny,
};
}
/**
* An object mapping permission flags to `true` (enabled), `null` (unset) or `false` (disabled).
* ```js
* {
* 'SendMessages': true,
* 'EmbedLinks': null,
* 'AttachFiles': false,
* }
* ```
* @typedef {Object} PermissionOverwriteOptions
*/
/**
* @typedef {Object} ResolvedOverwriteOptions
* @property {PermissionsBitField} allow The allowed permissions
* @property {PermissionsBitField} 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 PermissionsBitField(allow);
deny = new PermissionsBitField(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 OverwriteType) {
return {
id: overwrite.id,
type: overwrite.type,
allow: PermissionsBitField.resolve(overwrite.allow ?? PermissionsBitField.defaultBit).toString(),
deny: PermissionsBitField.resolve(overwrite.deny ?? PermissionsBitField.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 ? OverwriteType.Role : OverwriteType.Member;
return {
id: userOrRole.id,
type,
allow: PermissionsBitField.resolve(overwrite.allow ?? PermissionsBitField.defaultBit).toString(),
deny: PermissionsBitField.resolve(overwrite.deny ?? PermissionsBitField.defaultBit).toString(),
};
}
}
module.exports = PermissionOverwrites;
+396
View File
@@ -0,0 +1,396 @@
'use strict';
const Base = require('./Base');
const { Emoji } = require('./Emoji');
const ActivityFlagsBitField = require('../util/ActivityFlagsBitField');
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 {
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) {
if ('status' in data) {
/**
* The status of this presence
* @type {PresenceStatus}
*/
this.status = data.status;
} else {
this.status ??= 'offline';
}
if ('activities' in data) {
/**
* The activities of this presence
* @type {Activity[]}
*/
this.activities = data.activities.map(activity => new Activity(this, activity));
} else {
this.activities ??= [];
}
if ('client_status' in data) {
/**
* 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;
} else {
this.clientStatus ??= 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 = 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<ActivityFlagsBitField>}
*/
this.flags = new ActivityFlagsBitField(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 = Date.parse(data.created_at);
}
/**
* 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 {ImageURLOptions} [options={}] Options for the image URL
* @returns {?string}
*/
smallImageURL(options = {}) {
if (!this.smallImage) return null;
if (this.smallImage.includes(':')) {
const [platform, id] = this.smallImage.split(':');
switch (platform) {
case 'mp':
return `https://media.discordapp.net/${id}`;
default:
return null;
}
}
return this.activity.presence.client.rest.cdn.appAsset(this.activity.applicationId, this.smallImage, options);
}
/**
* Gets the URL of the large image asset
* @param {ImageURLOptions} [options={}] Options for the image URL
* @returns {?string}
*/
largeImageURL(options = {}) {
if (!this.largeImage) return null;
if (this.largeImage.includes(':')) {
const [platform, id] = this.largeImage.split(':');
switch (platform) {
case 'mp':
return `https://media.discordapp.net/${id}`;
case 'spotify':
return `https://i.scdn.co/image/${id}`;
case 'youtube':
return `https://i.ytimg.com/vi/${id}/hqdefault_live.jpg`;
case 'twitch':
return `https://static-cdn.jtvnw.net/previews-ttv/live_user_${id}.png`;
default:
return null;
}
}
return this.activity.presence.client.rest.cdn.appAsset(this.activity.applicationId, this.largeImage, options);
}
}
exports.Presence = Presence;
exports.Activity = Activity;
exports.RichPresenceAssets = RichPresenceAssets;
+229
View File
@@ -0,0 +1,229 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const Collector = require('./interfaces/Collector');
const Events = require('../util/Events');
/**
* @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 ({@link Client#event:messageDelete messageDelete} or
* {@link Client#event:messageDeleteBulk messageDeleteBulk}),
* channel ({@link Client#event:channelDelete channelDelete}),
* thread ({@link Client#event:threadDelete threadDelete}), or
* guild ({@link Client#event:guildDelete guildDelete}) is 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 that 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._handleThreadDeletion = this._handleThreadDeletion.bind(this);
this._handleGuildDeletion = this._handleGuildDeletion.bind(this);
this._handleMessageDeletion = this._handleMessageDeletion.bind(this);
const bulkDeleteListener = messages => {
if (messages.has(this.message.id)) this.stop('messageDelete');
};
this.client.incrementMaxListeners();
this.client.on(Events.MessageReactionAdd, this.handleCollect);
this.client.on(Events.MessageReactionRemove, this.handleDispose);
this.client.on(Events.MessageReactionRemoveAll, this.empty);
this.client.on(Events.MessageDelete, this._handleMessageDeletion);
this.client.on(Events.MessageBulkDelete, bulkDeleteListener);
this.client.on(Events.ChannelDelete, this._handleChannelDeletion);
this.client.on(Events.ThreadDelete, this._handleThreadDeletion);
this.client.on(Events.GuildDelete, this._handleGuildDeletion);
this.once('end', () => {
this.client.removeListener(Events.MessageReactionAdd, this.handleCollect);
this.client.removeListener(Events.MessageReactionRemove, this.handleDispose);
this.client.removeListener(Events.MessageReactionRemoveAll, this.empty);
this.client.removeListener(Events.MessageDelete, this._handleMessageDeletion);
this.client.removeListener(Events.MessageBulkDelete, bulkDeleteListener);
this.client.removeListener(Events.ChannelDelete, this._handleChannelDeletion);
this.client.removeListener(Events.ThreadDelete, this._handleThreadDeletion);
this.client.removeListener(Events.GuildDelete, this._handleGuildDeletion);
this.client.decrementMaxListeners();
});
this.on('collect', (reaction, user) => {
/**
* 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 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) {
this.emit('create', 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 {?(Snowflake|string)}
* @private
*/
collect(reaction) {
/**
* Emitted whenever a reaction is collected.
* @event ReactionCollector#collect
* @param {MessageReaction} reaction The reaction that was collected
* @param {User} user The user that added the reaction
*/
if (reaction.message.id !== this.message.id) return null;
return ReactionCollector.key(reaction);
}
/**
* Handles a reaction deletion for possible disposal.
* @param {MessageReaction} reaction The reaction to possibly dispose of
* @param {User} user The user that removed the reaction
* @returns {?(Snowflake|string)}
*/
dispose(reaction, user) {
/**
* Emitted when the reaction had all the users removed and the `dispose` option is set to true.
* @event ReactionCollector#dispose
* @param {MessageReaction} reaction The reaction that was disposed of
* @param {User} user The user that removed the reaction
*/
if (reaction.message.id !== this.message.id) return null;
/**
* Emitted when the reaction had one user removed and the `dispose` option is set to true.
* @event ReactionCollector#remove
* @param {MessageReaction} reaction The reaction that was removed
* @param {User} user The user that removed the reaction
*/
if (this.collected.has(ReactionCollector.key(reaction)) && this.users.has(user.id)) {
this.emit('remove', reaction, user);
}
return reaction.count ? null : ReactionCollector.key(reaction);
}
/**
* Empties this reaction collector.
*/
empty() {
this.total = 0;
this.collected.clear();
this.users.clear();
this.checkEnd();
}
/**
* 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 || channel.threads?.cache.has(this.message.channelId)) {
this.stop('channelDelete');
}
}
/**
* Handles checking if the thread has been deleted, and if so, stops the collector with the reason 'threadDelete'.
* @private
* @param {ThreadChannel} thread The thread that was deleted
* @returns {void}
*/
_handleThreadDeletion(thread) {
if (thread.id === this.message.channelId) {
this.stop('threadDelete');
}
}
/**
* 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
View File
@@ -0,0 +1,31 @@
'use strict';
const { Emoji } = require('./Emoji');
const Util = require('../util/Util');
/**
* Represents a limited emoji set used for both custom and unicode emojis. Custom emojis
* will use this class opposed to the Emoji class when the client doesn't know enough
* information about them.
* @extends {Emoji}
*/
class ReactionEmoji extends Emoji {
constructor(reaction, emoji) {
super(reaction.message.client, emoji);
/**
* The message reaction this emoji refers to
* @type {MessageReaction}
*/
this.reaction = reaction;
}
toJSON() {
return Util.flatten(this, { identifier: true });
}
valueOf() {
return this.id;
}
}
module.exports = ReactionEmoji;
+436
View File
@@ -0,0 +1,436 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { PermissionFlagsBits } = require('discord-api-types/v9');
const Base = require('./Base');
const { Error } = require('../errors');
const PermissionsBitField = require('../util/PermissionsBitField');
/**
* Represents a role on Discord.
* @extends {Base}
*/
class Role extends Base {
constructor(client, data, guild) {
super(client);
/**
* The guild that the role belongs to
* @type {Guild}
*/
this.guild = guild;
/**
* The icon hash of the role
* @type {?string}
*/
this.icon = null;
/**
* The unicode emoji for the role
* @type {?string}
*/
this.unicodeEmoji = null;
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;
if ('name' in data) {
/**
* The name of the role
* @type {string}
*/
this.name = data.name;
}
if ('color' in data) {
/**
* The base 10 color of the role
* @type {number}
*/
this.color = data.color;
}
if ('hoist' in data) {
/**
* 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;
}
if ('position' in data) {
/**
* The raw position of the role from the API
* @type {number}
*/
this.rawPosition = data.position;
}
if ('permissions' in data) {
/**
* The permissions of the role
* @type {Readonly<PermissionsBitField>}
*/
this.permissions = new PermissionsBitField(BigInt(data.permissions)).freeze();
}
if ('managed' in data) {
/**
* Whether or not the role is managed by an external service
* @type {boolean}
*/
this.managed = data.managed;
}
if ('mentionable' in data) {
/**
* Whether or not the role can be mentioned by anyone
* @type {boolean}
*/
this.mentionable = data.mentionable;
}
if ('icon' in data) this.icon = data.icon;
if ('unicode_emoji' in data) this.unicodeEmoji = data.unicode_emoji;
/**
* The tags this role has
* @type {?Object}
* @property {Snowflake} [botId] The id of the bot this role belongs to
* @property {Snowflake|string} [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 DiscordSnowflake.timestampFrom(this.id);
}
/**
* 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(PermissionFlagsBits.ManageRoles)) 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) {
return this.guild.roles.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
* @property {?(BufferResolvable|Base64Resolvable|EmojiResolvable)} [icon] The icon for the role
* <warn>The `EmojiResolvable` should belong to the same guild as the role.
* If not, pass the emoji's URL directly</warn>
* @property {?string} [unicodeEmoji] The unicode emoji for the role
*/
/**
* 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
* @param {boolean} [checkAdmin=true] Whether having `ADMINISTRATOR` will return all permissions
* @returns {Readonly<PermissionsBitField>}
*/
permissionsIn(channel, checkAdmin = true) {
channel = this.guild.channels.resolve(channel);
if (!channel) throw new Error('GUILD_CHANNEL_RESOLVE');
return channel.rolePermissions(this, checkAdmin);
}
/**
* 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=true] 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 = true, 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([PermissionFlagsBits.KickMembers, PermissionFlagsBits.BanMembers])
* .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=true] 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 = true, reason) {
return this.edit({ mentionable }, reason);
}
/**
* Sets a new icon for the role.
* @param {?(BufferResolvable|Base64Resolvable|EmojiResolvable)} icon The icon for the role
* <warn>The `EmojiResolvable` should belong to the same guild as the role.
* If not, pass the emoji's URL directly</warn>
* @param {string} [reason] Reason for changing the role's icon
* @returns {Promise<Role>}
*/
setIcon(icon, reason) {
return this.edit({ icon }, reason);
}
/**
* Sets a new unicode emoji for the role.
* @param {?string} unicodeEmoji The new unicode emoji for the role
* @param {string} [reason] Reason for changing the role's unicode emoji
* @returns {Promise<Role>}
* @example
* // Set a new unicode emoji for the role
* role.setUnicodeEmoji('🤖')
* .then(updated => console.log(`Set unicode emoji for the role to ${updated.unicodeEmoji}`))
* .catch(console.error);
*/
setUnicodeEmoji(unicodeEmoji, reason) {
return this.edit({ unicodeEmoji }, reason);
}
/**
* Options used to set the 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);
*/
setPosition(position, options = {}) {
return this.guild.roles.setPosition(this, position, options);
}
/**
* 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.guild.roles.delete(this.id, reason);
return this;
}
/**
* A link to the role's icon
* @param {ImageURLOptions} [options={}] Options for the image URL
* @returns {?string}
*/
iconURL(options = {}) {
return this.icon && this.client.rest.cdn.roleIcon(this.id, this.icon, options);
}
/**
* 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 &&
this.icon === role.icon &&
this.unicodeEmoji === role.unicodeEmoji
);
}
/**
* 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 }),
permissions: this.permissions.toJSON(),
};
}
}
exports.Role = Role;
/**
* @external APIRole
* @see {@link https://discord.com/developers/docs/topics/permissions#role-object}
*/
+12
View File
@@ -0,0 +1,12 @@
'use strict';
const { SelectMenuComponent: BuildersSelectMenuComponent } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
class SelectMenuComponent extends BuildersSelectMenuComponent {
constructor(data) {
super(Transformers.toSnakeCase(data));
}
}
module.exports = SelectMenuComponent;
+21
View File
@@ -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
View File
@@ -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 with 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;
+148
View File
@@ -0,0 +1,148 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const Base = require('./Base');
/**
* 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;
this._patch(data);
}
_patch(data) {
if ('guild_id' in data) {
/**
* The id of the guild associated with the stage channel
* @type {Snowflake}
*/
this.guildId = data.guild_id;
}
if ('channel_id' in data) {
/**
* The id of the channel associated with the stage channel
* @type {Snowflake}
*/
this.channelId = data.channel_id;
}
if ('topic' in data) {
/**
* The topic of the stage instance
* @type {string}
*/
this.topic = data.topic;
}
if ('privacy_level' in data) {
/**
* The privacy level of the stage instance
* @type {StageInstancePrivacyLevel}
*/
this.privacyLevel = data.privacy_level;
}
if ('discoverable_disabled' in data) {
/**
* Whether or not stage discovery is disabled
* @type {?boolean}
* @deprecated See https://github.com/discord/discord-api-docs/pull/4296 for more information
*/
this.discoverableDisabled = data.discoverable_disabled;
} else {
this.discoverableDisabled ??= 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();
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 DiscordSnowflake.timestampFrom(this.id);
}
/**
* The time this stage instance was created at
* @type {Date}
* @readonly
*/
get createdAt() {
return new Date(this.createdTimestamp);
}
}
exports.StageInstance = StageInstance;
+271
View File
@@ -0,0 +1,271 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { Routes, StickerFormatType } = require('discord-api-types/v9');
const Base = require('./Base');
/**
* Represents a Sticker.
* @extends {Base}
*/
class Sticker extends Base {
constructor(client, sticker) {
super(client);
this._patch(sticker);
}
_patch(sticker) {
/**
* The sticker's id
* @type {Snowflake}
*/
this.id = sticker.id;
if ('description' in sticker) {
/**
* The description of the sticker
* @type {?string}
*/
this.description = sticker.description;
} else {
this.description ??= null;
}
if ('type' in sticker) {
/**
* The type of the sticker
* @type {?StickerType}
*/
this.type = sticker.type;
} else {
this.type ??= null;
}
if ('format_type' in sticker) {
/**
* The format of the sticker
* @type {StickerFormatType}
*/
this.format = sticker.format_type;
}
if ('name' in sticker) {
/**
* The name of the sticker
* @type {string}
*/
this.name = sticker.name;
}
if ('pack_id' in sticker) {
/**
* The id of the pack the sticker is from, for standard stickers
* @type {?Snowflake}
*/
this.packId = sticker.pack_id;
} else {
this.packId ??= null;
}
if ('tags' in sticker) {
/**
* An array of tags for the sticker
* @type {?string[]}
*/
this.tags = sticker.tags.split(', ');
} else {
this.tags ??= null;
}
if ('available' in sticker) {
/**
* Whether or not the guild sticker is available
* @type {?boolean}
*/
this.available = sticker.available;
} else {
this.available ??= null;
}
if ('guild_id' in sticker) {
/**
* The id of the guild that owns this sticker
* @type {?Snowflake}
*/
this.guildId = sticker.guild_id;
} else {
this.guildId ??= null;
}
if ('user' in sticker) {
/**
* The user that uploaded the guild sticker
* @type {?User}
*/
this.user = this.client.users._add(sticker.user);
} else {
this.user ??= null;
}
if ('sort_value' in sticker) {
/**
* The standard sticker's sort order within its pack
* @type {?number}
*/
this.sortValue = sticker.sort_value;
} else {
this.sortValue ??= null;
}
}
/**
* The timestamp the sticker was created at
* @type {number}
* @readonly
*/
get createdTimestamp() {
return DiscordSnowflake.timestampFrom(this.id);
}
/**
* 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 {@link StickerFormatType.Lottie}, it returns
* the URL of the Lottie JSON file.</info>
* @type {string}
* @readonly
*/
get url() {
return this.client.rest.cdn.sticker(this.id, this.format === StickerFormatType.Lottie ? 'json' : 'png');
}
/**
* 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');
return this.guild.stickers.fetchUser(this);
}
/**
* 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(', ')
);
}
}
}
exports.Sticker = Sticker;
/**
* @external APISticker
* @see {@link https://discord.com/developers/docs/resources/sticker#sticker-object}
*/
+95
View File
@@ -0,0 +1,95 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { DiscordSnowflake } = require('@sapphire/snowflake');
const Base = require('./Base');
const { Sticker } = require('./Sticker');
/**
* Represents a pack of standard stickers.
* @extends {Base}
*/
class StickerPack extends Base {
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 ?? null;
}
/**
* The timestamp the sticker was created at
* @type {number}
* @readonly
*/
get createdTimestamp() {
return DiscordSnowflake.timestampFrom(this.id);
}
/**
* 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 {ImageURLOptions} [options={}] Options for the image URL
* @returns {?string}
*/
bannerURL(options = {}) {
return this.bannerId && this.client.rest.cdn.stickerPackBanner(this.bannerId, options);
}
}
module.exports = StickerPack;
+56
View File
@@ -0,0 +1,56 @@
'use strict';
const GuildChannel = require('./GuildChannel');
/**
* Represents a guild store channel on Discord.
* <warn>Store channels are deprecated and will be removed from Discord in March 2022. See
* [Self-serve Game Selling Deprecation](https://support-dev.discord.com/hc/en-us/articles/4414590563479)
* for more information.</warn>
* @extends {GuildChannel}
*/
class StoreChannel extends GuildChannel {
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;
+117
View File
@@ -0,0 +1,117 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { DiscordSnowflake } = require('@sapphire/snowflake');
const Base = require('./Base');
const TeamMember = require('./TeamMember');
/**
* 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;
if ('name' in data) {
/**
* The name of the Team
* @type {string}
*/
this.name = data.name;
}
if ('icon' in data) {
/**
* The Team's icon hash
* @type {?string}
*/
this.icon = data.icon;
} else {
this.icon ??= null;
}
if ('owner_user_id' in data) {
/**
* The Team's owner id
* @type {?Snowflake}
*/
this.ownerId = data.owner_user_id;
} else {
this.ownerId ??= 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 DiscordSnowflake.timestampFrom(this.id);
}
/**
* 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 {ImageURLOptions} [options={}] Options for the image URL
* @returns {?string}
*/
iconURL(options = {}) {
return this.icon && this.client.rest.cdn.teamIcon(this.id, this.icon, options);
}
/**
* 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;
+70
View File
@@ -0,0 +1,70 @@
'use strict';
const Base = require('./Base');
/**
* 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) {
if ('permissions' in data) {
/**
* The permissions this Team Member has with regard to the team
* @type {string[]}
*/
this.permissions = data.permissions;
}
if ('membership_state' in data) {
/**
* The permissions this Team Member has with regard to the team
* @type {TeamMemberMembershipState}
*/
this.membershipState = data.membership_state;
}
if ('user' in data) {
/**
* 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
View File
@@ -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 rate limit per user (slowmode) for this channel in seconds
* @type {number}
*/
this.rateLimitPerUser = data.rate_limit_per_user;
}
}
/**
* Sets the rate limit per user (slowmode) for this channel.
* @param {number} rateLimitPerUser The new rate limit in seconds
* @param {string} [reason] Reason for changing the channel's rate limit
* @returns {Promise<TextChannel>}
*/
setRateLimitPerUser(rateLimitPerUser, reason) {
return this.edit({ rateLimitPerUser }, reason);
}
}
module.exports = TextChannel;
+561
View File
@@ -0,0 +1,561 @@
'use strict';
const { ChannelType, PermissionFlagsBits, Routes } = require('discord-api-types/v9');
const { Channel } = require('./Channel');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const { RangeError } = require('../errors');
const MessageManager = require('../managers/MessageManager');
const ThreadMemberManager = require('../managers/ThreadMemberManager');
/**
* Represents a thread channel on Discord.
* @extends {Channel}
* @implements {TextBasedChannel}
*/
class ThreadChannel extends Channel {
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);
if ('name' in 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 {
this.parentId ??= null;
}
if ('thread_metadata' in data) {
/**
* Whether the thread is locked
* @type {?boolean}
*/
this.locked = data.thread_metadata.locked ?? false;
/**
* Whether members without `MANAGE_THREADS` can invite other members without `MANAGE_THREADS`
* <info>Always `null` in public threads</info>
* @type {?boolean}
*/
this.invitable = this.type === ChannelType.GuildPrivateThread ? data.thread_metadata.invitable ?? false : null;
/**
* 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 = Date.parse(data.thread_metadata.archive_timestamp);
if ('create_timestamp' in data.thread_metadata) {
// Note: this is needed because we can't assign directly to getters
this._createdTimestamp = Date.parse(data.thread_metadata.create_timestamp);
}
} else {
this.locked ??= null;
this.archived ??= null;
this.autoArchiveDuration ??= null;
this.archiveTimestamp ??= null;
this.invitable ??= null;
}
this._createdTimestamp ??= this.type === ChannelType.GuildPrivateThread ? super.createdTimestamp : null;
if ('owner_id' in data) {
/**
* The id of the member who created this thread
* @type {?Snowflake}
*/
this.ownerId = data.owner_id;
} else {
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 {
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 ? Date.parse(data.last_pin_timestamp) : null;
} else {
this.lastPinTimestamp ??= null;
}
if ('rate_limit_per_user' in data || !partial) {
/**
* The rate limit per user (slowmode) for this thread in seconds
* @type {?number}
*/
this.rateLimitPerUser = data.rate_limit_per_user ?? 0;
} else {
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 {
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 {
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);
}
/**
* The timestamp when this thread was created. This isn't available for threads
* created before 2022-01-09
* @type {?number}
* @readonly
*/
get createdTimestamp() {
return this._createdTimestamp;
}
/**
* 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() {
return this.archiveTimestamp && new Date(this.archiveTimestamp);
}
/**
* The time the thread was created at
* @type {?Date}
* @readonly
*/
get createdAt() {
return this.createdTimestamp && new Date(this.createdTimestamp);
}
/**
* 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
* @param {boolean} [checkAdmin=true] Whether having `ADMINISTRATOR` will return all permissions
* @returns {?Readonly<PermissionsBitField>}
*/
permissionsFor(memberOrRole, checkAdmin) {
return this.parent?.permissionsFor(memberOrRole, checkAdmin) ?? null;
}
/**
* Fetches the owner of this thread. If the thread member object isn't needed,
* use {@link ThreadChannel#ownerId} instead.
* @param {BaseFetchOptions} [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;
}
/**
* Fetches the message that started this thread, if any.
* <info>This only works when the thread started from a message in the parent channel, otherwise the promise will
* reject. If you just need the id of that message, use {@link ThreadChannel#id} instead.</info>
* @param {BaseFetchOptions} [options] Additional options for this fetch
* @returns {Promise<Message>}
*/
fetchStarterMessage(options) {
return this.parent.messages.fetch(this.id, options);
}
/**
* 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 rate limit per user (slowmode) for the thread in seconds
* @property {boolean} [locked] Whether the thread is locked
* @property {boolean} [invitable] Whether non-moderators can add other non-moderators to a thread
* <info>Can only be edited on {@link ChannelType.GuildPrivateThread}</info>
*/
/**
* 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({
body: {
name: (data.name ?? this.name).trim(),
archived: data.archived,
auto_archive_duration: autoArchiveDuration,
rate_limit_per_user: data.rateLimitPerUser,
locked: data.locked,
invitable: this.type === ChannelType.GuildPrivateThread ? data.invitable : undefined,
},
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 members without the `MANAGE_THREADS` permission can invite other members without the
* `MANAGE_THREADS` permission to this thread.
* @param {boolean} [invitable=true] Whether non-moderators can invite non-moderators to this thread
* @param {string} [reason] Reason for changing invite
* @returns {Promise<ThreadChannel>}
*/
setInvitable(invitable = true, reason) {
if (this.type !== ChannelType.GuildPrivateThread) {
return Promise.reject(new RangeError('THREAD_INVITABLE_TYPE', this.type));
}
return this.edit({ invitable }, 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 (slowmode) for this thread.
* @param {number} rateLimitPerUser The new rate limit in seconds
* @param {string} [reason] Reason for changing the thread's rate limit
* @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 !== ChannelType.GuildPrivateThread || 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 === ChannelType.GuildPrivateThread
? PermissionFlagsBits.ManageThreads
: PermissionFlagsBits.ViewChannel,
false,
)
);
}
/**
* Whether the thread is manageable by the client user, for deleting or editing rateLimitPerUser or locked.
* @type {boolean}
* @readonly
*/
get manageable() {
const permissions = this.permissionsFor(this.client.user);
if (!permissions) return false;
// This flag allows managing even if timed out
if (permissions.has(PermissionFlagsBits.Administrator, false)) return true;
return (
this.guild.me.communicationDisabledUntilTimestamp < Date.now() &&
permissions.has(PermissionFlagsBits.ManageThreads, false)
);
}
/**
* Whether the thread 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(PermissionFlagsBits.ViewChannel, false);
}
/**
* Whether the client user can send messages in this thread
* @type {boolean}
* @readonly
*/
get sendable() {
const permissions = this.permissionsFor(this.client.user);
if (!permissions) return false;
// This flag allows sending even if timed out
if (permissions.has(PermissionFlagsBits.Administrator, false)) return true;
return (
!(this.archived && this.locked && !this.manageable) &&
(this.type !== ChannelType.GuildPrivateThread || this.joined || this.manageable) &&
permissions.has(PermissionFlagsBits.SendMessagesInThreads, false) &&
this.guild.me.communicationDisabledUntilTimestamp < Date.now()
);
}
/**
* Whether the thread is unarchivable by the client user
* @type {boolean}
* @readonly
*/
get unarchivable() {
return this.archived && this.sendable && (!this.locked || this.manageable);
}
/**
* Whether this thread is a private thread
* @returns {boolean}
*/
isPrivate() {
return this.type === ChannelType.GuildPrivateThread;
}
/**
* 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.guild.channels.delete(this.id, 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;
+94
View File
@@ -0,0 +1,94 @@
'use strict';
const Base = require('./Base');
const ThreadMemberFlagsBitField = require('../util/ThreadMemberFlagsBitField');
/**
* Represents a Member for a Thread.
* @extends {Base}
*/
class ThreadMember extends Base {
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) {
if ('join_timestamp' in data) this.joinedTimestamp = Date.parse(data.join_timestamp);
if ('flags' in data) {
/**
* The flags for this thread member
* @type {ThreadMemberFlagsBitField}
*/
this.flags = new ThreadMemberFlagsBitField(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);
}
/**
* 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;
+74
View File
@@ -0,0 +1,74 @@
'use strict';
const Base = require('./Base');
/**
* Represents a typing state for a user in a channel.
* @extends {Base}
*/
class Typing extends Base {
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) {
if ('timestamp' in data) {
/**
* The UNIX timestamp in milliseconds the user started typing at
* @type {number}
*/
this.startedTimestamp = data.timestamp * 1_000;
}
}
/**
* 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;
+420
View File
@@ -0,0 +1,420 @@
'use strict';
const Base = require('./Base');
const { Error } = require('../errors/DJSError');
const { DiscordSnowflake } = require('@sapphire/snowflake');
const UserFlagsBitField = require('../util/UserFlagsBitField');
const { default: Collection } = require('@discordjs/collection');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
/**
* Represents a user on Discord.
* @implements {TextBasedChannel}
* @extends {Base}
*/
class User extends Base {
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.friend = client.friends.cache.has(this.id);
this.blocked = client.blocked.cache.has(this.id);
// Code written by https://github.com/aiko-chan-ai
this.connectedAccounds = [];
this.premiumSince = null;
this.premiumGuildSince = null;
this.mutualGuilds = new Collection();
this._patch(data);
}
_patch(data) {
if ('username' in data) {
/**
* The username of the user
* @type {?string}
*/
this.username = data.username;
} else {
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 {
this.discriminator ??= null;
}
if ('avatar' in data) {
/**
* The user avatar's hash
* @type {?string}
*/
this.avatar = data.avatar;
} else {
this.avatar ??= null;
}
if ('banner' in data) {
/**
* The user banner's hash
* <info>The user must be force fetched for this property to be present or be updated</info>
* @type {?string}
*/
this.banner = data.banner;
} else if (this.banner !== null) {
this.banner ??= undefined;
}
if ('accent_color' in data) {
/**
* The base 10 accent color of the user's banner
* <info>The user must be force fetched for this property to be present or be updated</info>
* @type {?number}
*/
this.accentColor = data.accent_color;
} else if (this.accentColor !== null) {
this.accentColor ??= undefined;
}
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 {?UserFlagsBitField}
*/
this.flags = new UserFlagsBitField(data.public_flags);
}
}
// Code written by https://github.com/aiko-chan-ai
_ProfilePatch(data) {
if(!data) return;
if(data.connected_accounts.length > 0) this.connectedAccounds = data.connected_accounts;
if('premium_since' in data) {
const date = new Date(data.premium_since);
this.premiumSince = date.getTime();
}
if('premium_guild_since' in data) {
const date = new Date(data.premium_guild_since);
this.premiumGuildSince = date.getTime();
}
this.mutualGuilds = new Collection(data.mutual_guilds.map((obj) => [obj.id, obj]));
}
/**
* Get profile from Discord, if client is in a server with the target.
* <br>Code written by https://github.com/aiko-chan-ai
*/
async getProfile() {
if(this.client.bot) throw new Error('INVALID_BOT_METHOD');
try {
const data = await this.client.api.users(this.id).profile.get();
this._ProfilePatch(data);
return this
} catch (e) {
throw e
}
}
/**
* Friends the user
* @returns {Promise<User>} the user object
*/
async friend() {
return this.client.api
.user('@me')
.relationships[this.id].put({data:{type:1}})
.then(_ => _)
}
/**
* Blocks the user
* @returns {Promise<User>} the user object
*/
async block() {
return this.client.api
.users('@me')
.relationships[this.id].put({data:{type: 2}})
.then(_ => _)
}
/**
* Removes the user from your blocks list
* @returns {Promise<User>} the user object
*/
async unblock() {
return this.client.api
.users('@me')
.relationships[this.id].delete
.then(_ => _)
}
/**
* Removes the user from your friends list
* @returns {Promise<User>} the user object
*/
async unfriend() {
return this.client.api
.users('@me')
.relationships[this.id].delete
.then(_ => _)
}
/**
* 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 DiscordSnowflake.timestampFrom(this.id);
}
/**
* 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(options = {}) {
return this.avatar && this.client.rest.cdn.avatar(this.id, this.avatar, options);
}
/**
* If the user is a bot then it'll return the slash commands else return null
* @readonly
*/
get slashCommands() {
if(this.bot) {
return this.client.api.applications(this.id).commands.get();
} else return null;
}
/**
* 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 hexadecimal version of the user accent color, with a leading hash
* <info>The user must be force fetched for this property to be present</info>
* @type {?string}
* @readonly
*/
get hexAccentColor() {
if (typeof this.accentColor !== 'number') return this.accentColor;
return `#${this.accentColor.toString(16).padStart(6, '0')}`;
}
/**
* A link to the user's banner. See {@link User#banner} for more info
* @param {ImageURLOptions} [options={}] Options for the image URL
* @returns {?string}
*/
bannerURL(options = {}) {
return this.banner && this.client.rest.cdn.banner(this.id, this.banner, options);
}
/**
* 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.users.dmChannel(this.id);
}
/**
* 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>}
*/
createDM(force = false) {
return this.client.users.createDM(this.id, force);
}
/**
* Deletes a DM channel (if one exists) between the client and the user. Resolves with the channel if successful.
* @returns {Promise<DMChannel>}
*/
deleteDM() {
return this.client.users.deleteDM(this.id);
}
/**
* Checks if the user is equal to another.
* It compares id, username, discriminator, avatar, banner, accent color, 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) {
return (
user &&
this.id === user.id &&
this.username === user.username &&
this.discriminator === user.discriminator &&
this.avatar === user.avatar &&
this.flags?.bitfield === user.flags?.bitfield &&
this.banner === user.banner &&
this.accentColor === user.accentColor
);
}
/**
* Compares the user with an API user object
* @param {APIUser} user The API user object to compare
* @returns {boolean}
* @private
*/
_equals(user) {
return (
user &&
this.id === user.id &&
this.username === user.username &&
this.discriminator === user.discriminator &&
this.avatar === user.avatar &&
this.flags?.bitfield === user.public_flags &&
('banner' in user ? this.banner === user.banner : true) &&
('accent_color' in user ? this.accentColor === user.accent_color : true)
);
}
/**
* Fetches this user's flags.
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<UserFlagsBitField>}
*/
fetchFlags(force = false) {
return this.client.users.fetchFlags(this.id, { force });
}
/**
* 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,
hexAccentColor: true,
tag: true,
},
...props,
);
json.avatarURL = this.avatarURL();
json.displayAvatarURL = this.displayAvatarURL();
json.bannerURL = this.banner ? this.bannerURL() : this.banner;
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}
*/
@@ -0,0 +1,29 @@
'use strict';
const ContextMenuCommandInteraction = require('./ContextMenuCommandInteraction');
/**
* Represents a user context menu interaction.
* @extends {ContextMenuCommandInteraction}
*/
class UserContextMenuCommandInteraction extends ContextMenuCommandInteraction {
/**
* The user this interaction was sent from
* @type {User}
* @readonly
*/
get targetUser() {
return this.options.getUser('user');
}
/**
* The member this interaction was sent from
* @type {?(GuildMember|APIGuildMember)}
* @readonly
*/
get targetMember() {
return this.options.getMember('user');
}
}
module.exports = UserContextMenuCommandInteraction;
+83
View File
@@ -0,0 +1,83 @@
'use strict';
const { PermissionFlagsBits } = require('discord-api-types/v9');
const BaseGuildVoiceChannel = require('./BaseGuildVoiceChannel');
/**
* Represents a guild voice channel on Discord.
* @extends {BaseGuildVoiceChannel}
*/
class VoiceChannel extends BaseGuildVoiceChannel {
/**
* 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(PermissionFlagsBits.MoveMembers, false)) return false;
return true;
}
/**
* Checks if the client has permission to send audio to the voice channel
* @type {boolean}
* @readonly
*/
get speakable() {
const permissions = this.permissionsFor(this.client.user);
if (!permissions) return false;
// This flag allows speaking even if timed out
if (permissions.has(PermissionFlagsBits.Administrator, false)) return true;
return (
this.guild.me.communicationDisabledUntilTimestamp < Date.now() &&
permissions.has(PermissionFlagsBits.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(48_000)
* .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;
+46
View File
@@ -0,0 +1,46 @@
'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 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;
+278
View File
@@ -0,0 +1,278 @@
'use strict';
const { ChannelType, Routes } = require('discord-api-types/v9');
const Base = require('./Base');
const { Error, TypeError } = require('../errors');
/**
* Represents the voice state for a Guild Member.
*/
class VoiceState extends Base {
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) {
if ('deaf' in data) {
/**
* Whether this member is deafened server-wide
* @type {?boolean}
*/
this.serverDeaf = data.deaf;
} else {
this.serverDeaf ??= null;
}
if ('mute' in data) {
/**
* Whether this member is muted server-wide
* @type {?boolean}
*/
this.serverMute = data.mute;
} else {
this.serverMute ??= null;
}
if ('self_deaf' in data) {
/**
* Whether this member is self-deafened
* @type {?boolean}
*/
this.selfDeaf = data.self_deaf;
} else {
this.selfDeaf ??= null;
}
if ('self_mute' in data) {
/**
* Whether this member is self-muted
* @type {?boolean}
*/
this.selfMute = data.self_mute;
} else {
this.selfMute ??= null;
}
if ('self_video' in data) {
/**
* Whether this member's camera is enabled
* @type {?boolean}
*/
this.selfVideo = data.self_video;
} else {
this.selfVideo ??= null;
}
if ('session_id' in data) {
/**
* The session id for this member's connection
* @type {?string}
*/
this.sessionId = data.session_id;
} else {
this.sessionId ??= null;
}
// The self_stream is property is omitted if false, check for another property
// here to avoid incorrectly clearing this when partial data is specified
if ('self_video' in data) {
/**
* Whether this member is streaming using "Screen Share"
* @type {?boolean}
*/
this.streaming = data.self_stream ?? false;
} else {
this.streaming ??= null;
}
if ('channel_id' in data) {
/**
* The {@link VoiceChannel} or {@link StageChannel} id the member is in
* @type {?Snowflake}
*/
this.channelId = data.channel_id;
} else {
this.channelId ??= null;
}
if ('suppress' in data) {
/**
* Whether this member is suppressed from speaking. This property is specific to stage channels only.
* @type {?boolean}
*/
this.suppress = data.suppress;
} else {
this.suppress ??= null;
}
if ('request_to_speak_timestamp' in data) {
/**
* The time at which the member requested to speak. This property is specific to stage channels only.
* @type {?number}
*/
this.requestToSpeakTimestamp = Date.parse(data.request_to_speak_timestamp);
} else {
this.requestToSpeakTimestamp ??= 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=true] Whether or not the member should be muted
* @param {string} [reason] Reason for muting or unmuting
* @returns {Promise<GuildMember>}
*/
setMute(mute = true, reason) {
return this.guild.members.edit(this.id, { mute }, reason);
}
/**
* Deafens/undeafens the member of this voice state.
* @param {boolean} [deaf=true] Whether or not the member should be deafened
* @param {string} [reason] Reason for deafening or undeafening
* @returns {Promise<GuildMember>}
*/
setDeaf(deaf = true, reason) {
return this.guild.members.edit(this.id, { deaf }, reason);
}
/**
* 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 {GuildVoiceChannelResolvable|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.guild.members.edit(this.id, { channel }, reason);
}
/**
* Toggles the request to speak in the channel.
* Only applicable for stage channels and for the client's own voice state.
* @param {boolean} [request=true] 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 = true) {
if (this.channel?.type !== ChannelType.GuildStageVoice) 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({
body: {
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=true] 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 = true) {
if (typeof suppressed !== 'boolean') throw new TypeError('VOICE_STATE_INVALID_TYPE', 'suppressed');
if (this.channel?.type !== ChannelType.GuildStageVoice) 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({
body: {
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;
+449
View File
@@ -0,0 +1,449 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { Routes, WebhookType } = require('discord-api-types/v9');
const MessagePayload = require('./MessagePayload');
const { Error } = require('../errors');
const DataResolver = require('../util/DataResolver');
/**
* 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) {
if ('name' in 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 });
if ('avatar' in data) {
/**
* The avatar for the webhook
* @type {?string}
*/
this.avatar = data.avatar;
}
/**
* The webhook's id
* @type {Snowflake}
*/
this.id = data.id;
if ('type' in data) {
/**
* The type of the webhook
* @type {WebhookType}
*/
this.type = data.type;
}
if ('guild_id' in data) {
/**
* The guild the webhook belongs to
* @type {Snowflake}
*/
this.guildId = data.guild_id;
}
if ('channel_id' in data) {
/**
* The channel the webhook belongs to
* @type {Snowflake}
*/
this.channelId = data.channel_id;
}
if ('user' in data) {
/**
* The owner of the webhook
* @type {?(User|APIUser)}
*/
this.owner = this.client.users?._add(data.user) ?? data.user;
} else {
this.owner ??= null;
}
if ('application_id' in data) {
/**
* The application that created this webhook
* @type {?Snowflake}
*/
this.applicationId = data.application_id;
} else {
this.applicationId ??= null;
}
if ('source_guild' in data) {
/**
* The source guild of the webhook
* @type {?(Guild|APIGuild)}
*/
this.sourceGuild = this.client.guilds?.resolve(data.source_guild.id) ?? data.source_guild;
} else {
this.sourceGuild ??= null;
}
if ('source_channel' in data) {
/**
* The source channel of the webhook
* @type {?(NewsChannel|APIChannel)}
*/
this.sourceChannel = this.client.channels?.resolve(data.source_channel?.id) ?? data.source_channel;
} else {
this.sourceChannel ??= 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>
* @property {MessageFlags} [flags] Which flags to set for the message. Only `SUPPRESS_EMBEDS` can be set.
*/
/**
* Options that can be passed into editMessage.
* @typedef {Object} WebhookEditMessageOptions
* @property {Embed[]|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 {MessageAttachment[]} [attachments] Attachments to send with the message
* @property {ActionRow[]|ActionRowOptions[]} [components]
* Action rows containing interactive components for the message (buttons, select menus)
* @property {Snowflake} [threadId] The id of the thread this message belongs to
* <info>For interaction webhooks, this property is ignored</info>
*/
/**
* 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.resolveBody();
} else {
messagePayload = MessagePayload.create(this, options).resolveBody();
}
const query = new URLSearchParams({ wait: true });
if (messagePayload.options.threadId) {
query.set('thread_id', messagePayload.options.threadId);
}
const { body, files } = await messagePayload.resolveFiles();
const d = await this.client.api.webhooks(this.id, this.token).post({ body, files, query, 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() / 1_000
* }]
* }).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: new URLSearchParams({ wait: true }),
auth: false,
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);
}
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,
auth: !this.token || Boolean(channel),
});
this.name = data.name;
this.avatar = data.avatar;
this.channelId = data.channel_id;
return this;
}
/**
* Options that can be passed into fetchMessage.
* @typedef {options} WebhookFetchMessageOptions
* @property {boolean} [cache=true] Whether to cache the message.
* @property {Snowflake} [threadId] The id of the thread this message belongs to.
* <info>For interaction webhooks, this property is ignored</info>
*/
/**
* Gets a message that was sent by this webhook.
* @param {Snowflake|'@original'} message The id of the message to fetch
* @param {WebhookFetchMessageOptions} [options={}] The options to provide to fetch 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, threadId } = {}) {
if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE');
const data = await this.client.api.webhooks(this.id, this.token).messages(message).get({
query: threadId
? new URLSearchParams({
thread_id: threadId,
})
: undefined,
auth: false
});
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 { body, files } = await messagePayload.resolveBody().resolveFiles();
const d = await this.client.api.webhooks(this.id, this.token).messages(typeof message === 'string' ? message : message.id).patch({
body, files, query: messagePayload.options.threadId ? new URLSearchParams({ thread_id: messagePayload.options.threadId }) : undefined, auth: false
});
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, auth: !this.token });
}
/**
* Delete a message that was sent by this webhook.
* @param {MessageResolvable|'@original'} message The message to delete
* @param {Snowflake} [threadId] The id of the thread this message belongs to
* @returns {Promise<void>}
*/
async deleteMessage(message, threadId) {
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({
query: threadId
? new URLSearchParams({
thread_id: threadId,
})
: undefined,
auth: false,
})
}
/**
* The timestamp the webhook was created at
* @type {number}
* @readonly
*/
get createdTimestamp() {
return DiscordSnowflake.timestampFrom(this.id);
}
/**
* 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.rest.api + Routes.webhook(this.id, this.token);
}
/**
* A link to the webhook's avatar.
* @param {ImageURLOptions} [options={}] Options for the image URL
* @returns {?string}
*/
avatarURL(options = {}) {
return this.avatar && this.client.rest.cdn.avatar(this.id, this.avatar, options);
}
/**
* Whether this webhook is created by a user.
* @returns {boolean}
*/
isUserCreated() {
return Boolean(this.type === WebhookType.Incoming && this.owner && !this.owner.bot);
}
/**
* Whether this webhook is created by an application.
* @returns {boolean}
*/
isApplicationCreated() {
return this.type === WebhookType.Application;
}
/**
* Whether or not this webhook is a channel follower webhook.
* @returns {boolean}
*/
isChannelFollower() {
return this.type === WebhookType.ChannelFollower;
}
/**
* Whether or not this webhook is an incoming webhook.
* @returns {boolean}
*/
isIncoming() {
return this.type === WebhookType.Incoming;
}
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
View File
@@ -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|StoreChannel)}
*/
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
View File
@@ -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;
+87
View File
@@ -0,0 +1,87 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v9');
const Base = require('./Base');
const WidgetMember = require('./WidgetMember');
/**
* Represents a Widget.
*/
class Widget extends Base {
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
*/
_patch(data) {
/**
* The id of the guild.
* @type {Snowflake}
*/
this.id = data.id;
if ('name' in data) {
/**
* The name of the guild.
* @type {string}
*/
this.name = data.name;
}
if ('instant_invite' in data) {
/**
* 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));
}
if ('presence_count' in data) {
/**
* The number of 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;
+98
View File
@@ -0,0 +1,98 @@
'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
*/
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;
/**
* If 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;
+109
View File
@@ -0,0 +1,109 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const Base = require('../Base');
/**
* Represents an OAuth2 Application.
* @abstract
*/
class Application extends Base {
constructor(client, data) {
super(client);
this._patch(data);
}
_patch(data) {
if(!data) return;
/**
* The application's id
* @type {Snowflake}
*/
this.id = data.id;
if ('name' in data) {
/**
* The name of the application
* @type {?string}
*/
this.name = data.name;
} else {
this.name ??= null;
}
if ('description' in data) {
/**
* The application's description
* @type {?string}
*/
this.description = data.description;
} else {
this.description ??= null;
}
if ('icon' in data) {
/**
* The application's icon hash
* @type {?string}
*/
this.icon = data.icon;
} else {
this.icon ??= null;
}
}
/**
* The timestamp the application was created at
* @type {number}
* @readonly
*/
get createdTimestamp() {
return DiscordSnowflake.timestampFrom(this.id);
}
/**
* 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 {ImageURLOptions} [options={}] Options for the image URL
* @returns {?string}
*/
iconURL(options = {}) {
return this.icon && this.client.rest.cdn.appIcon(this.id, this.icon, options);
}
/**
* A link to this application's cover image.
* @param {ImageURLOptions} [options={}] Options for the image URL
* @returns {?string}
*/
coverURL(options = {}) {
return this.cover && this.client.rest.cdn.appIcon(this.id, this.cover, options);
}
/**
* When concatenated with a string, this automatically returns the application's name instead of the
* Application 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;
+299
View File
@@ -0,0 +1,299 @@
'use strict';
const EventEmitter = require('node:events');
const { setTimeout, clearTimeout } = require('node:timers');
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 the 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 resetting
*/
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;
@@ -0,0 +1,251 @@
'use strict';
const { InteractionResponseType, MessageFlags, Routes } = require('discord-api-types/v9');
const { Error } = require('../../errors');
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 MessageComponentInteraction}.
* @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
* @property {MessageFlags} [flags] Which flags to set for the message.
* Only `MessageFlags.SuppressEmbeds` and `MessageFlags.Ephemeral` can be set.
*/
/**
* Options for updating the message received from a {@link MessageComponentInteraction}.
* @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');
this.ephemeral = options.ephemeral ?? false;
await this.client.api.interactions(this.id, this.token).callback.post({
body: {
type: InteractionResponseType.DeferredChannelMessageWithSource,
data: {
flags: options.ephemeral ? MessageFlags.Ephemeral : undefined,
},
},
auth: false,
})
this.deferred = true;
return options.fetchReply ? this.fetchReply() : undefined;
}
/**
* Creates a reply to this interaction.
* <info>Use the `fetchReply` option to get the bot's reply message.</info>
* @param {string|MessagePayload|InteractionReplyOptions} options The options for the reply
* @returns {Promise<Message|APIMessage|void>}
* @example
* // Reply to the interaction and fetch the response
* interaction.reply({ content: 'Pong!', fetchReply: true })
* .then((message) => console.log(`Reply sent with content ${message.content}`))
* .catch(console.error);
* @example
* // Create an ephemeral reply with an embed
* const embed = new Embed().setDescription('Pong!');
*
* interaction.reply({ embeds: [embed], ephemeral: true })
* .then(() => console.log('Reply sent.'))
* .catch(console.error);
*/
async reply(options) {
if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED');
this.ephemeral = options.ephemeral ?? false;
let messagePayload;
if (options instanceof MessagePayload) messagePayload = options;
else messagePayload = MessagePayload.create(this, options);
const { body: data, files } = await messagePayload.resolveBody().resolveFiles();
await this.client.api.interactions(this.id, this.token).callback.post({
body: {
type: InteractionResponseType.ChannelMessageWithSource,
data,
},
files,
auth: false,
});
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() {
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) {
if (!this.deferred && !this.replied) return Promise.reject(new Error('INTERACTION_NOT_REPLIED'));
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');
await this.client.api.interactions(this.id, this.token).callback.post({
body: {
type: InteractionResponseType.DeferredMessageUpdate,
},
auth: false,
});
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');
let messagePayload;
if (options instanceof MessagePayload) messagePayload = options;
else messagePayload = MessagePayload.create(this, options);
const { body: data, files } = await messagePayload.resolveBody().resolveFiles();
await this.client.api.interactions(this.id, this.token).callback.post({
body: {
type: InteractionResponseType.UpdateMessage,
data,
},
files,
auth: false,
});
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;
@@ -0,0 +1,363 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { InteractionType, Routes } = require('discord-api-types/v9');
const { TypeError, Error } = require('../../errors');
const InteractionCollector = require('../InteractionCollector');
const MessageCollector = require('../MessageCollector');
const MessagePayload = require('../MessagePayload');
/**
* 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);
}
/**
* 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 {Embed[]|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 {ActionRow[]|ActionRowOptions[]} [components]
* Action rows containing interactive components for the message (buttons, select menus)
* @property {MessageAttachment[]} [attachments] Attachments 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
* @property {StickerResolvable[]} [stickers=[]] Stickers to send in the message
* @property {MessageFlags} [flags] Which flags to set for the message. Only `MessageFlags.SuppressEmbeds` can be set.
*/
/**
* 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
* @property {string} description The description of the file
*/
/**
* Options for sending a message with a reply.
* @typedef {Object} ReplyOptions
* @property {MessageResolvable} messageReference The message to reply to (must be in the same channel and not system)
* @property {boolean} [failIfNotExists=this.client.options.failIfNotExists] 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',
* description: 'A description of the file'
* }]
* })
* .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',
* description: 'A description of the file'
* }]
* })
* .then(console.log)
* .catch(console.error);
*/
async send(options) {
await this.client.api.channels(this.id).typing.post();
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.resolveBody();
} else {
messagePayload = MessagePayload.create(this, options).resolveBody();
}
const { body, files } = await messagePayload.resolveFiles();
const d = await this.client.api.channels[this.id].messages.post({ body, files });
await this.client.api.channels(this.id).typing.delete();
return this.messages.cache.get(d.id) ?? 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: 15_000 });
* 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: 60_000, 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 component 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: 15_000 });
* 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: InteractionType.MessageComponent,
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: 15_000 })
* .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() - DiscordSnowflake.timestampFrom(id) < 1_209_600_000);
}
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({ body: { 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
// eslint-disable-next-line import/order
const MessageManager = require('../../managers/MessageManager');