Downgrade to v13

[vi] cảm giác đau khổ
This commit is contained in:
March 7th
2022-03-24 17:55:32 +07:00
parent 9596b1a210
commit 7dfdef46a5
218 changed files with 8584 additions and 9108 deletions

View File

@@ -1,12 +0,0 @@
'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;

View File

@@ -1,6 +1,7 @@
'use strict';
const BaseGuild = require('./BaseGuild');
const { VerificationLevels, NSFWLevels } = require('../util/Constants');
/**
* Bundles common attributes and methods between {@link Guild} and {@link InviteGuild}
@@ -43,9 +44,9 @@ class AnonymousGuild extends BaseGuild {
if ('verification_level' in data) {
/**
* The verification level of the guild
* @type {GuildVerificationLevel}
* @type {VerificationLevel}
*/
this.verificationLevel = data.verification_level;
this.verificationLevel = VerificationLevels[data.verification_level];
}
if ('vanity_url_code' in data) {
@@ -59,36 +60,28 @@ class AnonymousGuild extends BaseGuild {
if ('nsfw_level' in data) {
/**
* The NSFW level of this guild
* @type {GuildNSFWLevel}
* @type {NSFWLevel}
*/
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;
this.nsfwLevel = NSFWLevels[data.nsfw_level];
}
}
/**
* The URL to this guild's banner.
* @param {ImageURLOptions} [options={}] Options for the image URL
* @param {StaticImageURLOptions} [options={}] Options for the Image URL
* @returns {?string}
*/
bannerURL(options = {}) {
return this.banner && this.client.rest.cdn.banner(this.id, this.banner, options);
bannerURL({ format, size } = {}) {
return this.banner && this.client.rest.cdn.Banner(this.id, this.banner, format, size);
}
/**
* The URL to this guild's invite splash image.
* @param {ImageURLOptions} [options={}] Options for the image URL
* @param {StaticImageURLOptions} [options={}] Options for the Image URL
* @returns {?string}
*/
splashURL(options = {}) {
return this.splash && this.client.rest.cdn.splash(this.id, this.splash, options);
splashURL({ format, size } = {}) {
return this.splash && this.client.rest.cdn.Splash(this.id, this.splash, format, size);
}
}

View File

@@ -1,9 +1,9 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { ApplicationCommandOptionType } = require('discord-api-types/v9');
const Base = require('./Base');
const ApplicationCommandPermissionsManager = require('../managers/ApplicationCommandPermissionsManager');
const { ApplicationCommandOptionTypes, ApplicationCommandTypes, ChannelTypes } = require('../util/Constants');
const SnowflakeUtil = require('../util/SnowflakeUtil');
/**
* Represents an application command.
@@ -48,7 +48,7 @@ class ApplicationCommand extends Base {
* The type of this application command
* @type {ApplicationCommandType}
*/
this.type = data.type;
this.type = ApplicationCommandTypes[data.type];
this._patch(data);
}
@@ -103,7 +103,7 @@ class ApplicationCommand extends Base {
* @readonly
*/
get createdTimestamp() {
return DiscordSnowflake.timestampFrom(this.id);
return SnowflakeUtil.timestampFrom(this.id);
}
/**
@@ -127,13 +127,11 @@ class ApplicationCommand extends Base {
/**
* 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 {string} name The name of the command
* @property {string} description The description of the command
* @property {ApplicationCommandType} [type] The type of the command
* @property {ApplicationCommandOptionData[]} [options] Options for the command
* @property {boolean} [defaultPermission=true] Whether the command is enabled by default when the app is added to a
* guild
* @property {boolean} [defaultPermission] Whether the command is enabled by default when the app is added to a guild
*/
/**
@@ -143,21 +141,17 @@ class ApplicationCommand extends Base {
* <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 {ApplicationCommandOptionType|number} type The type of the option
* @property {string} name The name of the option
* @property {string} description The description of the option
* @property {boolean} [autocomplete] Whether the autocomplete interaction is enabled for a
* {@link ApplicationCommandOptionType.String}, {@link ApplicationCommandOptionType.Integer} or
* {@link ApplicationCommandOptionType.Number} option
* @property {boolean} [autocomplete] Whether the option is an autocomplete 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,
* @property {ChannelType[]|number[]} [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
* @property {number} [minValue] The minimum value for an `INTEGER` or `NUMBER` option
* @property {number} [maxValue] The maximum value for an `INTEGER` or `NUMBER` option
*/
/**
@@ -239,12 +233,13 @@ class ApplicationCommand extends Base {
if (command.id && this.id !== command.id) return false;
// Check top level parameters
const commandType = typeof command.type === 'string' ? command.type : ApplicationCommandTypes[command.type];
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) ||
(commandType && commandType !== this.type) ||
// Future proof for options being nullable
// TODO: remove ?? 0 on each when nullable
(command.options?.length ?? 0) !== (this.options?.length ?? 0) ||
@@ -294,15 +289,14 @@ class ApplicationCommand extends Base {
* @private
*/
static _optionEquals(existing, option, enforceOptionOrder = false) {
const optionType = typeof option.type === 'string' ? option.type : ApplicationCommandOptionTypes[option.type];
if (
option.name !== existing.name ||
option.type !== existing.type ||
optionType !== existing.type ||
option.description !== existing.description ||
option.autocomplete !== existing.autocomplete ||
(option.required ??
([ApplicationCommandOptionType.Subcommand, ApplicationCommandOptionType.SubcommandGroup].includes(option.type)
? undefined
: false)) !== existing.required ||
(option.required ?? (['SUB_COMMAND', 'SUB_COMMAND_GROUP'].includes(optionType) ? 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 ||
@@ -331,7 +325,9 @@ class ApplicationCommand extends Base {
}
if (existing.channelTypes) {
const newTypes = option.channelTypes ?? option.channel_types;
const newTypes = (option.channelTypes ?? option.channel_types).map(type =>
typeof type === 'number' ? ChannelTypes[type] : type,
);
for (const type of existing.channelTypes) {
if (!newTypes.includes(type)) return false;
}
@@ -350,17 +346,13 @@ class ApplicationCommand extends Base {
* @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 {boolean} [autocomplete] Whether the option is an autocomplete 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
* @property {number} [minValue] The minimum value for an `INTEGER` or `NUMBER` option
* @property {number} [maxValue] The maximum value for an `INTEGER` or `NUMBER` option
*/
/**
@@ -378,23 +370,24 @@ class ApplicationCommand extends Base {
* @private
*/
static transformOption(option, received) {
const stringType = typeof option.type === 'string' ? option.type : ApplicationCommandOptionTypes[option.type];
const channelTypesKey = received ? 'channelTypes' : 'channel_types';
const minValueKey = received ? 'minValue' : 'min_value';
const maxValueKey = received ? 'maxValue' : 'max_value';
return {
type: option.type,
type: typeof option.type === 'number' && !received ? option.type : ApplicationCommandOptionTypes[option.type],
name: option.name,
description: option.description,
required:
option.required ??
(option.type === ApplicationCommandOptionType.Subcommand ||
option.type === ApplicationCommandOptionType.SubcommandGroup
? undefined
: false),
option.required ?? (stringType === 'SUB_COMMAND' || stringType === 'SUB_COMMAND_GROUP' ? undefined : false),
autocomplete: option.autocomplete,
choices: option.choices,
options: option.options?.map(o => this.transformOption(o, received)),
[channelTypesKey]: option.channelTypes ?? option.channel_types,
[channelTypesKey]: received
? option.channel_types?.map(type => ChannelTypes[type])
: option.channelTypes?.map(type => (typeof type === 'string' ? ChannelTypes[type] : type)) ??
// When transforming to API data, accept API data
option.channel_types,
[minValueKey]: option.minValue ?? option.min_value,
[maxValueKey]: option.maxValue ?? option.max_value,
};

View File

@@ -1,8 +1,8 @@
'use strict';
const { InteractionResponseType, Routes } = require('discord-api-types/v9');
const CommandInteractionOptionResolver = require('./CommandInteractionOptionResolver');
const Interaction = require('./Interaction');
const { InteractionResponseTypes, ApplicationCommandOptionTypes } = require('../util/Constants');
/**
* Represents an autocomplete interaction.
@@ -30,12 +30,6 @@ class AutocompleteInteraction extends Interaction {
*/
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}
@@ -46,7 +40,10 @@ class AutocompleteInteraction extends Interaction {
* The options passed to the command
* @type {CommandInteractionOptionResolver}
*/
this.options = new CommandInteractionOptionResolver(this.client, data.data.options ?? []);
this.options = new CommandInteractionOptionResolver(
this.client,
data.data.options?.map(option => this.transformOption(option, data.data.resolved)) ?? [],
);
}
/**
@@ -58,6 +55,25 @@ class AutocompleteInteraction extends Interaction {
return this.guild?.commands.cache.get(id) ?? this.client.application.commands.cache.get(id) ?? null;
}
/**
* Transforms an option received from the API.
* @param {APIApplicationCommandOption} option The received option
* @returns {CommandInteractionOption}
* @private
*/
transformOption(option) {
const result = {
name: option.name,
type: ApplicationCommandOptionTypes[option.type],
};
if ('value' in option) result.value = option.value;
if ('options' in option) result.options = option.options.map(opt => this.transformOption(opt));
if ('focused' in option) result.focused = option.focused;
return result;
}
/**
* Sends results for the autocomplete of this interaction.
* @param {ApplicationCommandOptionChoice[]} options The options for the autocomplete
@@ -77,14 +93,14 @@ class AutocompleteInteraction extends Interaction {
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: {
type: InteractionResponseTypes.APPLICATION_COMMAND_AUTOCOMPLETE_RESULT,
data: {
choices: options,
},
},
auth: false,
})
});
this.responded = true;
}
}

View File

@@ -0,0 +1,195 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const Interaction = require('./Interaction');
const InteractionWebhook = require('./InteractionWebhook');
const InteractionResponses = require('./interfaces/InteractionResponses');
const { ApplicationCommandOptionTypes } = require('../util/Constants');
/**
* Represents a command interaction.
* @extends {Interaction}
* @implements {InteractionResponses}
* @abstract
*/
class BaseCommandInteraction extends Interaction {
constructor(client, data) {
super(client, data);
/**
* The id of the channel this interaction was sent in
* @type {Snowflake}
* @name BaseCommandInteraction#channelId
*/
/**
* The invoked application command's id
* @type {Snowflake}
*/
this.commandId = data.data.id;
/**
* The invoked application command's name
* @type {string}
*/
this.commandName = data.data.name;
/**
* Whether the reply to this interaction has been deferred
* @type {boolean}
*/
this.deferred = false;
/**
* Whether this interaction has already been replied to
* @type {boolean}
*/
this.replied = false;
/**
* Whether the reply to this interaction is ephemeral
* @type {?boolean}
*/
this.ephemeral = null;
/**
* An associated interaction webhook, can be used to further interact with this interaction
* @type {InteractionWebhook}
*/
this.webhook = new InteractionWebhook(this.client, this.applicationId, this.token);
}
/**
* The invoked application command, if it was fetched before
* @type {?ApplicationCommand}
*/
get command() {
const id = this.commandId;
return this.guild?.commands.cache.get(id) ?? this.client.application.commands.cache.get(id) ?? null;
}
/**
* Represents the resolved data of a received command interaction.
* @typedef {Object} CommandInteractionResolvedData
* @property {Collection<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
*/
/**
* Transforms the resolved received from the API.
* @param {APIInteractionDataResolved} resolved The received resolved objects
* @returns {CommandInteractionResolvedData}
* @private
*/
transformResolved({ members, users, channels, roles, messages }) {
const result = {};
if (members) {
result.members = new Collection();
for (const [id, member] of Object.entries(members)) {
const user = users[id];
result.members.set(id, this.guild?.members._add({ user, ...member }) ?? member);
}
}
if (users) {
result.users = new Collection();
for (const user of Object.values(users)) {
result.users.set(user.id, this.client.users._add(user));
}
}
if (roles) {
result.roles = new Collection();
for (const role of Object.values(roles)) {
result.roles.set(role.id, this.guild?.roles._add(role) ?? role);
}
}
if (channels) {
result.channels = new Collection();
for (const channel of Object.values(channels)) {
result.channels.set(channel.id, this.client.channels._add(channel, this.guild) ?? channel);
}
}
if (messages) {
result.messages = new Collection();
for (const message of Object.values(messages)) {
result.messages.set(message.id, this.channel?.messages?._add(message) ?? message);
}
}
return result;
}
/**
* Represents an option of a received command interaction.
* @typedef {Object} CommandInteractionOption
* @property {string} name The name of the option
* @property {ApplicationCommandOptionType} type The type of the option
* @property {boolean} [autocomplete] Whether the option is an autocomplete 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
*/
/**
* Transforms an option received from the API.
* @param {APIApplicationCommandOption} option The received option
* @param {APIInteractionDataResolved} resolved The resolved interaction data
* @returns {CommandInteractionOption}
* @private
*/
transformOption(option, resolved) {
const result = {
name: option.name,
type: ApplicationCommandOptionTypes[option.type],
};
if ('value' in option) result.value = option.value;
if ('options' in option) result.options = option.options.map(opt => this.transformOption(opt, resolved));
if (resolved) {
const user = resolved.users?.[option.value];
if (user) result.user = this.client.users._add(user);
const member = resolved.members?.[option.value];
if (member) result.member = this.guild?.members._add({ user, ...member }) ?? member;
const channel = resolved.channels?.[option.value];
if (channel) result.channel = this.client.channels._add(channel, this.guild) ?? channel;
const role = resolved.roles?.[option.value];
if (role) result.role = this.guild?.roles._add(role) ?? role;
}
return result;
}
// These are here only for documentation purposes - they are implemented by InteractionResponses
/* eslint-disable no-empty-function */
deferReply() {}
reply() {}
fetchReply() {}
editReply() {}
deleteReply() {}
followUp() {}
}
InteractionResponses.applyToClass(BaseCommandInteraction, ['deferUpdate', 'update']);
module.exports = BaseCommandInteraction;
/* eslint-disable max-len */
/**
* @external APIInteractionDataResolved
* @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-resolved-data-structure}
*/

View File

@@ -1,8 +1,7 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { Routes } = require('discord-api-types/v9');
const Base = require('./Base');
const SnowflakeUtil = require('../util/SnowflakeUtil');
/**
* The base class for {@link Guild}, {@link OAuth2Guild} and {@link InviteGuild}.
@@ -33,7 +32,7 @@ class BaseGuild extends Base {
/**
* An array of features available to this guild
* @type {GuildFeature[]}
* @type {Features[]}
*/
this.features = data.features;
}
@@ -44,7 +43,7 @@ class BaseGuild extends Base {
* @readonly
*/
get createdTimestamp() {
return DiscordSnowflake.timestampFrom(this.id);
return SnowflakeUtil.timestampFrom(this.id);
}
/**
@@ -88,11 +87,12 @@ class BaseGuild extends Base {
/**
* The URL to this guild's icon.
* @param {ImageURLOptions} [options={}] Options for the image URL
* @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);
iconURL({ format, size, dynamic } = {}) {
if (!this.icon) return null;
return this.client.rest.cdn.Icon(this.id, this.icon, format, size, dynamic);
}
/**
@@ -100,9 +100,7 @@ class BaseGuild extends Base {
* @returns {Promise<Guild>}
*/
async fetch() {
const data = await this.client.api.guilds(this.id).get({
query: new URLSearchParams({ with_counts: true }),
});
const data = await this.client.api.guilds(this.id).get({ query: { with_counts: true } });
return this.client.guilds._add(data);
}

View File

@@ -1,9 +1,12 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const GuildChannel = require('./GuildChannel');
const Webhook = require('./Webhook');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const MessageManager = require('../managers/MessageManager');
const ThreadManager = require('../managers/ThreadManager');
const DataResolver = require('../util/DataResolver');
/**
* Represents a text-based guild channel on Discord.
@@ -63,7 +66,7 @@ class BaseGuildTextChannel extends GuildChannel {
* 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;
this.lastPinTimestamp = data.last_pin_timestamp ? new Date(data.last_pin_timestamp).getTime() : null;
}
if ('default_auto_archive_duration' in data) {
@@ -118,8 +121,11 @@ class BaseGuildTextChannel extends GuildChannel {
* .then(hooks => console.log(`This channel has ${hooks.size} hooks`))
* .catch(console.error);
*/
fetchWebhooks() {
return this.guild.channels.fetchWebhooks(this.id);
async fetchWebhooks() {
const data = await this.client.api.channels[this.id].webhooks.get();
const hooks = new Collection();
for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook));
return hooks;
}
/**
@@ -143,8 +149,18 @@ class BaseGuildTextChannel extends GuildChannel {
* .then(console.log)
* .catch(console.error)
*/
createWebhook(name, options = {}) {
return this.guild.channels.createWebhook(this.id, name, options);
async createWebhook(name, { avatar, reason } = {}) {
if (typeof avatar === 'string' && !avatar.startsWith('data:')) {
avatar = await DataResolver.resolveImage(avatar);
}
const data = await this.client.api.channels[this.id].webhooks.post({
data: {
name,
avatar,
},
reason,
});
return new Webhook(this.client, data);
}
/**
@@ -162,28 +178,19 @@ class BaseGuildTextChannel extends GuildChannel {
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
* @property {boolean} [temporary=false] Whether members that joined via the invite should be automatically
* kicked after 24 hours if they have not yet received a role
* @property {number} [maxAge] 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 {number} [maxAge=86400] How long the invite should last (in seconds, 0 for forever)
* @property {number} [maxUses=0] Maximum number of uses
* @property {boolean} [unique=false] Create a unique invite, or use an existing one with similar settings
* @property {UserResolvable} [targetUser] The user whose stream to display for this invite,
* required if `targetType` is {@link InviteTargetType.Stream}, the user must be streaming in the channel
* required if `targetType` is 1, the user must be streaming in the channel
* @property {ApplicationResolvable} [targetApplication] The embedded application to open for this invite,
* required if `targetType` is {@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
* required if `targetType` is 2, the application must have the `EMBEDDED` flag
* @property {TargetType} [targetType] The type of the target for this voice channel invite
* @property {string} [reason] The reason for creating the invite
*/

View File

@@ -1,8 +1,8 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { PermissionFlagsBits } = require('discord-api-types/v9');
const GuildChannel = require('./GuildChannel');
const Permissions = require('../util/Permissions');
/**
* Represents a voice-based guild channel on Discord.
@@ -72,11 +72,11 @@ class BaseGuildVoiceChannel extends GuildChannel {
if (!permissions) return false;
// This flag allows joining even if timed out
if (permissions.has(PermissionFlagsBits.Administrator, false)) return true;
if (permissions.has(Permissions.FLAGS.ADMINISTRATOR, false)) return true;
return (
this.guild.me.communicationDisabledUntilTimestamp < Date.now() &&
permissions.has(PermissionFlagsBits.Connect, false)
permissions.has(Permissions.FLAGS.CONNECT, false)
);
}

View File

@@ -0,0 +1,103 @@
'use strict';
const { TypeError } = require('../errors');
const { MessageComponentTypes, Events } = require('../util/Constants');
/**
* Represents an interactive component of a Message. It should not be necessary to construct this directly.
* See {@link MessageComponent}
*/
class BaseMessageComponent {
/**
* Options for a BaseMessageComponent
* @typedef {Object} BaseMessageComponentOptions
* @property {MessageComponentTypeResolvable} type The type of this component
*/
/**
* Data that can be resolved into options for a MessageComponent. This can be:
* * MessageActionRowOptions
* * MessageButtonOptions
* * MessageSelectMenuOptions
* @typedef {MessageActionRowOptions|MessageButtonOptions|MessageSelectMenuOptions} MessageComponentOptions
*/
/**
* Components that can be sent in a message. These can be:
* * MessageActionRow
* * MessageButton
* * MessageSelectMenu
* @typedef {MessageActionRow|MessageButton|MessageSelectMenu} MessageComponent
* @see {@link https://discord.com/developers/docs/interactions/message-components#component-object-component-types}
*/
/**
* Data that can be resolved to a MessageComponentType. This can be:
* * MessageComponentType
* * string
* * number
* @typedef {string|number|MessageComponentType} MessageComponentTypeResolvable
*/
/**
* @param {BaseMessageComponent|BaseMessageComponentOptions} [data={}] The options for this component
*/
constructor(data) {
/**
* The type of this component
* @type {?MessageComponentType}
*/
this.type = 'type' in data ? BaseMessageComponent.resolveType(data.type) : null;
}
/**
* Constructs a MessageComponent based on the type of the incoming data
* @param {MessageComponentOptions} data Data for a MessageComponent
* @param {Client|WebhookClient} [client] Client constructing this component
* @returns {?MessageComponent}
* @private
*/
static create(data, client) {
let component;
let type = data.type;
if (typeof type === 'string') type = MessageComponentTypes[type];
switch (type) {
case MessageComponentTypes.ACTION_ROW: {
const MessageActionRow = require('./MessageActionRow');
component = data instanceof MessageActionRow ? data : new MessageActionRow(data, client);
break;
}
case MessageComponentTypes.BUTTON: {
const MessageButton = require('./MessageButton');
component = data instanceof MessageButton ? data : new MessageButton(data);
break;
}
case MessageComponentTypes.SELECT_MENU: {
const MessageSelectMenu = require('./MessageSelectMenu');
component = data instanceof MessageSelectMenu ? data : new MessageSelectMenu(data);
break;
}
default:
if (client) {
client.emit(Events.DEBUG, `[BaseMessageComponent] Received component with unknown type: ${data.type}`);
} else {
throw new TypeError('INVALID_TYPE', 'data.type', 'valid MessageComponentType');
}
}
return component;
}
/**
* Resolves the type of a MessageComponent
* @param {MessageComponentTypeResolvable} type The type to resolve
* @returns {MessageComponentType}
* @private
*/
static resolveType(type) {
return typeof type === 'string' ? type : MessageComponentTypes[type];
}
}
module.exports = BaseMessageComponent;

View File

@@ -1,12 +0,0 @@
'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;

View File

@@ -1,7 +1,6 @@
'use strict';
const GuildChannel = require('./GuildChannel');
const CategoryChannelChildManager = require('../managers/CategoryChannelChildManager');
/**
* Represents a guild category channel on Discord.
@@ -9,12 +8,12 @@ const CategoryChannelChildManager = require('../managers/CategoryChannelChildMan
*/
class CategoryChannel extends GuildChannel {
/**
* A manager of the channels belonging to this category
* @type {CategoryChannelChildManager}
* Channels that are a part of this category
* @type {Collection<Snowflake, GuildChannel>}
* @readonly
*/
get children() {
return new CategoryChannelChildManager(this);
return this.guild.channels.cache.filter(c => c.parentId === this.id);
}
/**
@@ -27,6 +26,36 @@ class CategoryChannel extends GuildChannel {
* @param {SetParentOptions} [options={}] The options for setting the parent
* @returns {Promise<GuildChannel>}
*/
/**
* Options for creating a channel using {@link CategoryChannel#createChannel}.
* @typedef {Object} CategoryCreateChannelOptions
* @property {ChannelType|number} [type='GUILD_TEXT'] The type of the new channel.
* @property {string} [topic] The topic for the new channel
* @property {boolean} [nsfw] Whether the new channel is NSFW
* @property {number} [bitrate] Bitrate of the new channel in bits (only voice)
* @property {number} [userLimit] Maximum amount of users allowed in the new channel (only voice)
* @property {OverwriteResolvable[]|Collection<Snowflake, OverwriteResolvable>} [permissionOverwrites]
* Permission overwrites of the new channel
* @property {number} [position] Position of the new channel
* @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the new channel in seconds
* @property {string} [rtcRegion] The specific region of the new channel.
* @property {string} [reason] Reason for creating the new channel
*/
/**
* Creates a new channel within this category.
* <info>You cannot create a channel of type `GUILD_CATEGORY` inside a CategoryChannel.</info>
* @param {string} name The name of the new channel
* @param {CategoryCreateChannelOptions} options Options for creating the new channel
* @returns {Promise<GuildChannel>}
*/
createChannel(name, options) {
return this.guild.channels.create(name, {
...options,
parent: this.id,
});
}
}
module.exports = CategoryChannel;

View File

@@ -1,9 +1,7 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { ChannelType, Routes } = require('discord-api-types/v9');
const process = require('node:process');
const Base = require('./Base');
const { ThreadChannelTypes } = require('../util/Constants');
let CategoryChannel;
let DMChannel;
let NewsChannel;
@@ -12,6 +10,16 @@ let StoreChannel;
let TextChannel;
let ThreadChannel;
let VoiceChannel;
const { ChannelTypes, ThreadChannelTypes, VoiceBasedChannelTypes } = require('../util/Constants');
const SnowflakeUtil = require('../util/SnowflakeUtil');
/**
* @type {WeakSet<Channel>}
* @private
* @internal
*/
const deletedChannels = new WeakSet();
let deprecationEmittedForDeleted = false;
/**
* Represents any channel on Discord.
@@ -22,11 +30,12 @@ class Channel extends Base {
constructor(client, data, immediatePatch = true) {
super(client);
const type = ChannelTypes[data?.type];
/**
* The type of the channel
* @type {ChannelType}
*/
this.type = data.type;
this.type = type ?? 'UNKNOWN';
if (data && immediatePatch) this._patch(data);
}
@@ -45,7 +54,7 @@ class Channel extends Base {
* @readonly
*/
get createdTimestamp() {
return DiscordSnowflake.timestampFrom(this.id);
return SnowflakeUtil.timestampFrom(this.id);
}
/**
@@ -58,12 +67,33 @@ class Channel extends Base {
}
/**
* The URL to the channel
* @type {string}
* @readonly
* Whether or not the structure has been deleted
* @type {boolean}
* @deprecated This will be removed in the next major version, see https://github.com/discordjs/discord.js/issues/7091
*/
get url() {
return `https://discord.com/channels/${this.isDMBased() ? '@me' : this.guildId}/${this.id}`;
get deleted() {
if (!deprecationEmittedForDeleted) {
deprecationEmittedForDeleted = true;
process.emitWarning(
'Channel#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.',
'DeprecationWarning',
);
}
return deletedChannels.has(this);
}
set deleted(value) {
if (!deprecationEmittedForDeleted) {
deprecationEmittedForDeleted = true;
process.emitWarning(
'Channel#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.',
'DeprecationWarning',
);
}
if (value) deletedChannels.add(this);
else deletedChannels.delete(this);
}
/**
@@ -111,59 +141,19 @@ class Channel extends Base {
}
/**
* Indicates whether this channel is a {@link TextChannel}.
* Indicates whether this channel is {@link TextBasedChannels text-based}.
* @returns {boolean}
*/
isText() {
return this.type === ChannelType.GuildText;
return 'messages' in this;
}
/**
* Indicates whether this channel is a {@link DMChannel}.
* @returns {boolean}
*/
isDM() {
return this.type === ChannelType.DM;
}
/**
* Indicates whether this channel is a {@link VoiceChannel}.
* Indicates whether this channel is {@link BaseGuildVoiceChannel voice-based}.
* @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;
return VoiceBasedChannelTypes.includes(this.type);
}
/**
@@ -174,38 +164,6 @@ class Channel extends Base {
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');
@@ -218,9 +176,9 @@ class Channel extends Base {
let channel;
if (!data.guild_id && !guild) {
if ((data.recipients && data.type !== ChannelType.GroupDM) || data.type === ChannelType.DM) {
if ((data.recipients && data.type !== ChannelTypes.GROUP_DM) || data.type === ChannelTypes.DM) {
channel = new DMChannel(client, data);
} else if (data.type === ChannelType.GroupDM) {
} else if (data.type === ChannelTypes.GROUP_DM) {
const PartialGroupDMChannel = require('./PartialGroupDMChannel');
channel = new PartialGroupDMChannel(client, data);
}
@@ -229,33 +187,33 @@ class Channel extends Base {
if (guild || allowUnknownGuild) {
switch (data.type) {
case ChannelType.GuildText: {
case ChannelTypes.GUILD_TEXT: {
channel = new TextChannel(guild, data, client);
break;
}
case ChannelType.GuildVoice: {
case ChannelTypes.GUILD_VOICE: {
channel = new VoiceChannel(guild, data, client);
break;
}
case ChannelType.GuildCategory: {
case ChannelTypes.GUILD_CATEGORY: {
channel = new CategoryChannel(guild, data, client);
break;
}
case ChannelType.GuildNews: {
case ChannelTypes.GUILD_NEWS: {
channel = new NewsChannel(guild, data, client);
break;
}
case ChannelType.GuildStore: {
case ChannelTypes.GUILD_STORE: {
channel = new StoreChannel(guild, data, client);
break;
}
case ChannelType.GuildStageVoice: {
case ChannelTypes.GUILD_STAGE_VOICE: {
channel = new StageChannel(guild, data, client);
break;
}
case ChannelType.GuildNewsThread:
case ChannelType.GuildPublicThread:
case ChannelType.GuildPrivateThread: {
case ChannelTypes.GUILD_NEWS_THREAD:
case ChannelTypes.GUILD_PUBLIC_THREAD:
case ChannelTypes.GUILD_PRIVATE_THREAD: {
channel = new ThreadChannel(guild, data, client, fromInteraction);
if (!allowUnknownGuild) channel.parent?.threads.cache.set(channel.id, channel);
break;
@@ -273,6 +231,7 @@ class Channel extends Base {
}
exports.Channel = Channel;
exports.deletedChannels = deletedChannels;
/**
* @external APIChannel

View File

@@ -1,41 +0,0 @@
'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;

View File

@@ -3,8 +3,8 @@
const Team = require('./Team');
const { Error } = require('../errors/DJSError');
const Application = require('./interfaces/Application');
const ApplicationFlagsBitField = require('../util/ApplicationFlagsBitField');
const ApplicationCommandManager = require('../managers/ApplicationCommandManager');
const ApplicationFlags = require('../util/ApplicationFlags');
/**
* Represents a Client OAuth2 Application.
@@ -24,14 +24,12 @@ class ClientApplication extends Application {
_patch(data) {
super._patch(data);
if(!data) return;
if ('flags' in data) {
/**
* The flags this application has
* @type {ApplicationFlagsBitField}
* @type {ApplicationFlags}
*/
this.flags = new ApplicationFlagsBitField(data.flags).freeze();
this.flags = new ApplicationFlags(data.flags).freeze();
}
if ('cover_image' in data) {

View File

@@ -1,8 +1,8 @@
'use strict';
const { GatewayOpcodes } = require('discord-api-types/v9');
const { Presence } = require('./Presence');
const { TypeError } = require('../errors');
const { ActivityTypes, Opcodes } = require('../util/Constants');
/**
* Represents the client's presence.
@@ -10,7 +10,7 @@ const { TypeError } = require('../errors');
*/
class ClientPresence extends Presence {
constructor(client, data = {}) {
super(client, Object.assign(data, { status: data.status || client.setting.status || 'online', user: { id: null } }));
super(client, Object.assign(data, { status: data.status ?? 'online', user: { id: null } }));
}
/**
@@ -22,13 +22,13 @@ class ClientPresence extends Presence {
const packet = this._parse(presence);
this._patch(packet);
if (typeof presence.shardId === 'undefined') {
this.client.ws.broadcast({ op: GatewayOpcodes.PresenceUpdate, d: packet });
this.client.ws.broadcast({ op: Opcodes.STATUS_UPDATE, d: packet });
} else if (Array.isArray(presence.shardId)) {
for (const shardId of presence.shardId) {
this.client.ws.shards.get(shardId).send({ op: GatewayOpcodes.PresenceUpdate, d: packet });
this.client.ws.shards.get(shardId).send({ op: Opcodes.STATUS_UPDATE, d: packet });
}
} else {
this.client.ws.shards.get(presence.shardId).send({ op: GatewayOpcodes.PresenceUpdate, d: packet });
this.client.ws.shards.get(presence.shardId).send({ op: Opcodes.STATUS_UPDATE, d: packet });
}
return this;
}

View File

@@ -1,6 +1,5 @@
'use strict';
const { Routes } = require('discord-api-types/v9');
const User = require('./User');
const DataResolver = require('../util/DataResolver');
@@ -159,7 +158,7 @@ class ClientUser extends User {
* @returns {ClientPresence}
* @example
* // Set the client user's activity
* client.user.setActivity('discord.js', { type: ActivityType.Watching });
* client.user.setActivity('discord.js', { type: 'WATCHING' });
*/
setActivity(name, options = {}) {
if (!name) return this.setPresence({ activities: [], shardId: options.shardId });

View File

@@ -1,216 +1,41 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const Interaction = require('./Interaction');
const InteractionWebhook = require('./InteractionWebhook');
const MessageAttachment = require('./MessageAttachment');
const InteractionResponses = require('./interfaces/InteractionResponses');
const BaseCommandInteraction = require('./BaseCommandInteraction');
const CommandInteractionOptionResolver = require('./CommandInteractionOptionResolver');
/**
* Represents a command interaction.
* @extends {Interaction}
* @implements {InteractionResponses}
* @abstract
* @extends {BaseCommandInteraction}
*/
class CommandInteraction extends Interaction {
class CommandInteraction extends BaseCommandInteraction {
constructor(client, data) {
super(client, data);
/**
* The id of the channel this interaction was sent in
* @type {Snowflake}
* @name CommandInteraction#channelId
* The options passed to the command.
* @type {CommandInteractionOptionResolver}
*/
/**
* 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);
this.options = new CommandInteractionOptionResolver(
this.client,
data.data.options?.map(option => this.transformOption(option, data.data.resolved)) ?? [],
this.transformResolved(data.data.resolved ?? {}),
);
}
/**
* The invoked application command, if it was fetched before
* @type {?ApplicationCommand}
* 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}
*/
get command() {
const id = this.commandId;
return this.guild?.commands.cache.get(id) ?? this.client.application.commands.cache.get(id) ?? null;
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(' ')}`;
}
/**
* 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}
*/

View File

@@ -1,6 +1,5 @@
'use strict';
const { ApplicationCommandOptionType } = require('discord-api-types/v9');
const { TypeError } = require('../errors');
/**
@@ -39,12 +38,12 @@ class CommandInteractionOptionResolver {
this._hoistedOptions = options;
// Hoist subcommand group if present
if (this._hoistedOptions[0]?.type === ApplicationCommandOptionType.SubcommandGroup) {
if (this._hoistedOptions[0]?.type === 'SUB_COMMAND_GROUP') {
this._group = this._hoistedOptions[0].name;
this._hoistedOptions = this._hoistedOptions[0].options ?? [];
}
// Hoist subcommand if present
if (this._hoistedOptions[0]?.type === ApplicationCommandOptionType.Subcommand) {
if (this._hoistedOptions[0]?.type === 'SUB_COMMAND') {
this._subcommand = this._hoistedOptions[0].name;
this._hoistedOptions = this._hoistedOptions[0].options ?? [];
}
@@ -117,10 +116,10 @@ class CommandInteractionOptionResolver {
/**
* Gets the selected subcommand group.
* @param {boolean} [required=false] Whether to throw an error if there is no subcommand group.
* @param {boolean} [required=true] Whether to throw an error if there is no subcommand group.
* @returns {?string} The name of the selected subcommand group, or null if not set and not required.
*/
getSubcommandGroup(required = false) {
getSubcommandGroup(required = true) {
if (required && !this._group) {
throw new TypeError('COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND_GROUP');
}
@@ -134,7 +133,7 @@ class CommandInteractionOptionResolver {
* @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);
const option = this._getTypedOption(name, 'BOOLEAN', ['value'], required);
return option?.value ?? null;
}
@@ -146,7 +145,7 @@ class CommandInteractionOptionResolver {
* 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);
const option = this._getTypedOption(name, 'CHANNEL', ['channel'], required);
return option?.channel ?? null;
}
@@ -157,7 +156,7 @@ class CommandInteractionOptionResolver {
* @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);
const option = this._getTypedOption(name, 'STRING', ['value'], required);
return option?.value ?? null;
}
@@ -168,7 +167,7 @@ class CommandInteractionOptionResolver {
* @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);
const option = this._getTypedOption(name, 'INTEGER', ['value'], required);
return option?.value ?? null;
}
@@ -179,7 +178,7 @@ class CommandInteractionOptionResolver {
* @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);
const option = this._getTypedOption(name, 'NUMBER', ['value'], required);
return option?.value ?? null;
}
@@ -190,18 +189,19 @@ class CommandInteractionOptionResolver {
* @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);
const option = this._getTypedOption(name, 'USER', ['user'], required);
return option?.user ?? null;
}
/**
* Gets a member option.
* @param {string} name The name of the option.
* @param {boolean} [required=false] Whether to throw an error if the option is not found.
* @returns {?(GuildMember|APIGuildMember)}
* The value of the option, or null if the user is not present in the guild or the option is not set.
* The value of the option, or null if not set and not required.
*/
getMember(name) {
const option = this._getTypedOption(name, ApplicationCommandOptionType.User, ['member'], false);
getMember(name, required = false) {
const option = this._getTypedOption(name, 'USER', ['member'], required);
return option?.member ?? null;
}
@@ -212,21 +212,10 @@ class CommandInteractionOptionResolver {
* @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);
const option = this._getTypedOption(name, '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.
@@ -235,12 +224,7 @@ class CommandInteractionOptionResolver {
* 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,
);
const option = this._getTypedOption(name, 'MENTIONABLE', ['user', 'member', 'role'], required);
return option?.member ?? option?.user ?? option?.role ?? null;
}

View File

@@ -1,14 +1,14 @@
'use strict';
const { ApplicationCommandOptionType } = require('discord-api-types/v9');
const CommandInteraction = require('./CommandInteraction');
const BaseCommandInteraction = require('./BaseCommandInteraction');
const CommandInteractionOptionResolver = require('./CommandInteractionOptionResolver');
const { ApplicationCommandOptionTypes, ApplicationCommandTypes } = require('../util/Constants');
/**
* Represents a context menu interaction.
* @extends {CommandInteraction}
* @extends {BaseCommandInteraction}
*/
class ContextMenuCommandInteraction extends CommandInteraction {
class ContextMenuInteraction extends BaseCommandInteraction {
constructor(client, data) {
super(client, data);
/**
@@ -26,6 +26,12 @@ class ContextMenuCommandInteraction extends CommandInteraction {
* @type {Snowflake}
*/
this.targetId = data.data.target_id;
/**
* The type of the target of the interaction; either USER or MESSAGE
* @type {ApplicationCommandType}
*/
this.targetType = ApplicationCommandTypes[data.data.type];
}
/**
@@ -39,7 +45,7 @@ class ContextMenuCommandInteraction extends CommandInteraction {
if (resolved.users?.[target_id]) {
result.push(
this.transformOption({ name: 'user', type: ApplicationCommandOptionType.User, value: target_id }, resolved),
this.transformOption({ name: 'user', type: ApplicationCommandOptionTypes.USER, value: target_id }, resolved),
);
}
@@ -56,4 +62,4 @@ class ContextMenuCommandInteraction extends CommandInteraction {
}
}
module.exports = ContextMenuCommandInteraction;
module.exports = ContextMenuInteraction;

View File

@@ -1,6 +1,5 @@
'use strict';
const { ChannelType } = require('discord-api-types/v9');
const { Channel } = require('./Channel');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const MessageManager = require('../managers/MessageManager');
@@ -15,7 +14,7 @@ class DMChannel extends Channel {
super(client, data);
// Override the channel type so partials have a known type
this.type = ChannelType.DM;
this.type = 'DM';
/**
* A manager of the messages belonging to this channel
@@ -48,7 +47,7 @@ class DMChannel extends Channel {
* The timestamp when the last pinned message was pinned, if there was one
* @type {?number}
*/
this.lastPinTimestamp = Date.parse(data.last_pin_timestamp);
this.lastPinTimestamp = new Date(data.last_pin_timestamp).getTime();
} else {
this.lastPinTimestamp ??= null;
}

View File

@@ -1,12 +0,0 @@
'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;

View File

@@ -1,7 +1,16 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const process = require('node:process');
const Base = require('./Base');
const SnowflakeUtil = require('../util/SnowflakeUtil');
/**
* @type {WeakSet<Emoji>}
* @private
* @internal
*/
const deletedEmojis = new WeakSet();
let deprecationEmittedForDeleted = false;
/**
* Represents raw emoji data from the API
@@ -37,6 +46,36 @@ class Emoji extends Base {
this.id = emoji.id;
}
/**
* Whether or not the structure has been deleted
* @type {boolean}
* @deprecated This will be removed in the next major version, see https://github.com/discordjs/discord.js/issues/7091
*/
get deleted() {
if (!deprecationEmittedForDeleted) {
deprecationEmittedForDeleted = true;
process.emitWarning(
'Emoji#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.',
'DeprecationWarning',
);
}
return deletedEmojis.has(this);
}
set deleted(value) {
if (!deprecationEmittedForDeleted) {
deprecationEmittedForDeleted = true;
process.emitWarning(
'Emoji#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.',
'DeprecationWarning',
);
}
if (value) deletedEmojis.add(this);
else deletedEmojis.delete(this);
}
/**
* The identifier of this emoji, used for message reactions
* @type {string}
@@ -53,7 +92,7 @@ class Emoji extends Base {
* @readonly
*/
get url() {
return this.id && this.client.rest.cdn.emoji(this.id, this.animated ? 'gif' : 'png');
return this.id && this.client.rest.cdn.Emoji(this.id, this.animated ? 'gif' : 'png');
}
/**
@@ -62,7 +101,7 @@ class Emoji extends Base {
* @readonly
*/
get createdTimestamp() {
return this.id && DiscordSnowflake.timestampFrom(this.id);
return this.id && SnowflakeUtil.timestampFrom(this.id);
}
/**
@@ -101,6 +140,7 @@ class Emoji extends Base {
}
exports.Emoji = Emoji;
exports.deletedEmojis = deletedEmojis;
/**
* @external APIEmoji

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +1,31 @@
'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 { OverwriteTypes, PartialTypes } = require('../util/Constants');
const SnowflakeUtil = require('../util/SnowflakeUtil');
const Util = require('../util/Util');
/**
* The target type of an entry. Here are the available types:
* * Guild
* * Channel
* * User
* * Role
* * Invite
* * Webhook
* * Emoji
* * Message
* * Integration
* * StageInstance
* * Sticker
* * Thread
* * GuildScheduledEvent
* * GUILD
* * CHANNEL
* * USER
* * ROLE
* * INVITE
* * WEBHOOK
* * EMOJI
* * MESSAGE
* * INTEGRATION
* * STAGE_INSTANCE
* * STICKER
* * THREAD
* * GUILD_SCHEDULED_EVENT
* @typedef {string} AuditLogTargetType
*/
@@ -36,21 +35,131 @@ const Util = require('../util/Util');
* @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',
ALL: 'ALL',
GUILD: 'GUILD',
GUILD_SCHEDULED_EVENT: 'GUILD_SCHEDULED_EVENT',
CHANNEL: 'CHANNEL',
USER: 'USER',
ROLE: 'ROLE',
INVITE: 'INVITE',
WEBHOOK: 'WEBHOOK',
EMOJI: 'EMOJI',
MESSAGE: 'MESSAGE',
INTEGRATION: 'INTEGRATION',
STAGE_INSTANCE: 'STAGE_INSTANCE',
STICKER: 'STICKER',
THREAD: 'THREAD',
UNKNOWN: 'UNKNOWN',
};
/**
* The action of an entry. Here are the available actions:
* * ALL: null
* * GUILD_UPDATE: 1
* * CHANNEL_CREATE: 10
* * CHANNEL_UPDATE: 11
* * CHANNEL_DELETE: 12
* * CHANNEL_OVERWRITE_CREATE: 13
* * CHANNEL_OVERWRITE_UPDATE: 14
* * CHANNEL_OVERWRITE_DELETE: 15
* * MEMBER_KICK: 20
* * MEMBER_PRUNE: 21
* * MEMBER_BAN_ADD: 22
* * MEMBER_BAN_REMOVE: 23
* * MEMBER_UPDATE: 24
* * MEMBER_ROLE_UPDATE: 25
* * MEMBER_MOVE: 26
* * MEMBER_DISCONNECT: 27
* * BOT_ADD: 28,
* * ROLE_CREATE: 30
* * ROLE_UPDATE: 31
* * ROLE_DELETE: 32
* * INVITE_CREATE: 40
* * INVITE_UPDATE: 41
* * INVITE_DELETE: 42
* * WEBHOOK_CREATE: 50
* * WEBHOOK_UPDATE: 51
* * WEBHOOK_DELETE: 52
* * EMOJI_CREATE: 60
* * EMOJI_UPDATE: 61
* * EMOJI_DELETE: 62
* * MESSAGE_DELETE: 72
* * MESSAGE_BULK_DELETE: 73
* * MESSAGE_PIN: 74
* * MESSAGE_UNPIN: 75
* * INTEGRATION_CREATE: 80
* * INTEGRATION_UPDATE: 81
* * INTEGRATION_DELETE: 82
* * STAGE_INSTANCE_CREATE: 83
* * STAGE_INSTANCE_UPDATE: 84
* * STAGE_INSTANCE_DELETE: 85
* * STICKER_CREATE: 90
* * STICKER_UPDATE: 91
* * STICKER_DELETE: 92
* * GUILD_SCHEDULED_EVENT_CREATE: 100
* * GUILD_SCHEDULED_EVENT_UPDATE: 101
* * GUILD_SCHEDULED_EVENT_DELETE: 102
* * THREAD_CREATE: 110
* * THREAD_UPDATE: 111
* * THREAD_DELETE: 112
* @typedef {?(number|string)} AuditLogAction
* @see {@link https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-audit-log-events}
*/
/**
* All available actions keyed under their names to their numeric values.
* @name GuildAuditLogs.Actions
* @type {Object<string, number>}
*/
const Actions = {
ALL: null,
GUILD_UPDATE: 1,
CHANNEL_CREATE: 10,
CHANNEL_UPDATE: 11,
CHANNEL_DELETE: 12,
CHANNEL_OVERWRITE_CREATE: 13,
CHANNEL_OVERWRITE_UPDATE: 14,
CHANNEL_OVERWRITE_DELETE: 15,
MEMBER_KICK: 20,
MEMBER_PRUNE: 21,
MEMBER_BAN_ADD: 22,
MEMBER_BAN_REMOVE: 23,
MEMBER_UPDATE: 24,
MEMBER_ROLE_UPDATE: 25,
MEMBER_MOVE: 26,
MEMBER_DISCONNECT: 27,
BOT_ADD: 28,
ROLE_CREATE: 30,
ROLE_UPDATE: 31,
ROLE_DELETE: 32,
INVITE_CREATE: 40,
INVITE_UPDATE: 41,
INVITE_DELETE: 42,
WEBHOOK_CREATE: 50,
WEBHOOK_UPDATE: 51,
WEBHOOK_DELETE: 52,
EMOJI_CREATE: 60,
EMOJI_UPDATE: 61,
EMOJI_DELETE: 62,
MESSAGE_DELETE: 72,
MESSAGE_BULK_DELETE: 73,
MESSAGE_PIN: 74,
MESSAGE_UNPIN: 75,
INTEGRATION_CREATE: 80,
INTEGRATION_UPDATE: 81,
INTEGRATION_DELETE: 82,
STAGE_INSTANCE_CREATE: 83,
STAGE_INSTANCE_UPDATE: 84,
STAGE_INSTANCE_DELETE: 85,
STICKER_CREATE: 90,
STICKER_UPDATE: 91,
STICKER_DELETE: 92,
GUILD_SCHEDULED_EVENT_CREATE: 100,
GUILD_SCHEDULED_EVENT_UPDATE: 101,
GUILD_SCHEDULED_EVENT_DELETE: 102,
THREAD_CREATE: 110,
THREAD_UPDATE: 111,
THREAD_DELETE: 112,
};
/**
@@ -132,28 +241,28 @@ class GuildAuditLogs {
* @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;
if (target < 10) return Targets.GUILD;
if (target < 20) return Targets.CHANNEL;
if (target < 30) return Targets.USER;
if (target < 40) return Targets.ROLE;
if (target < 50) return Targets.INVITE;
if (target < 60) return Targets.WEBHOOK;
if (target < 70) return Targets.EMOJI;
if (target < 80) return Targets.MESSAGE;
if (target < 83) return Targets.INTEGRATION;
if (target < 86) return Targets.STAGE_INSTANCE;
if (target < 100) return Targets.STICKER;
if (target < 110) return Targets.GUILD_SCHEDULED_EVENT;
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
* The action type of an entry, e.g. `CREATE`. Here are the available types:
* * CREATE
* * DELETE
* * UPDATE
* * ALL
* @typedef {string} AuditLogActionType
*/
@@ -165,73 +274,73 @@ class GuildAuditLogs {
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,
Actions.CHANNEL_CREATE,
Actions.CHANNEL_OVERWRITE_CREATE,
Actions.MEMBER_BAN_REMOVE,
Actions.BOT_ADD,
Actions.ROLE_CREATE,
Actions.INVITE_CREATE,
Actions.WEBHOOK_CREATE,
Actions.EMOJI_CREATE,
Actions.MESSAGE_PIN,
Actions.INTEGRATION_CREATE,
Actions.STAGE_INSTANCE_CREATE,
Actions.STICKER_CREATE,
Actions.GUILD_SCHEDULED_EVENT_CREATE,
Actions.THREAD_CREATE,
].includes(action)
) {
return 'Create';
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,
Actions.CHANNEL_DELETE,
Actions.CHANNEL_OVERWRITE_DELETE,
Actions.MEMBER_KICK,
Actions.MEMBER_PRUNE,
Actions.MEMBER_BAN_ADD,
Actions.MEMBER_DISCONNECT,
Actions.ROLE_DELETE,
Actions.INVITE_DELETE,
Actions.WEBHOOK_DELETE,
Actions.EMOJI_DELETE,
Actions.MESSAGE_DELETE,
Actions.MESSAGE_BULK_DELETE,
Actions.MESSAGE_UNPIN,
Actions.INTEGRATION_DELETE,
Actions.STAGE_INSTANCE_DELETE,
Actions.STICKER_DELETE,
Actions.GUILD_SCHEDULED_EVENT_DELETE,
Actions.THREAD_DELETE,
].includes(action)
) {
return 'Delete';
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,
Actions.GUILD_UPDATE,
Actions.CHANNEL_UPDATE,
Actions.CHANNEL_OVERWRITE_UPDATE,
Actions.MEMBER_UPDATE,
Actions.MEMBER_ROLE_UPDATE,
Actions.MEMBER_MOVE,
Actions.ROLE_UPDATE,
Actions.INVITE_UPDATE,
Actions.WEBHOOK_UPDATE,
Actions.EMOJI_UPDATE,
Actions.INTEGRATION_UPDATE,
Actions.STAGE_INSTANCE_UPDATE,
Actions.STICKER_UPDATE,
Actions.GUILD_SCHEDULED_EVENT_UPDATE,
Actions.THREAD_UPDATE,
].includes(action)
) {
return 'Update';
return 'UPDATE';
}
return 'All';
return 'ALL';
}
toJSON() {
@@ -261,7 +370,7 @@ class GuildAuditLogsEntry {
* Specific action type of this entry in its string presentation
* @type {AuditLogAction}
*/
this.action = Object.keys(AuditLogEvent).find(k => AuditLogEvent[k] === data.action_type);
this.action = Object.keys(Actions).find(k => Actions[k] === data.action_type);
/**
* The reason of this entry
@@ -274,7 +383,7 @@ class GuildAuditLogsEntry {
* @type {?User}
*/
this.executor = data.user_id
? guild.client.options.partials.includes(Partials.User)
? guild.client.options.partials.includes(PartialTypes.USER)
? guild.client.users._add({ id: data.user_id })
: guild.client.users.cache.get(data.user_id)
: null;
@@ -305,52 +414,52 @@ class GuildAuditLogsEntry {
*/
this.extra = null;
switch (data.action_type) {
case AuditLogEvent.MemberPrune:
case Actions.MEMBER_PRUNE:
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:
case Actions.MEMBER_MOVE:
case Actions.MESSAGE_DELETE:
case Actions.MESSAGE_BULK_DELETE:
this.extra = {
channel: guild.channels.cache.get(data.options.channel_id) ?? { id: data.options.channel_id },
count: Number(data.options.count),
};
break;
case AuditLogEvent.MessagePin:
case AuditLogEvent.MessageUnpin:
case Actions.MESSAGE_PIN:
case Actions.MESSAGE_UNPIN:
this.extra = {
channel: guild.client.channels.cache.get(data.options.channel_id) ?? { id: data.options.channel_id },
messageId: data.options.message_id,
};
break;
case AuditLogEvent.MemberDisconnect:
case Actions.MEMBER_DISCONNECT:
this.extra = {
count: Number(data.options.count),
};
break;
case AuditLogEvent.ChannelOverwriteCreate:
case AuditLogEvent.ChannelOverwriteUpdate:
case AuditLogEvent.ChannelOverwriteDelete:
switch (data.options.type) {
case OverwriteType.Role:
case Actions.CHANNEL_OVERWRITE_CREATE:
case Actions.CHANNEL_OVERWRITE_UPDATE:
case Actions.CHANNEL_OVERWRITE_DELETE:
switch (Number(data.options.type)) {
case OverwriteTypes.role:
this.extra = guild.roles.cache.get(data.options.id) ?? {
id: data.options.id,
name: data.options.role_name,
type: OverwriteType.Role,
type: OverwriteTypes[OverwriteTypes.role],
};
break;
case OverwriteType.Member:
case OverwriteTypes.member:
this.extra = guild.members.cache.get(data.options.id) ?? {
id: data.options.id,
type: OverwriteType.Member,
type: OverwriteTypes[OverwriteTypes.member],
};
break;
@@ -359,9 +468,9 @@ class GuildAuditLogsEntry {
}
break;
case AuditLogEvent.StageInstanceCreate:
case AuditLogEvent.StageInstanceDelete:
case AuditLogEvent.StageInstanceUpdate:
case Actions.STAGE_INSTANCE_CREATE:
case Actions.STAGE_INSTANCE_DELETE:
case Actions.STAGE_INSTANCE_UPDATE:
this.extra = {
channel: guild.client.channels.cache.get(data.options?.channel_id) ?? { id: data.options?.channel_id },
};
@@ -376,20 +485,20 @@ class GuildAuditLogsEntry {
* @type {?AuditLogEntryTarget}
*/
this.target = null;
if (targetType === Targets.Unknown) {
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)
// MEMBER_DISCONNECT and similar types do not provide a target_id.
} else if (targetType === Targets.USER && data.target_id) {
this.target = guild.client.options.partials.includes(PartialTypes.USER)
? guild.client.users._add({ id: data.target_id })
: guild.client.users.cache.get(data.target_id);
} else if (targetType === Targets.Guild) {
} else if (targetType === Targets.GUILD) {
this.target = guild.client.guilds.cache.get(data.target_id);
} else if (targetType === Targets.Webhook) {
} else if (targetType === Targets.WEBHOOK) {
this.target =
logs.webhooks.get(data.target_id) ??
new Webhook(
@@ -405,7 +514,7 @@ class GuildAuditLogsEntry {
},
),
);
} else if (targetType === Targets.Invite) {
} else if (targetType === Targets.INVITE) {
let change = this.changes.find(c => c.key === 'code');
change = change.new ?? change.old;
@@ -421,13 +530,13 @@ class GuildAuditLogsEntry {
{ guild },
),
);
} else if (targetType === Targets.Message) {
// Discord sends a channel id for the MessageBulkDelete action type.
} else if (targetType === Targets.MESSAGE) {
// Discord sends a channel id for the MESSAGE_BULK_DELETE action type.
this.target =
data.action_type === AuditLogEvent.MessageBulkDelete
data.action_type === Actions.MESSAGE_BULK_DELETE
? guild.channels.cache.get(data.target_id) ?? { id: data.target_id }
: guild.client.users.cache.get(data.target_id);
} else if (targetType === Targets.Integration) {
} else if (targetType === Targets.INTEGRATION) {
this.target =
logs.integrations.get(data.target_id) ??
new Integration(
@@ -441,7 +550,7 @@ class GuildAuditLogsEntry {
),
guild,
);
} else if (targetType === Targets.Channel || targetType === Targets.Thread) {
} else if (targetType === Targets.CHANNEL || targetType === Targets.THREAD) {
this.target =
guild.channels.cache.get(data.target_id) ??
this.changes.reduce(
@@ -451,7 +560,7 @@ class GuildAuditLogsEntry {
},
{ id: data.target_id },
);
} else if (targetType === Targets.StageInstance) {
} else if (targetType === Targets.STAGE_INSTANCE) {
this.target =
guild.stageInstances.cache.get(data.target_id) ??
new StageInstance(
@@ -468,7 +577,7 @@ class GuildAuditLogsEntry {
},
),
);
} else if (targetType === Targets.Sticker) {
} else if (targetType === Targets.STICKER) {
this.target =
guild.stickers.cache.get(data.target_id) ??
new Sticker(
@@ -481,7 +590,7 @@ class GuildAuditLogsEntry {
{ id: data.target_id },
),
);
} else if (targetType === Targets.GuildScheduledEvent) {
} else if (targetType === Targets.GUILD_SCHEDULED_EVENT) {
this.target =
guild.scheduledEvents.cache.get(data.target_id) ??
new GuildScheduledEvent(
@@ -505,7 +614,7 @@ class GuildAuditLogsEntry {
* @readonly
*/
get createdTimestamp() {
return DiscordSnowflake.timestampFrom(this.id);
return SnowflakeUtil.timestampFrom(this.id);
}
/**
@@ -522,6 +631,7 @@ class GuildAuditLogsEntry {
}
}
GuildAuditLogs.Actions = Actions;
GuildAuditLogs.Targets = Targets;
GuildAuditLogs.Entry = GuildAuditLogsEntry;

View File

@@ -1,11 +1,12 @@
'use strict';
const { PermissionFlagsBits } = require('discord-api-types/v9');
const { Channel } = require('./Channel');
const PermissionOverwrites = require('./PermissionOverwrites');
const { Error } = require('../errors');
const PermissionOverwriteManager = require('../managers/PermissionOverwriteManager');
const { VoiceBasedChannelTypes } = require('../util/Constants');
const PermissionsBitField = require('../util/PermissionsBitField');
const { ChannelTypes, VoiceBasedChannelTypes } = require('../util/Constants');
const Permissions = require('../util/Permissions');
const Util = require('../util/Util');
/**
* Represents a guild channel from any of the following:
@@ -120,11 +121,11 @@ class GuildChannel extends Channel {
// Handle empty overwrite
if (
(!channelVal &&
parentVal.deny.bitfield === PermissionsBitField.defaultBit &&
parentVal.allow.bitfield === PermissionsBitField.defaultBit) ||
parentVal.deny.bitfield === Permissions.defaultBit &&
parentVal.allow.bitfield === Permissions.defaultBit) ||
(!parentVal &&
channelVal.deny.bitfield === PermissionsBitField.defaultBit &&
channelVal.allow.bitfield === PermissionsBitField.defaultBit)
channelVal.deny.bitfield === Permissions.defaultBit &&
channelVal.allow.bitfield === Permissions.defaultBit)
) {
return true;
}
@@ -153,7 +154,7 @@ class GuildChannel extends Channel {
* 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>}
* @returns {?Readonly<Permissions>}
*/
permissionsFor(memberOrRole, checkAdmin = true) {
const member = this.guild.members.resolve(memberOrRole);
@@ -192,30 +193,28 @@ class GuildChannel extends Channel {
* 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>}
* @returns {Readonly<Permissions>}
* @private
*/
memberPermissions(member, checkAdmin) {
if (checkAdmin && member.id === this.guild.ownerId) {
return new PermissionsBitField(PermissionsBitField.All).freeze();
}
if (checkAdmin && member.id === this.guild.ownerId) return new Permissions(Permissions.ALL).freeze();
const roles = member.roles.cache;
const permissions = new PermissionsBitField(roles.map(role => role.permissions));
const permissions = new Permissions(roles.map(role => role.permissions));
if (checkAdmin && permissions.has(PermissionFlagsBits.Administrator)) {
return new PermissionsBitField(PermissionsBitField.All).freeze();
if (checkAdmin && permissions.has(Permissions.FLAGS.ADMINISTRATOR)) {
return new Permissions(Permissions.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)
.remove(overwrites.everyone?.deny ?? Permissions.defaultBit)
.add(overwrites.everyone?.allow ?? Permissions.defaultBit)
.remove(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.deny) : Permissions.defaultBit)
.add(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.allow) : Permissions.defaultBit)
.remove(overwrites.member?.deny ?? Permissions.defaultBit)
.add(overwrites.member?.allow ?? Permissions.defaultBit)
.freeze();
}
@@ -223,22 +222,22 @@ class GuildChannel extends Channel {
* 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>}
* @returns {Readonly<Permissions>}
* @private
*/
rolePermissions(role, checkAdmin) {
if (checkAdmin && role.permissions.has(PermissionFlagsBits.Administrator)) {
return new PermissionsBitField(PermissionsBitField.All).freeze();
if (checkAdmin && role.permissions.has(Permissions.FLAGS.ADMINISTRATOR)) {
return new Permissions(Permissions.ALL).freeze();
}
const everyoneOverwrites = this.permissionOverwrites.cache.get(this.guild.id);
const roleOverwrites = this.permissionOverwrites.cache.get(role.id);
return role.permissions
.remove(everyoneOverwrites?.deny ?? PermissionsBitField.defaultBit)
.add(everyoneOverwrites?.allow ?? PermissionsBitField.defaultBit)
.remove(roleOverwrites?.deny ?? PermissionsBitField.defaultBit)
.add(roleOverwrites?.allow ?? PermissionsBitField.defaultBit)
.remove(everyoneOverwrites?.deny ?? Permissions.defaultBit)
.add(everyoneOverwrites?.allow ?? Permissions.defaultBit)
.remove(roleOverwrites?.deny ?? Permissions.defaultBit)
.add(roleOverwrites?.allow ?? Permissions.defaultBit)
.freeze();
}
@@ -260,9 +259,30 @@ class GuildChannel extends Channel {
* @readonly
*/
get members() {
return this.guild.members.cache.filter(m => this.permissionsFor(m).has(PermissionFlagsBits.ViewChannel, false));
return this.guild.members.cache.filter(m => this.permissionsFor(m).has(Permissions.FLAGS.VIEW_CHANNEL, false));
}
/**
* The data for a guild channel.
* @typedef {Object} ChannelData
* @property {string} [name] The name of the channel
* @property {ChannelType} [type] The type of the channel (only conversion between text and news is supported)
* @property {number} [position] The position of the channel
* @property {string} [topic] The topic of the text channel
* @property {boolean} [nsfw] Whether the channel is NSFW
* @property {number} [bitrate] The bitrate of the voice channel
* @property {number} [userLimit] The user limit of the voice channel
* @property {?CategoryChannelResolvable} [parent] The parent of the channel
* @property {boolean} [lockPermissions]
* Lock the permissions of the channel to what the parent's permissions are
* @property {OverwriteResolvable[]|Collection<Snowflake, OverwriteResolvable>} [permissionOverwrites]
* Permission overwrites for the channel
* @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the channel in seconds
* @property {ThreadAutoArchiveDuration} [defaultAutoArchiveDuration]
* The default auto archive duration for all new threads in this channel
* @property {?string} [rtcRegion] The RTC region of the channel
*/
/**
* Edits the channel.
* @param {ChannelData} data The new data for the channel
@@ -274,8 +294,64 @@ class GuildChannel extends Channel {
* .then(console.log)
* .catch(console.error);
*/
edit(data, reason) {
return this.guild.channels.edit(this, data, reason);
async edit(data, reason) {
data.parent &&= this.client.channels.resolveId(data.parent);
if (typeof data.position !== 'undefined') {
const updatedChannels = await Util.setPosition(
this,
data.position,
false,
this.guild._sortedChannels(this),
this.client.api.guilds(this.guild.id).channels,
reason,
);
this.client.actions.GuildChannelsPositionUpdate.handle({
guild_id: this.guild.id,
channels: updatedChannels,
});
}
let permission_overwrites;
if (data.permissionOverwrites) {
permission_overwrites = data.permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild));
}
if (data.lockPermissions) {
if (data.parent) {
const newParent = this.guild.channels.resolve(data.parent);
if (newParent?.type === 'GUILD_CATEGORY') {
permission_overwrites = newParent.permissionOverwrites.cache.map(o =>
PermissionOverwrites.resolve(o, this.guild),
);
}
} else if (this.parent) {
permission_overwrites = this.parent.permissionOverwrites.cache.map(o =>
PermissionOverwrites.resolve(o, this.guild),
);
}
}
const newData = await this.client.api.channels(this.id).patch({
data: {
name: (data.name ?? this.name).trim(),
type: ChannelTypes[data.type],
topic: data.topic,
nsfw: data.nsfw,
bitrate: data.bitrate ?? this.bitrate,
user_limit: data.userLimit ?? this.userLimit,
rtc_region: data.rtcRegion ?? this.rtcRegion,
parent_id: data.parent,
lock_permissions: data.lockPermissions,
rate_limit_per_user: data.rateLimitPerUser,
default_auto_archive_duration: data.defaultAutoArchiveDuration,
permission_overwrites,
},
reason,
});
return this.client.actions.ChannelUpdate.handle(newData).updated;
}
/**
@@ -339,10 +415,30 @@ class GuildChannel extends Channel {
* .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);
async setPosition(position, { relative, reason } = {}) {
const updatedChannels = await Util.setPosition(
this,
position,
relative,
this.guild._sortedChannels(this),
this.client.api.guilds(this.guild.id).channels,
reason,
);
this.client.actions.GuildChannelsPositionUpdate.handle({
guild_id: this.guild.id,
channels: updatedChannels,
});
return this;
}
/**
* Data that can be resolved to an Application. This can be:
* * An Application
* * An Activity with associated Application
* * A Snowflake
* @typedef {Application|Snowflake} ApplicationResolvable
*/
/**
* Options used to clone a guild channel.
* @typedef {GuildChannelCreateOptions} GuildChannelCloneOptions
@@ -416,12 +512,12 @@ class GuildChannel extends Channel {
if (!permissions) return false;
// This flag allows managing even if timed out
if (permissions.has(PermissionFlagsBits.Administrator, false)) return true;
if (permissions.has(Permissions.FLAGS.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;
? Permissions.FLAGS.MANAGE_CHANNELS | Permissions.FLAGS.CONNECT
: Permissions.FLAGS.VIEW_CHANNEL | Permissions.FLAGS.MANAGE_CHANNELS;
return permissions.has(bitfield, false);
}
@@ -434,7 +530,7 @@ class GuildChannel extends Channel {
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);
return permissions.has(Permissions.FLAGS.VIEW_CHANNEL, false);
}
/**
@@ -448,7 +544,7 @@ class GuildChannel extends Channel {
* .catch(console.error);
*/
async delete(reason) {
await this.guild.channels.delete(this.id, reason);
await this.client.api.channels(this.id).delete({ reason });
return this;
}
}

View File

@@ -1,9 +1,9 @@
'use strict';
const { PermissionFlagsBits } = require('discord-api-types/v9');
const BaseGuildEmoji = require('./BaseGuildEmoji');
const { Error } = require('../errors');
const GuildEmojiRoleManager = require('../managers/GuildEmojiRoleManager');
const Permissions = require('../util/Permissions');
/**
* Represents a custom emoji.
@@ -56,7 +56,7 @@ class GuildEmoji extends BaseGuildEmoji {
*/
get deletable() {
if (!this.guild.me) throw new Error('GUILD_UNCACHED_ME');
return !this.managed && this.guild.me.permissions.has(PermissionFlagsBits.ManageEmojisAndStickers);
return !this.managed && this.guild.me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS_AND_STICKERS);
}
/**
@@ -72,8 +72,18 @@ class GuildEmoji extends BaseGuildEmoji {
* Fetches the author for this emoji
* @returns {Promise<User>}
*/
fetchAuthor() {
return this.guild.emojis.fetchAuthor(this);
async fetchAuthor() {
if (this.managed) {
throw new Error('EMOJI_MANAGED');
} else {
if (!this.guild.me) throw new Error('GUILD_UNCACHED_ME');
if (!this.guild.me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS_AND_STICKERS)) {
throw new Error('MISSING_MANAGE_EMOJIS_AND_STICKERS_PERMISSION', this.guild);
}
}
const data = await this.client.api.guilds(this.guild.id).emojis(this.id).get();
this._patch(data);
return this.author;
}
/**
@@ -94,8 +104,21 @@ class GuildEmoji extends BaseGuildEmoji {
* .then(e => console.log(`Edited emoji ${e}`))
* .catch(console.error);
*/
edit(data, reason) {
return this.guild.emojis.edit(this.id, data, reason);
async edit(data, reason) {
const roles = data.roles?.map(r => r.id ?? r);
const newData = await this.client.api
.guilds(this.guild.id)
.emojis(this.id)
.patch({
data: {
name: data.name,
roles,
},
reason,
});
const clone = this._clone();
clone._patch(newData);
return clone;
}
/**
@@ -114,7 +137,7 @@ class GuildEmoji extends BaseGuildEmoji {
* @returns {Promise<GuildEmoji>}
*/
async delete(reason) {
await this.guild.emojis.delete(this.id, reason);
await this.client.api.guilds(this.guild.id).emojis(this.id).delete({ reason });
return this;
}

View File

@@ -1,12 +1,20 @@
'use strict';
const { PermissionFlagsBits } = require('discord-api-types/v9');
const process = require('node:process');
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');
const Permissions = require('../util/Permissions');
/**
* @type {WeakSet<GuildMember>}
* @private
* @internal
*/
const deletedGuildMembers = new WeakSet();
let deprecationEmittedForDeleted = false;
/**
* Represents a member of a guild on Discord.
@@ -43,9 +51,9 @@ class GuildMember extends Base {
/**
* Whether this member has yet to pass the guild's membership gate
* @type {?boolean}
* @type {boolean}
*/
this.pending = null;
this.pending = false;
/**
* The timestamp this member's timeout will be removed
@@ -76,18 +84,12 @@ class GuildMember extends Base {
} else if (typeof this.avatar !== 'string') {
this.avatar = null;
}
if ('joined_at' in data) this.joinedTimestamp = Date.parse(data.joined_at);
if ('joined_at' in data) this.joinedTimestamp = new Date(data.joined_at).getTime();
if ('premium_since' in data) {
this.premiumSinceTimestamp = data.premium_since ? Date.parse(data.premium_since) : null;
this.premiumSinceTimestamp = data.premium_since ? new Date(data.premium_since).getTime() : 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;
}
this.pending = data.pending ?? false;
if ('communication_disabled_until' in data) {
this.communicationDisabledUntilTimestamp =
@@ -101,6 +103,36 @@ class GuildMember extends Base {
return clone;
}
/**
* Whether or not the structure has been deleted
* @type {boolean}
* @deprecated This will be removed in the next major version, see https://github.com/discordjs/discord.js/issues/7091
*/
get deleted() {
if (!deprecationEmittedForDeleted) {
deprecationEmittedForDeleted = true;
process.emitWarning(
'GuildMember#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.',
'DeprecationWarning',
);
}
return deletedGuildMembers.has(this);
}
set deleted(value) {
if (!deprecationEmittedForDeleted) {
deprecationEmittedForDeleted = true;
process.emitWarning(
'GuildMember#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.',
'DeprecationWarning',
);
}
if (value) deletedGuildMembers.add(this);
else deletedGuildMembers.delete(this);
}
/**
* Whether this GuildMember is a partial
* @type {boolean}
@@ -130,11 +162,12 @@ class GuildMember extends Base {
/**
* A link to the member's guild avatar.
* @param {ImageURLOptions} [options={}] Options for the image URL
* @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);
avatarURL({ format, size, dynamic } = {}) {
if (!this.avatar) return null;
return this.client.rest.cdn.GuildMemberAvatar(this.guild.id, this.id, this.avatar, format, size, dynamic);
}
/**
@@ -153,7 +186,7 @@ class GuildMember extends Base {
* @readonly
*/
get joinedAt() {
return this.joinedTimestamp && new Date(this.joinedTimestamp);
return this.joinedTimestamp ? new Date(this.joinedTimestamp) : null;
}
/**
@@ -171,7 +204,7 @@ class GuildMember extends Base {
* @readonly
*/
get premiumSince() {
return this.premiumSinceTimestamp && new Date(this.premiumSinceTimestamp);
return this.premiumSinceTimestamp ? new Date(this.premiumSinceTimestamp) : null;
}
/**
@@ -221,12 +254,12 @@ class GuildMember extends Base {
/**
* The overall set of permissions for this member, taking only roles and owner status into account
* @type {Readonly<PermissionsBitField>}
* @type {Readonly<Permissions>}
* @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();
if (this.user.id === this.guild.ownerId) return new Permissions(Permissions.ALL).freeze();
return new Permissions(this.roles.cache.map(role => role.permissions)).freeze();
}
/**
@@ -249,8 +282,7 @@ class GuildMember extends Base {
* @readonly
*/
get kickable() {
if (!this.guild.me) throw new Error('GUILD_UNCACHED_ME');
return this.manageable && this.guild.me.permissions.has(PermissionFlagsBits.KickMembers);
return this.manageable && this.guild.me.permissions.has(Permissions.FLAGS.KICK_MEMBERS);
}
/**
@@ -259,8 +291,7 @@ class GuildMember extends Base {
* @readonly
*/
get bannable() {
if (!this.guild.me) throw new Error('GUILD_UNCACHED_ME');
return this.manageable && this.guild.me.permissions.has(PermissionFlagsBits.BanMembers);
return this.manageable && this.guild.me.permissions.has(Permissions.FLAGS.BAN_MEMBERS);
}
/**
@@ -269,11 +300,7 @@ class GuildMember extends Base {
* @readonly
*/
get moderatable() {
return (
!this.permissions.has(PermissionFlagsBits.Administrator) &&
this.manageable &&
(this.guild.me?.permissions.has(PermissionFlagsBits.ModerateMembers) ?? false)
);
return this.manageable && (this.guild.me?.permissions.has(Permissions.FLAGS.MODERATE_MEMBERS) ?? false);
}
/**
@@ -288,7 +315,7 @@ class GuildMember extends Base {
* 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>}
* @returns {Readonly<Permissions>}
*/
permissionsIn(channel) {
channel = this.guild.channels.resolve(channel);
@@ -348,7 +375,7 @@ class GuildMember extends Base {
* @returns {Promise<GuildMember>}
* @example
* // ban a guild member
* guildMember.ban({ deleteMessageDays: 7, reason: 'They deserved it' })
* guildMember.ban({ days: 7, reason: 'They deserved it' })
* .then(console.log)
* .catch(console.error);
*/
@@ -451,6 +478,7 @@ class GuildMember extends Base {
TextBasedChannel.applyToClass(GuildMember);
exports.GuildMember = GuildMember;
exports.deletedGuildMembers = deletedGuildMembers;
/**
* @external APIGuildMember

View File

@@ -1,11 +1,9 @@
'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');
const SnowflakeUtil = require('../util/SnowflakeUtil');
/**
* Represents the data about the guild any bot can preview, connected to the specified guild.
@@ -62,7 +60,7 @@ class GuildPreview extends Base {
if ('features' in data) {
/**
* An array of enabled guild features
* @type {GuildFeature[]}
* @type {Features[]}
*/
this.features = data.features;
}
@@ -105,24 +103,14 @@ class GuildPreview extends Base {
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);
return SnowflakeUtil.timestampFrom(this.id);
}
/**
@@ -136,29 +124,29 @@ class GuildPreview extends Base {
/**
* The URL to this guild's splash.
* @param {ImageURLOptions} [options={}] Options for the image URL
* @param {StaticImageURLOptions} [options={}] Options for the Image URL
* @returns {?string}
*/
splashURL(options = {}) {
return this.splash && this.client.rest.cdn.splash(this.id, this.splash, options);
splashURL({ format, size } = {}) {
return this.splash && this.client.rest.cdn.Splash(this.id, this.splash, format, size);
}
/**
* The URL to this guild's discovery splash.
* @param {ImageURLOptions} [options={}] Options for the image URL
* @param {StaticImageURLOptions} [options={}] Options for the Image URL
* @returns {?string}
*/
discoverySplashURL(options = {}) {
return this.discoverySplash && this.client.rest.cdn.discoverySplash(this.id, this.discoverySplash, options);
discoverySplashURL({ format, size } = {}) {
return this.discoverySplash && this.client.rest.cdn.DiscoverySplash(this.id, this.discoverySplash, format, size);
}
/**
* The URL to this guild's icon.
* @param {ImageURLOptions} [options={}] Options for the image URL
* @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);
iconURL({ format, size, dynamic } = {}) {
return this.icon && this.client.rest.cdn.Icon(this.id, this.icon, format, size, dynamic);
}
/**

View File

@@ -1,9 +1,14 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { GuildScheduledEventStatus, GuildScheduledEventEntityType, RouteBases } = require('discord-api-types/v9');
const Base = require('./Base');
const { Error } = require('../errors');
const {
GuildScheduledEventEntityTypes,
GuildScheduledEventStatuses,
GuildScheduledEventPrivacyLevels,
Endpoints,
} = require('../util/Constants');
const SnowflakeUtil = require('../util/SnowflakeUtil');
/**
* Represents a scheduled event in a {@link Guild}.
@@ -31,8 +36,7 @@ class GuildScheduledEvent extends Base {
_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}
* The channel id in which the scheduled event will be hosted, or `null` if entity type is `EXTERNAL`
* @type {?Snowflake}
*/
this.channelId = data.channel_id;
@@ -82,21 +86,21 @@ class GuildScheduledEvent extends Base {
/**
* The privacy level of the guild scheduled event
* @type {GuildScheduledEventPrivacyLevel}
* @type {PrivacyLevel}
*/
this.privacyLevel = data.privacy_level;
this.privacyLevel = GuildScheduledEventPrivacyLevels[data.privacy_level];
/**
* The status of the guild scheduled event
* @type {GuildScheduledEventStatus}
*/
this.status = data.status;
this.status = GuildScheduledEventStatuses[data.status];
/**
* The type of hosting entity associated with the scheduled event
* @type {GuildScheduledEventEntityType}
*/
this.entityType = data.entity_type;
this.entityType = GuildScheduledEventEntityTypes[data.entity_type];
if ('entity_id' in data) {
/**
@@ -152,21 +156,6 @@ class GuildScheduledEvent extends Base {
} 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);
}
/**
@@ -175,7 +164,7 @@ class GuildScheduledEvent extends Base {
* @readonly
*/
get createdTimestamp() {
return DiscordSnowflake.timestampFrom(this.id);
return SnowflakeUtil.timestampFrom(this.id);
}
/**
@@ -230,15 +219,14 @@ class GuildScheduledEvent extends Base {
* @readonly
*/
get url() {
return `${RouteBases.scheduledEvent}/${this.guildId}/${this.id}`;
return Endpoints.scheduledEvent(this.client.options.http.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>
* <warn>This is required when the `entityType` of `GuildScheduledEvent` is `EXTERNAL`, gets ignored otherwise</warn>
*/
/**
@@ -248,13 +236,13 @@ class GuildScheduledEvent extends Base {
*/
async createInviteURL(options) {
let channelId = this.channelId;
if (this.entityType === GuildScheduledEventEntityType.External) {
if (this.entityType === '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}`;
return Endpoints.invite(this.client.options.http.invite, invite.code, this.id);
}
/**
@@ -355,7 +343,7 @@ class GuildScheduledEvent extends Base {
* @returns {Promise<GuildScheduledEvent>}
* @example
* // Set status of a guild scheduled event
* guildScheduledEvent.setStatus(GuildScheduledEventStatus.Active)
* guildScheduledEvent.setStatus('ACTIVE')
* .then(guildScheduledEvent => console.log(`Set the status to: ${guildScheduledEvent.status}`))
* .catch(console.error);
*/
@@ -399,35 +387,35 @@ class GuildScheduledEvent extends Base {
}
/**
* Indicates whether this guild scheduled event has an {@link GuildScheduledEventStatus.Active} status.
* Indicates whether this guild scheduled event has an `ACTIVE` status.
* @returns {boolean}
*/
isActive() {
return this.status === GuildScheduledEventStatus.Active;
return GuildScheduledEventStatuses[this.status] === GuildScheduledEventStatuses.ACTIVE;
}
/**
* Indicates whether this guild scheduled event has a {@link GuildScheduledEventStatus.Canceled} status.
* Indicates whether this guild scheduled event has a `CANCELED` status.
* @returns {boolean}
*/
isCanceled() {
return this.status === GuildScheduledEventStatus.Canceled;
return GuildScheduledEventStatuses[this.status] === GuildScheduledEventStatuses.CANCELED;
}
/**
* Indicates whether this guild scheduled event has a {@link GuildScheduledEventStatus.Completed} status.
* Indicates whether this guild scheduled event has a `COMPLETED` status.
* @returns {boolean}
*/
isCompleted() {
return this.status === GuildScheduledEventStatus.Completed;
return GuildScheduledEventStatuses[this.status] === GuildScheduledEventStatuses.COMPLETED;
}
/**
* Indicates whether this guild scheduled event has a {@link GuildScheduledEventStatus.Scheduled} status.
* Indicates whether this guild scheduled event has a `SCHEDULED` status.
* @returns {boolean}
*/
isScheduled() {
return this.status === GuildScheduledEventStatus.Scheduled;
return GuildScheduledEventStatuses[this.status] === GuildScheduledEventStatuses.SCHEDULED;
}
}

View File

@@ -1,10 +1,9 @@
'use strict';
const { setTimeout, clearTimeout } = require('node:timers');
const { RouteBases, Routes } = require('discord-api-types/v9');
const { setTimeout } = require('node:timers');
const Base = require('./Base');
const { Events } = require('../util/Constants');
const DataResolver = require('../util/DataResolver');
const Events = require('../util/Events');
/**
* Represents the template for a guild.
@@ -67,18 +66,18 @@ class GuildTemplate extends Base {
if ('created_at' in data) {
/**
* The timestamp of when this template was created at
* @type {number}
* The time when this template was created at
* @type {Date}
*/
this.createdTimestamp = Date.parse(data.created_at);
this.createdAt = new Date(data.created_at);
}
if ('updated_at' in data) {
/**
* The timestamp of when this template was last synced to the guild
* @type {number}
* The time when this template was last synced to the guild
* @type {Date}
*/
this.updatedTimestamp = Date.parse(data.updated_at);
this.updatedAt = new Date(data.updated_at);
}
if ('source_guild_id' in data) {
@@ -115,8 +114,8 @@ class GuildTemplate extends Base {
*/
async createGuild(name, icon) {
const { client } = this;
const data = await client.rest.post(Routes.template(this.code), {
body: {
const data = await client.api.guilds.templates(this.code).post({
data: {
name,
icon: await DataResolver.resolveImage(icon),
},
@@ -126,7 +125,7 @@ class GuildTemplate extends Base {
return new Promise(resolve => {
const resolveGuild = guild => {
client.off(Events.GuildCreate, handleGuild);
client.off(Events.GUILD_CREATE, handleGuild);
client.decrementMaxListeners();
resolve(guild);
};
@@ -139,7 +138,7 @@ class GuildTemplate extends Base {
};
client.incrementMaxListeners();
client.on(Events.GuildCreate, handleGuild);
client.on(Events.GUILD_CREATE, handleGuild);
const timeout = setTimeout(() => resolveGuild(client.guilds._add(data)), 10_000).unref();
});
@@ -158,7 +157,7 @@ class GuildTemplate extends Base {
* @returns {Promise<GuildTemplate>}
*/
async edit({ name, description } = {}) {
const data = await this.client.api.guilds(this.guildId).templates(this.code).patch({ body: { name, description } });
const data = await this.client.api.guilds(this.guildId).templates(this.code).patch({ data: { name, description } });
return this._patch(data);
}
@@ -181,21 +180,21 @@ class GuildTemplate extends Base {
}
/**
* The time when this template was created at
* @type {Date}
* The timestamp of when this template was created at
* @type {number}
* @readonly
*/
get createdAt() {
return new Date(this.createdTimestamp);
get createdTimestamp() {
return this.createdAt.getTime();
}
/**
* The time when this template was last synced to the guild
* @type {Date}
* The timestamp of when this template was last synced to the guild
* @type {number}
* @readonly
*/
get updatedAt() {
return new Date(this.updatedTimestamp);
get updatedTimestamp() {
return this.updatedAt.getTime();
}
/**
@@ -213,7 +212,7 @@ class GuildTemplate extends Base {
* @readonly
*/
get url() {
return `${RouteBases.template}/${this.code}`;
return `${this.client.options.http.template}/${this.code}`;
}
/**

View File

@@ -1,6 +1,5 @@
'use strict';
const { Routes } = require('discord-api-types/v9');
const Base = require('./Base');
const IntegrationApplication = require('./IntegrationApplication');
@@ -56,21 +55,17 @@ class Integration extends Base {
*/
this.enabled = data.enabled;
if ('syncing' in data) {
/**
* Whether this integration is syncing
* @type {?boolean}
*/
this.syncing = data.syncing;
} else {
this.syncing ??= null;
}
/**
* Whether this integration is syncing
* @type {?boolean}
*/
this.syncing = data.syncing;
/**
* The role that this integration uses for subscribers
* @type {?Role}
*/
this.role = this.guild.roles.resolve(data.role_id);
this.role = this.guild.roles.cache.get(data.role_id);
if ('enable_emoticons' in data) {
/**
@@ -89,7 +84,7 @@ class Integration extends Base {
*/
this.user = this.client.users._add(data.user);
} else {
this.user ??= null;
this.user = null;
}
/**
@@ -98,15 +93,11 @@ class Integration extends Base {
*/
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;
}
/**
* The last time this integration was last synced
* @type {?number}
*/
this.syncedAt = data.synced_at;
if ('subscriber_count' in data) {
/**
@@ -131,15 +122,6 @@ class Integration extends Base {
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>}
@@ -154,21 +136,17 @@ class Integration extends Base {
if ('expire_behavior' in data) {
/**
* The behavior of expiring subscribers
* @type {?IntegrationExpireBehavior}
* @type {?number}
*/
this.expireBehavior = data.expire_behavior;
} else {
this.expireBehavior ??= null;
}
if ('expire_grace_period' in data) {
/**
* The grace period (in days) before expiring subscribers
* The grace period before expiring subscribers
* @type {?number}
*/
this.expireGracePeriod = data.expire_grace_period;
} else {
this.expireGracePeriod ??= null;
}
if ('application' in data) {

View File

@@ -1,9 +1,9 @@
'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');
const { InteractionTypes, MessageComponentTypes, ApplicationCommandTypes } = require('../util/Constants');
const Permissions = require('../util/Permissions');
const SnowflakeUtil = require('../util/SnowflakeUtil');
/**
* Represents an interaction.
@@ -17,7 +17,7 @@ class Interaction extends Base {
* The interaction's type
* @type {InteractionType}
*/
this.type = data.type;
this.type = InteractionTypes[data.type];
/**
* The interaction's id
@@ -71,16 +71,14 @@ class Interaction extends Base {
/**
* The permissions of the member, if one exists, in the channel this interaction was executed in
* @type {?Readonly<PermissionsBitField>}
* @type {?Readonly<Permissions>}
*/
this.memberPermissions = data.member?.permissions
? new PermissionsBitField(data.member.permissions).freeze()
: null;
this.memberPermissions = data.member?.permissions ? new Permissions(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}
* @see {@link https://discord.com/developers/docs/dispatch/field-values#predefined-field-values-accepted-locales}
*/
this.locale = data.locale;
@@ -97,7 +95,7 @@ class Interaction extends Base {
* @readonly
*/
get createdTimestamp() {
return DiscordSnowflake.timestampFrom(this.id);
return SnowflakeUtil.timestampFrom(this.id);
}
/**
@@ -151,44 +149,44 @@ class Interaction extends Base {
return Boolean(this.guildId && !this.guild && this.member);
}
/**
* Indicates whether this interaction is a {@link BaseCommandInteraction}.
* @returns {boolean}
*/
isApplicationCommand() {
return InteractionTypes[this.type] === InteractionTypes.APPLICATION_COMMAND;
}
/**
* Indicates whether this interaction is a {@link CommandInteraction}.
* @returns {boolean}
*/
isCommand() {
return this.type === InteractionType.ApplicationCommand;
return InteractionTypes[this.type] === InteractionTypes.APPLICATION_COMMAND && typeof this.targetId === 'undefined';
}
/**
* Indicates whether this interaction is a {@link ChatInputCommandInteraction}.
* Indicates whether this interaction is a {@link ContextMenuInteraction}
* @returns {boolean}
*/
isChatInputCommand() {
return this.isCommand() && this.commandType === ApplicationCommandType.ChatInput;
isContextMenu() {
return InteractionTypes[this.type] === InteractionTypes.APPLICATION_COMMAND && typeof this.targetId !== 'undefined';
}
/**
* Indicates whether this interaction is a {@link ContextMenuCommandInteraction}
* Indicates whether this interaction is a {@link UserContextMenuInteraction}
* @returns {boolean}
*/
isContextMenuCommand() {
return this.isCommand() && [ApplicationCommandType.User, ApplicationCommandType.Message].includes(this.commandType);
isUserContextMenu() {
return this.isContextMenu() && ApplicationCommandTypes[this.targetType] === ApplicationCommandTypes.USER;
}
/**
* Indicates whether this interaction is a {@link UserContextMenuCommandInteraction}
* Indicates whether this interaction is a {@link MessageContextMenuInteraction}
* @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;
isMessageContextMenu() {
return this.isContextMenu() && ApplicationCommandTypes[this.targetType] === ApplicationCommandTypes.MESSAGE;
}
/**
@@ -196,7 +194,7 @@ class Interaction extends Base {
* @returns {boolean}
*/
isAutocomplete() {
return this.type === InteractionType.ApplicationCommandAutocomplete;
return InteractionTypes[this.type] === InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE;
}
/**
@@ -204,7 +202,7 @@ class Interaction extends Base {
* @returns {boolean}
*/
isMessageComponent() {
return this.type === InteractionType.MessageComponent;
return InteractionTypes[this.type] === InteractionTypes.MESSAGE_COMPONENT;
}
/**
@@ -212,7 +210,10 @@ class Interaction extends Base {
* @returns {boolean}
*/
isButton() {
return this.isMessageComponent() && this.componentType === ComponentType.Button;
return (
InteractionTypes[this.type] === InteractionTypes.MESSAGE_COMPONENT &&
MessageComponentTypes[this.componentType] === MessageComponentTypes.BUTTON
);
}
/**
@@ -220,15 +221,10 @@ class Interaction extends Base {
* @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);
return (
InteractionTypes[this.type] === InteractionTypes.MESSAGE_COMPONENT &&
MessageComponentTypes[this.componentType] === MessageComponentTypes.SELECT_MENU
);
}
}

View File

@@ -2,13 +2,14 @@
const { Collection } = require('@discordjs/collection');
const Collector = require('./interfaces/Collector');
const Events = require('../util/Events');
const { Events } = require('../util/Constants');
const { InteractionTypes, MessageComponentTypes } = require('../util/Constants');
/**
* @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 {TextBasedChannels} [channel] The channel to listen to interactions from
* @property {MessageComponentType} [componentType] The type of component to listen for
* @property {Guild} [guild] The guild to listen to interactions from
* @property {InteractionType} [interactionType] The type of interaction to listen for
* @property {number} [max] The maximum total amount of interactions to collect
* @property {number} [maxComponents] The maximum number of components to collect
@@ -63,13 +64,19 @@ class InteractionCollector extends Collector {
* The type of interaction to collect
* @type {?InteractionType}
*/
this.interactionType = options.interactionType ?? null;
this.interactionType =
typeof options.interactionType === 'number'
? InteractionTypes[options.interactionType]
: options.interactionType ?? null;
/**
* The type of component to collect
* @type {?ComponentType}
* @type {?MessageComponentType}
*/
this.componentType = options.componentType ?? null;
this.componentType =
typeof options.componentType === 'number'
? MessageComponentTypes[options.componentType]
: options.componentType ?? null;
/**
* The users that have interacted with this collector
@@ -92,31 +99,31 @@ class InteractionCollector extends Collector {
if (this.messageId) {
this._handleMessageDeletion = this._handleMessageDeletion.bind(this);
this.client.on(Events.MessageDelete, this._handleMessageDeletion);
this.client.on(Events.MessageBulkDelete, bulkDeleteListener);
this.client.on(Events.MESSAGE_DELETE, this._handleMessageDeletion);
this.client.on(Events.MESSAGE_BULK_DELETE, 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);
this.client.on(Events.CHANNEL_DELETE, this._handleChannelDeletion);
this.client.on(Events.THREAD_DELETE, this._handleThreadDeletion);
}
if (this.guildId) {
this._handleGuildDeletion = this._handleGuildDeletion.bind(this);
this.client.on(Events.GuildDelete, this._handleGuildDeletion);
this.client.on(Events.GUILD_DELETE, this._handleGuildDeletion);
}
this.client.on(Events.InteractionCreate, this.handleCollect);
this.client.on(Events.INTERACTION_CREATE, 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.removeListener(Events.INTERACTION_CREATE, this.handleCollect);
this.client.removeListener(Events.MESSAGE_DELETE, this._handleMessageDeletion);
this.client.removeListener(Events.MESSAGE_BULK_DELETE, bulkDeleteListener);
this.client.removeListener(Events.CHANNEL_DELETE, this._handleChannelDeletion);
this.client.removeListener(Events.THREAD_DELETE, this._handleThreadDeletion);
this.client.removeListener(Events.GUILD_DELETE, this._handleGuildDeletion);
this.client.decrementMaxListeners();
});

View File

@@ -1,11 +1,14 @@
'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');
const { Endpoints } = require('../util/Constants');
const Permissions = require('../util/Permissions');
// TODO: Convert `inviter` and `channel` in this class to a getter.
/**
* Represents an invitation to a guild channel.
@@ -112,13 +115,20 @@ class Invite extends Base {
* @type {?Snowflake}
*/
this.inviterId = data.inviter_id;
this.inviter = this.client.users.resolve(data.inviter_id);
} else {
this.inviterId ??= null;
}
if ('inviter' in data) {
this.client.users._add(data.inviter);
/**
* The user who created this invite
* @type {?User}
*/
this.inviter ??= this.client.users._add(data.inviter);
this.inviterId = data.inviter.id;
} else {
this.inviter ??= null;
}
if ('target_user' in data) {
@@ -141,10 +151,18 @@ class Invite extends Base {
this.targetApplication ??= null;
}
/**
* The type of the invite target:
* * 1: STREAM
* * 2: EMBEDDED_APPLICATION
* @typedef {number} TargetType
* @see {@link https://discord.com/developers/docs/resources/invite#invite-object-invite-target-types}
*/
if ('target_type' in data) {
/**
* The target type
* @type {?InviteTargetType}
* @type {?TargetType}
*/
this.targetType = data.target_type;
} else {
@@ -153,21 +171,19 @@ class Invite extends Base {
if ('channel_id' in data) {
/**
* The id of the channel this invite is for
* @type {?Snowflake}
* The channel's id this invite is for
* @type {Snowflake}
*/
this.channelId = data.channel_id;
this.channel = this.client.channels.cache.get(data.channel_id);
}
if ('channel' in data) {
/**
* The channel this invite is for
* @type {?Channel}
* @type {Channel}
*/
this.channel =
this.client.channels._add(data.channel, this.guild, { cache: false }) ??
this.client.channels.resolve(this.channelId);
this.channel ??= this.client.channels._add(data.channel, this.guild, { cache: false });
this.channelId ??= data.channel.id;
}
@@ -176,19 +192,18 @@ class Invite extends Base {
* The timestamp this invite was created at
* @type {?number}
*/
this.createdTimestamp = Date.parse(data.created_at);
this.createdTimestamp = new Date(data.created_at).getTime();
} else {
this.createdTimestamp ??= null;
}
if ('expires_at' in data) this._expiresTimestamp = Date.parse(data.expires_at);
if ('expires_at' in data) this._expiresTimestamp = new Date(data.expires_at).getTime();
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 {
@@ -212,7 +227,7 @@ class Invite extends Base {
* @readonly
*/
get createdAt() {
return this.createdTimestamp && new Date(this.createdTimestamp);
return this.createdTimestamp ? new Date(this.createdTimestamp) : null;
}
/**
@@ -224,9 +239,9 @@ class Invite extends Base {
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),
return (
this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_CHANNELS, false) ||
guild.me.permissions.has(Permissions.FLAGS.MANAGE_GUILD)
);
}
@@ -248,16 +263,8 @@ class Invite extends Base {
* @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);
const { expiresTimestamp } = this;
return expiresTimestamp ? new Date(expiresTimestamp) : null;
}
/**
@@ -266,7 +273,7 @@ class Invite extends Base {
* @readonly
*/
get url() {
return `${RouteBases.invite}/${this.code}`;
return Endpoints.invite(this.client.options.http.invite, this.code);
}
/**

View File

@@ -6,7 +6,6 @@ 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) {

View File

@@ -1,30 +1,33 @@
'use strict';
const { createComponent, Embed } = require('@discordjs/builders');
const process = require('node:process');
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 BaseMessageComponent = require('./BaseMessageComponent');
const ClientApplication = require('./ClientApplication');
const InteractionCollector = require('./InteractionCollector');
const MessageAttachment = require('./MessageAttachment');
const Embed = require('./MessageEmbed');
const Mentions = require('./MessageMentions');
const MessagePayload = require('./MessagePayload');
const ReactionCollector = require('./ReactionCollector');
const { Sticker } = require('./Sticker');
const { Error } = require('../errors');
const ReactionManager = require('../managers/ReactionManager');
const { NonSystemMessageTypes } = require('../util/Constants');
const MessageFlagsBitField = require('../util/MessageFlagsBitField');
const PermissionsBitField = require('../util/PermissionsBitField');
const { InteractionTypes, MessageTypes, SystemMessageTypes } = require('../util/Constants');
const MessageFlags = require('../util/MessageFlags');
const Permissions = require('../util/Permissions');
const SnowflakeUtil = require('../util/SnowflakeUtil');
const Util = require('../util/Util');
/**
* @type {WeakSet<Message>}
* @private
* @internal
*/
const deletedMessages = new WeakSet();
let deprecationEmittedForDeleted = false;
/**
* Represents a message on Discord.
* @extends {Base}
@@ -59,20 +62,20 @@ class Message extends Base {
* The timestamp the message was sent at
* @type {number}
*/
this.createdTimestamp = DiscordSnowflake.timestampFrom(this.id);
this.createdTimestamp = SnowflakeUtil.timestampFrom(this.id);
if ('type' in data) {
/**
* The type of the message
* @type {?MessageType}
*/
this.type = data.type;
this.type = MessageTypes[data.type];
/**
* Whether or not this message was sent by Discord, not actually a user (e.g. pin notifications)
* @type {?boolean}
*/
this.system = !NonSystemMessageTypes.includes(this.type);
this.system = SystemMessageTypes.includes(this.type);
} else {
this.system ??= null;
this.type ??= null;
@@ -133,9 +136,9 @@ class Message extends Base {
if ('embeds' in data) {
/**
* A list of embeds in the message - e.g. YouTube Player
* @type {Embed[]}
* @type {MessageEmbed[]}
*/
this.embeds = data.embeds.map(e => new Embed(e));
this.embeds = data.embeds.map(e => new Embed(e, true));
} else {
this.embeds = this.embeds?.slice() ?? [];
}
@@ -143,9 +146,9 @@ class Message extends Base {
if ('components' in data) {
/**
* A list of MessageActionRows in the message
* @type {ActionRow[]}
* @type {MessageActionRow[]}
*/
this.components = data.components.map(c => createComponent(c));
this.components = data.components.map(c => BaseMessageComponent.create(c, this.client));
} else {
this.components = this.components?.slice() ?? [];
}
@@ -183,7 +186,7 @@ class Message extends Base {
* The timestamp the message was last edited at (if applicable)
* @type {?number}
*/
this.editedTimestamp = Date.parse(data.edited_timestamp);
this.editedTimestamp = new Date(data.edited_timestamp).getTime();
} else {
this.editedTimestamp ??= null;
}
@@ -283,21 +286,21 @@ class Message extends Base {
if ('flags' in data) {
/**
* Flags that are applied to the message
* @type {Readonly<MessageFlagsBitField>}
* @type {Readonly<MessageFlags>}
*/
this.flags = new MessageFlagsBitField(data.flags).freeze();
this.flags = new MessageFlags(data.flags).freeze();
} else {
this.flags = new MessageFlagsBitField(this.flags).freeze();
this.flags = new MessageFlags(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}
* * Crossposted messages (IS_CROSSPOST {@link MessageFlags.FLAGS message flag})
* * CHANNEL_FOLLOW_ADD
* * CHANNEL_PINNED_MESSAGE
* * REPLY
* * THREAD_STARTER_MESSAGE
* @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
@@ -339,7 +342,7 @@ class Message extends Base {
*/
this.interaction = {
id: data.interaction.id,
type: data.interaction.type,
type: InteractionTypes[data.interaction.type],
commandName: data.interaction.name,
user: this.client.users._add(data.interaction.user),
};
@@ -348,6 +351,36 @@ class Message extends Base {
}
}
/**
* Whether or not the structure has been deleted
* @type {boolean}
* @deprecated This will be removed in the next major version, see https://github.com/discordjs/discord.js/issues/7091
*/
get deleted() {
if (!deprecationEmittedForDeleted) {
deprecationEmittedForDeleted = true;
process.emitWarning(
'Message#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.',
'DeprecationWarning',
);
}
return deletedMessages.has(this);
}
set deleted(value) {
if (!deprecationEmittedForDeleted) {
deprecationEmittedForDeleted = true;
process.emitWarning(
'Message#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.',
'DeprecationWarning',
);
}
if (value) deletedMessages.add(this);
else deletedMessages.delete(this);
}
/**
* The channel that the message was sent in
* @type {TextChannel|DMChannel|NewsChannel|ThreadChannel}
@@ -391,7 +424,7 @@ class Message extends Base {
* @readonly
*/
get editedAt() {
return this.editedTimestamp && new Date(this.editedTimestamp);
return this.editedTimestamp ? new Date(this.editedTimestamp) : null;
}
/**
@@ -409,7 +442,7 @@ class Message extends Base {
* @readonly
*/
get hasThread() {
return this.flags.has(MessageFlags.HasThread);
return this.flags.has(MessageFlags.FLAGS.HAS_THREAD);
}
/**
@@ -488,7 +521,7 @@ class Message extends Base {
/**
* @typedef {CollectorOptions} MessageComponentCollectorOptions
* @property {ComponentType} [componentType] The type of component to listen for
* @property {MessageComponentType} [componentType] The type of component to listen for
* @property {number} [max] The maximum total amount of interactions to collect
* @property {number} [maxComponents] The maximum number of components to collect
* @property {number} [maxUsers] The maximum number of users to interact
@@ -508,7 +541,7 @@ class Message extends Base {
createMessageComponentCollector(options = {}) {
return new InteractionCollector(this.client, {
...options,
interactionType: InteractionType.MessageComponent,
interactionType: InteractionTypes.MESSAGE_COMPONENT,
message: this,
});
}
@@ -518,7 +551,7 @@ class Message extends Base {
* @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
* @property {MessageComponentType} [componentType] The type of component interaction to collect
*/
/**
@@ -551,7 +584,9 @@ class Message extends Base {
* @readonly
*/
get editable() {
const precheck = Boolean(this.author.id === this.client.user.id && (!this.guild || this.channel?.viewable));
const precheck = Boolean(
this.author.id === this.client.user.id && !deletedMessages.has(this) && (!this.guild || this.channel?.viewable),
);
// Regardless of permissions thread messages cannot be edited if
// the thread is locked.
if (this.channel?.isThread()) {
@@ -566,6 +601,9 @@ class Message extends Base {
* @readonly
*/
get deletable() {
if (deletedMessages.has(this)) {
return false;
}
if (!this.guild) {
return this.author.id === this.client.user.id;
}
@@ -577,11 +615,11 @@ class Message extends Base {
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;
if (permissions.has(Permissions.FLAGS.ADMINISTRATOR, false)) return true;
return Boolean(
this.author.id === this.client.user.id ||
(permissions.has(PermissionFlagsBits.ManageMessages, false) &&
(permissions.has(Permissions.FLAGS.MANAGE_MESSAGES, false) &&
this.guild.me.communicationDisabledUntilTimestamp < Date.now()),
);
}
@@ -595,9 +633,10 @@ class Message extends Base {
const { channel } = this;
return Boolean(
!this.system &&
!deletedMessages.has(this) &&
(!this.guild ||
(channel?.viewable &&
channel?.permissionsFor(this.client.user)?.has(PermissionFlagsBits.ManageMessages, false))),
channel?.permissionsFor(this.client.user)?.has(Permissions.FLAGS.MANAGE_MESSAGES, false))),
);
}
@@ -621,15 +660,16 @@ class Message extends Base {
*/
get crosspostable() {
const bitfield =
PermissionFlagsBits.SendMessages |
(this.author.id === this.client.user.id ? PermissionsBitField.defaultBit : PermissionFlagsBits.ManageMessages);
Permissions.FLAGS.SEND_MESSAGES |
(this.author.id === this.client.user.id ? Permissions.defaultBit : Permissions.FLAGS.MANAGE_MESSAGES);
const { channel } = this;
return Boolean(
channel?.type === ChannelType.GuildNews &&
!this.flags.has(MessageFlags.Crossposted) &&
this.type === MessageType.Default &&
channel?.type === 'GUILD_NEWS' &&
!this.flags.has(MessageFlags.FLAGS.CROSSPOSTED) &&
this.type === 'DEFAULT' &&
channel.viewable &&
channel.permissionsFor(this.client.user)?.has(bitfield, false),
channel.permissionsFor(this.client.user)?.has(bitfield, false) &&
!deletedMessages.has(this),
);
}
@@ -637,14 +677,13 @@ class Message extends Base {
* 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 {MessageEmbed[]|APIEmbed[]} [embeds] Embeds to be added/edited
* @property {MessageMentionOptions} [allowedMentions] Which mentions should be parsed from the message content
* @property {MessageFlags} [flags] Which flags to set for the message.
* Only `MessageFlags.SuppressEmbeds` can be edited.
* @property {MessageFlags} [flags] Which flags to set for the message. Only `SUPPRESS_EMBEDS` can be edited.
* @property {MessageAttachment[]} [attachments] An array of attachments to keep,
* all attachments will be kept if omitted
* @property {FileOptions[]|BufferResolvable[]|MessageAttachment[]} [files] Files to add to the message
* @property {ActionRow[]|ActionRowOptions[]} [components]
* @property {MessageActionRow[]|MessageActionRowOptions[]} [components]
* Action rows containing interactive components for the message (buttons, select menus)
*/
@@ -668,7 +707,7 @@ class Message extends Base {
* @returns {Promise<Message>}
* @example
* // Crosspost a message
* if (message.channel.type === ChannelType.GuildNews) {
* if (message.channel.type === 'GUILD_NEWS') {
* message.crosspost()
* .then(() => console.log('Crossposted message'))
* .catch(console.error);
@@ -681,7 +720,6 @@ class Message extends Base {
/**
* Pins this message to the channel's pinned messages.
* @param {string} [reason] Reason for pinning
* @returns {Promise<Message>}
* @example
* // Pin a message
@@ -689,15 +727,14 @@ class Message extends Base {
* .then(console.log)
* .catch(console.error)
*/
async pin(reason) {
async pin() {
if (!this.channel) throw new Error('CHANNEL_NOT_CACHED');
await this.channel.messages.pin(this.id, reason);
await this.channel.messages.pin(this.id);
return this;
}
/**
* Unpins this message from the channel's pinned messages.
* @param {string} [reason] Reason for unpinning
* @returns {Promise<Message>}
* @example
* // Unpin a message
@@ -705,9 +742,9 @@ class Message extends Base {
* .then(console.log)
* .catch(console.error)
*/
async unpin(reason) {
async unpin() {
if (!this.channel) throw new Error('CHANNEL_NOT_CACHED');
await this.channel.messages.unpin(this.id, reason);
await this.channel.messages.unpin(this.id);
return this;
}
@@ -759,8 +796,8 @@ class Message extends Base {
/**
* 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 {boolean} [failIfNotExists=true] 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
*/
@@ -820,7 +857,7 @@ class Message extends Base {
*/
startThread(options = {}) {
if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED'));
if (![ChannelType.GuildText, ChannelType.GuildNews].includes(this.channel.type)) {
if (!['GUILD_TEXT', 'GUILD_NEWS'].includes(this.channel.type)) {
return Promise.reject(new Error('MESSAGE_THREAD_PARENT'));
}
if (this.hasThread) return Promise.reject(new Error('MESSAGE_EXISTING_THREAD'));
@@ -853,12 +890,12 @@ class Message extends Base {
* @returns {Promise<Message>}
*/
suppressEmbeds(suppress = true) {
const flags = new MessageFlagsBitField(this.flags.bitfield);
const flags = new MessageFlags(this.flags.bitfield);
if (suppress) {
flags.add(MessageFlags.SuppressEmbeds);
flags.add(MessageFlags.FLAGS.SUPPRESS_EMBEDS);
} else {
flags.remove(MessageFlags.SuppressEmbeds);
flags.remove(MessageFlags.FLAGS.SUPPRESS_EMBEDS);
}
return this.edit({ flags });
@@ -906,8 +943,8 @@ class Message extends Base {
if (equal && rawData) {
equal =
this.mentions.everyone === message.mentions.everyone &&
this.createdTimestamp === Date.parse(rawData.timestamp) &&
this.editedTimestamp === Date.parse(rawData.edited_timestamp);
this.createdTimestamp === new Date(rawData.timestamp).getTime() &&
this.editedTimestamp === new Date(rawData.edited_timestamp).getTime();
}
return equal;
@@ -946,3 +983,4 @@ class Message extends Base {
}
exports.Message = Message;
exports.deletedMessages = deletedMessages;

View File

@@ -0,0 +1,101 @@
'use strict';
const BaseMessageComponent = require('./BaseMessageComponent');
const { MessageComponentTypes } = require('../util/Constants');
/**
* Represents an action row containing message components.
* @extends {BaseMessageComponent}
*/
class MessageActionRow extends BaseMessageComponent {
/**
* Components that can be placed in an action row
* * MessageButton
* * MessageSelectMenu
* @typedef {MessageButton|MessageSelectMenu} MessageActionRowComponent
*/
/**
* Options for components that can be placed in an action row
* * MessageButtonOptions
* * MessageSelectMenuOptions
* @typedef {MessageButtonOptions|MessageSelectMenuOptions} MessageActionRowComponentOptions
*/
/**
* Data that can be resolved into components that can be placed in an action row
* * MessageActionRowComponent
* * MessageActionRowComponentOptions
* @typedef {MessageActionRowComponent|MessageActionRowComponentOptions} MessageActionRowComponentResolvable
*/
/**
* @typedef {BaseMessageComponentOptions} MessageActionRowOptions
* @property {MessageActionRowComponentResolvable[]} [components]
* The components to place in this action row
*/
/**
* @param {MessageActionRow|MessageActionRowOptions} [data={}] MessageActionRow to clone or raw data
* @param {Client} [client] The client constructing this MessageActionRow, if provided
*/
constructor(data = {}, client = null) {
super({ type: 'ACTION_ROW' });
/**
* The components in this action row
* @type {MessageActionRowComponent[]}
*/
this.components = data.components?.map(c => BaseMessageComponent.create(c, client)) ?? [];
}
/**
* Adds components to the action row.
* @param {...MessageActionRowComponentResolvable[]} components The components to add
* @returns {MessageActionRow}
*/
addComponents(...components) {
this.components.push(...components.flat(Infinity).map(c => BaseMessageComponent.create(c)));
return this;
}
/**
* Sets the components of the action row.
* @param {...MessageActionRowComponentResolvable[]} components The components to set
* @returns {MessageActionRow}
*/
setComponents(...components) {
this.spliceComponents(0, this.components.length, components);
return this;
}
/**
* Removes, replaces, and inserts components in the action row.
* @param {number} index The index to start at
* @param {number} deleteCount The number of components to remove
* @param {...MessageActionRowComponentResolvable[]} [components] The replacing components
* @returns {MessageActionRow}
*/
spliceComponents(index, deleteCount, ...components) {
this.components.splice(index, deleteCount, ...components.flat(Infinity).map(c => BaseMessageComponent.create(c)));
return this;
}
/**
* Transforms the action row to a plain object.
* @returns {APIMessageComponent} The raw data of this action row
*/
toJSON() {
return {
components: this.components.map(c => c.toJSON()),
type: MessageComponentTypes[this.type],
};
}
}
module.exports = MessageActionRow;
/**
* @external APIMessageComponent
* @see {@link https://discord.com/developers/docs/interactions/message-components#component-object}
*/

View File

@@ -124,7 +124,7 @@ class MessageAttachment {
if ('content_type' in data) {
/**
* The media type of this attachment
* This media type of this attachment
* @type {?string}
*/
this.contentType = data.content_type;

View File

@@ -0,0 +1,165 @@
'use strict';
const BaseMessageComponent = require('./BaseMessageComponent');
const { RangeError } = require('../errors');
const { MessageButtonStyles, MessageComponentTypes } = require('../util/Constants');
const Util = require('../util/Util');
/**
* Represents a button message component.
* @extends {BaseMessageComponent}
*/
class MessageButton extends BaseMessageComponent {
/**
* @typedef {BaseMessageComponentOptions} MessageButtonOptions
* @property {string} [label] The text to be displayed on this button
* @property {string} [customId] A unique string to be sent in the interaction when clicked
* @property {MessageButtonStyleResolvable} [style] The style of this button
* @property {EmojiIdentifierResolvable} [emoji] The emoji to be displayed to the left of the text
* @property {string} [url] Optional URL for link-style buttons
* @property {boolean} [disabled=false] Disables the button to prevent interactions
*/
/**
* @param {MessageButton|MessageButtonOptions} [data={}] MessageButton to clone or raw data
*/
constructor(data = {}) {
super({ type: 'BUTTON' });
this.setup(data);
}
setup(data) {
/**
* The text to be displayed on this button
* @type {?string}
*/
this.label = data.label ?? null;
/**
* A unique string to be sent in the interaction when clicked
* @type {?string}
*/
this.customId = data.custom_id ?? data.customId ?? null;
/**
* The style of this button
* @type {?MessageButtonStyle}
*/
this.style = data.style ? MessageButton.resolveStyle(data.style) : null;
/**
* Emoji for this button
* @type {?RawEmoji}
*/
this.emoji = data.emoji ? Util.resolvePartialEmoji(data.emoji) : null;
/**
* The URL this button links to, if it is a Link style button
* @type {?string}
*/
this.url = data.url ?? null;
/**
* Whether this button is currently disabled
* @type {boolean}
*/
this.disabled = data.disabled ?? false;
}
/**
* Sets the custom id for this button
* @param {string} customId A unique string to be sent in the interaction when clicked
* @returns {MessageButton}
*/
setCustomId(customId) {
this.customId = Util.verifyString(customId, RangeError, 'BUTTON_CUSTOM_ID');
return this;
}
/**
* Sets the interactive status of the button
* @param {boolean} [disabled=true] Whether this button should be disabled
* @returns {MessageButton}
*/
setDisabled(disabled = true) {
this.disabled = disabled;
return this;
}
/**
* Set the emoji of this button
* @param {EmojiIdentifierResolvable} emoji The emoji to be displayed on this button
* @returns {MessageButton}
*/
setEmoji(emoji) {
this.emoji = Util.resolvePartialEmoji(emoji);
return this;
}
/**
* Sets the label of this button
* @param {string} label The text to be displayed on this button
* @returns {MessageButton}
*/
setLabel(label) {
this.label = Util.verifyString(label, RangeError, 'BUTTON_LABEL');
return this;
}
/**
* Sets the style of this button
* @param {MessageButtonStyleResolvable} style The style of this button
* @returns {MessageButton}
*/
setStyle(style) {
this.style = MessageButton.resolveStyle(style);
return this;
}
/**
* Sets the URL of this button.
* <info>MessageButton#style must be LINK when setting a URL</info>
* @param {string} url The URL of this button
* @returns {MessageButton}
*/
setURL(url) {
this.url = Util.verifyString(url, RangeError, 'BUTTON_URL');
return this;
}
/**
* Transforms the button to a plain object.
* @returns {APIMessageButton} The raw data of this button
*/
toJSON() {
return {
custom_id: this.customId,
disabled: this.disabled,
emoji: this.emoji,
label: this.label,
style: MessageButtonStyles[this.style],
type: MessageComponentTypes[this.type],
url: this.url,
};
}
/**
* Data that can be resolved to a MessageButtonStyle. This can be
* * MessageButtonStyle
* * number
* @typedef {number|MessageButtonStyle} MessageButtonStyleResolvable
*/
/**
* Resolves the style of a button
* @param {MessageButtonStyleResolvable} style The style to resolve
* @returns {MessageButtonStyle}
* @private
*/
static resolveStyle(style) {
return typeof style === 'string' ? style : MessageButtonStyles[style];
}
}
module.exports = MessageButton;

View File

@@ -1,7 +1,7 @@
'use strict';
const Collector = require('./interfaces/Collector');
const Events = require('../util/Events');
const { Events } = require('../util/Constants');
/**
* @typedef {CollectorOptions} MessageCollectorOptions
@@ -46,20 +46,20 @@ class MessageCollector extends Collector {
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.client.on(Events.MESSAGE_CREATE, this.handleCollect);
this.client.on(Events.MESSAGE_DELETE, this.handleDispose);
this.client.on(Events.MESSAGE_BULK_DELETE, bulkDeleteListener);
this.client.on(Events.CHANNEL_DELETE, this._handleChannelDeletion);
this.client.on(Events.THREAD_DELETE, this._handleThreadDeletion);
this.client.on(Events.GUILD_DELETE, 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.removeListener(Events.MESSAGE_CREATE, this.handleCollect);
this.client.removeListener(Events.MESSAGE_DELETE, this.handleDispose);
this.client.removeListener(Events.MESSAGE_BULK_DELETE, bulkDeleteListener);
this.client.removeListener(Events.CHANNEL_DELETE, this._handleChannelDeletion);
this.client.removeListener(Events.THREAD_DELETE, this._handleThreadDeletion);
this.client.removeListener(Events.GUILD_DELETE, this._handleGuildDeletion);
this.client.decrementMaxListeners();
});
}

View File

@@ -3,6 +3,7 @@
const Interaction = require('./Interaction');
const InteractionWebhook = require('./InteractionWebhook');
const InteractionResponses = require('./interfaces/InteractionResponses');
const { MessageComponentTypes } = require('../util/Constants');
/**
* Represents a message component interaction.
@@ -33,9 +34,9 @@ class MessageComponentInteraction extends Interaction {
/**
* The type of component which was interacted with
* @type {ComponentType}
* @type {string}
*/
this.componentType = data.data.component_type;
this.componentType = MessageComponentInteraction.resolveType(data.data.component_type);
/**
* Whether the reply to this interaction has been deferred
@@ -80,6 +81,16 @@ class MessageComponentInteraction extends Interaction {
.find(component => (component.customId ?? component.custom_id) === this.customId);
}
/**
* Resolves the type of a MessageComponent
* @param {MessageComponentTypeResolvable} type The type to resolve
* @returns {MessageComponentType}
* @private
*/
static resolveType(type) {
return typeof type === 'string' ? type : MessageComponentTypes[type];
}
// These are here only for documentation purposes - they are implemented by InteractionResponses
/* eslint-disable no-empty-function */
deferReply() {}
@@ -105,28 +116,3 @@ module.exports = MessageComponentInteraction;
* @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}
*/

View File

@@ -1,20 +0,0 @@
'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;

View File

@@ -0,0 +1,20 @@
'use strict';
const ContextMenuInteraction = require('./ContextMenuInteraction');
/**
* Represents a message context menu interaction.
* @extends {ContextMenuInteraction}
*/
class MessageContextMenuInteraction extends ContextMenuInteraction {
/**
* The message this interaction was sent from
* @type {Message|APIMessage}
* @readonly
*/
get targetMessage() {
return this.options.getMessage('message');
}
}
module.exports = MessageContextMenuInteraction;

View File

@@ -0,0 +1,575 @@
'use strict';
const process = require('node:process');
const { RangeError } = require('../errors');
const Util = require('../util/Util');
let deprecationEmittedForSetAuthor = false;
let deprecationEmittedForSetFooter = false;
// TODO: Remove the deprecated code for `setAuthor()` and `setFooter()`.
/**
* Represents an embed in a message (image/video preview, rich embed, etc.)
*/
class MessageEmbed {
/**
* A `Partial` object is a representation of any existing object.
* This object contains between 0 and all of the original objects parameters.
* This is true regardless of whether the parameters are optional in the base object.
* @typedef {Object} Partial
*/
/**
* Represents the possible options for a MessageEmbed
* @typedef {Object} MessageEmbedOptions
* @property {string} [title] The title of this embed
* @property {string} [description] The description of this embed
* @property {string} [url] The URL of this embed
* @property {Date|number} [timestamp] The timestamp of this embed
* @property {ColorResolvable} [color] The color of this embed
* @property {EmbedFieldData[]} [fields] The fields of this embed
* @property {Partial<MessageEmbedAuthor>} [author] The author of this embed
* @property {Partial<MessageEmbedThumbnail>} [thumbnail] The thumbnail of this embed
* @property {Partial<MessageEmbedImage>} [image] The image of this embed
* @property {Partial<MessageEmbedVideo>} [video] The video of this embed
* @property {Partial<MessageEmbedFooter>} [footer] The footer of this embed
*/
// eslint-disable-next-line valid-jsdoc
/**
* @param {MessageEmbed|MessageEmbedOptions|APIEmbed} [data={}] MessageEmbed to clone or raw embed data
*/
constructor(data = {}, skipValidation = false) {
this.setup(data, skipValidation);
}
setup(data, skipValidation) {
/**
* The type of this embed, either:
* * `rich` - a generic embed rendered from embed attributes
* * `image` - an image embed
* * `video` - a video embed
* * `gifv` - an animated gif image embed rendered as a video embed
* * `article` - an article embed
* * `link` - a link embed
* @type {string}
* @see {@link https://discord.com/developers/docs/resources/channel#embed-object-embed-types}
* @deprecated
*/
this.type = data.type ?? 'rich';
/**
* The title of this embed
* @type {?string}
*/
this.title = data.title ?? null;
/**
* The description of this embed
* @type {?string}
*/
this.description = data.description ?? null;
/**
* The URL of this embed
* @type {?string}
*/
this.url = data.url ?? null;
/**
* The color of this embed
* @type {?number}
*/
this.color = 'color' in data ? Util.resolveColor(data.color) : null;
/**
* The timestamp of this embed
* @type {?number}
*/
this.timestamp = 'timestamp' in data ? new Date(data.timestamp).getTime() : null;
/**
* Represents a field of a MessageEmbed
* @typedef {Object} EmbedField
* @property {string} name The name of this field
* @property {string} value The value of this field
* @property {boolean} inline If this field will be displayed inline
*/
/**
* The fields of this embed
* @type {EmbedField[]}
*/
this.fields = [];
if (data.fields) {
this.fields = skipValidation ? data.fields.map(Util.cloneObject) : this.constructor.normalizeFields(data.fields);
}
/**
* Represents the thumbnail of a MessageEmbed
* @typedef {Object} MessageEmbedThumbnail
* @property {string} url URL for this thumbnail
* @property {string} proxyURL ProxyURL for this thumbnail
* @property {number} height Height of this thumbnail
* @property {number} width Width of this thumbnail
*/
/**
* The thumbnail of this embed (if there is one)
* @type {?MessageEmbedThumbnail}
*/
this.thumbnail = data.thumbnail
? {
url: data.thumbnail.url,
proxyURL: data.thumbnail.proxyURL ?? data.thumbnail.proxy_url,
height: data.thumbnail.height,
width: data.thumbnail.width,
}
: null;
/**
* Represents the image of a MessageEmbed
* @typedef {Object} MessageEmbedImage
* @property {string} url URL for this image
* @property {string} proxyURL ProxyURL for this image
* @property {number} height Height of this image
* @property {number} width Width of this image
*/
/**
* The image of this embed, if there is one
* @type {?MessageEmbedImage}
*/
this.image = data.image
? {
url: data.image.url,
proxyURL: data.image.proxyURL ?? data.image.proxy_url,
height: data.image.height,
width: data.image.width,
}
: null;
/**
* Represents the video of a MessageEmbed
* @typedef {Object} MessageEmbedVideo
* @property {string} url URL of this video
* @property {string} proxyURL ProxyURL for this video
* @property {number} height Height of this video
* @property {number} width Width of this video
*/
/**
* The video of this embed (if there is one)
* @type {?MessageEmbedVideo}
* @readonly
*/
this.video = data.video
? {
url: data.video.url,
proxyURL: data.video.proxyURL ?? data.video.proxy_url,
height: data.video.height,
width: data.video.width,
}
: null;
/**
* Represents the author field of a MessageEmbed
* @typedef {Object} MessageEmbedAuthor
* @property {string} name The name of this author
* @property {string} url URL of this author
* @property {string} iconURL URL of the icon for this author
* @property {string} proxyIconURL Proxied URL of the icon for this author
*/
/**
* The author of this embed (if there is one)
* @type {?MessageEmbedAuthor}
*/
this.author = data.author
? {
name: data.author.name,
url: data.author.url,
iconURL: data.author.iconURL ?? data.author.icon_url,
proxyIconURL: data.author.proxyIconURL ?? data.author.proxy_icon_url,
}
: null;
/**
* Represents the provider of a MessageEmbed
* @typedef {Object} MessageEmbedProvider
* @property {string} name The name of this provider
* @property {string} url URL of this provider
*/
/**
* The provider of this embed (if there is one)
* @type {?MessageEmbedProvider}
*/
this.provider = data.provider
? {
name: data.provider.name,
url: data.provider.name,
}
: null;
/**
* Represents the footer field of a MessageEmbed
* @typedef {Object} MessageEmbedFooter
* @property {string} text The text of this footer
* @property {string} iconURL URL of the icon for this footer
* @property {string} proxyIconURL Proxied URL of the icon for this footer
*/
/**
* The footer of this embed
* @type {?MessageEmbedFooter}
*/
this.footer = data.footer
? {
text: data.footer.text,
iconURL: data.footer.iconURL ?? data.footer.icon_url,
proxyIconURL: data.footer.proxyIconURL ?? data.footer.proxy_icon_url,
}
: null;
}
/**
* The date displayed on this embed
* @type {?Date}
* @readonly
*/
get createdAt() {
return this.timestamp ? new Date(this.timestamp) : null;
}
/**
* The hexadecimal version of the embed color, with a leading hash
* @type {?string}
* @readonly
*/
get hexColor() {
return this.color ? `#${this.color.toString(16).padStart(6, '0')}` : null;
}
/**
* The accumulated length for the embed title, description, fields, footer text, and author name
* @type {number}
* @readonly
*/
get length() {
return (
(this.title?.length ?? 0) +
(this.description?.length ?? 0) +
(this.fields.length >= 1
? this.fields.reduce((prev, curr) => prev + curr.name.length + curr.value.length, 0)
: 0) +
(this.footer?.text.length ?? 0) +
(this.author?.name.length ?? 0)
);
}
/**
* Checks if this embed is equal to another one by comparing every single one of their properties.
* @param {MessageEmbed|APIEmbed} embed The embed to compare with
* @returns {boolean}
*/
equals(embed) {
return (
this.type === embed.type &&
this.author?.name === embed.author?.name &&
this.author?.url === embed.author?.url &&
this.author?.iconURL === (embed.author?.iconURL ?? embed.author?.icon_url) &&
this.color === embed.color &&
this.title === embed.title &&
this.description === embed.description &&
this.url === embed.url &&
this.timestamp === embed.timestamp &&
this.fields.length === embed.fields.length &&
this.fields.every((field, i) => this._fieldEquals(field, embed.fields[i])) &&
this.footer?.text === embed.footer?.text &&
this.footer?.iconURL === (embed.footer?.iconURL ?? embed.footer?.icon_url) &&
this.image?.url === embed.image?.url &&
this.thumbnail?.url === embed.thumbnail?.url &&
this.video?.url === embed.video?.url &&
this.provider?.name === embed.provider?.name &&
this.provider?.url === embed.provider?.url
);
}
/**
* Compares two given embed fields to see if they are equal
* @param {EmbedFieldData} field The first field to compare
* @param {EmbedFieldData} other The second field to compare
* @returns {boolean}
* @private
*/
_fieldEquals(field, other) {
return field.name === other.name && field.value === other.value && field.inline === other.inline;
}
/**
* Adds a field to the embed (max 25).
* @param {string} name The name of this field
* @param {string} value The value of this field
* @param {boolean} [inline=false] If this field will be displayed inline
* @returns {MessageEmbed}
*/
addField(name, value, inline) {
return this.addFields({ name, value, inline });
}
/**
* Adds fields to the embed (max 25).
* @param {...EmbedFieldData|EmbedFieldData[]} fields The fields to add
* @returns {MessageEmbed}
*/
addFields(...fields) {
this.fields.push(...this.constructor.normalizeFields(fields));
return this;
}
/**
* Removes, replaces, and inserts fields in the embed (max 25).
* @param {number} index The index to start at
* @param {number} deleteCount The number of fields to remove
* @param {...EmbedFieldData|EmbedFieldData[]} [fields] The replacing field objects
* @returns {MessageEmbed}
*/
spliceFields(index, deleteCount, ...fields) {
this.fields.splice(index, deleteCount, ...this.constructor.normalizeFields(...fields));
return this;
}
/**
* Sets the embed's fields (max 25).
* @param {...EmbedFieldData|EmbedFieldData[]} fields The fields to set
* @returns {MessageEmbed}
*/
setFields(...fields) {
this.spliceFields(0, this.fields.length, fields);
return this;
}
/**
* The options to provide for setting an author for a {@link MessageEmbed}.
* @typedef {Object} EmbedAuthorData
* @property {string} name The name of this author.
* @property {string} [url] The URL of this author.
* @property {string} [iconURL] The icon URL of this author.
*/
/**
* Sets the author of this embed.
* @param {string|EmbedAuthorData|null} options The options to provide for the author.
* Provide `null` to remove the author data.
* @param {string} [deprecatedIconURL] The icon URL of this author.
* <warn>This parameter is **deprecated**. Use the `options` parameter instead.</warn>
* @param {string} [deprecatedURL] The URL of this author.
* <warn>This parameter is **deprecated**. Use the `options` parameter instead.</warn>
* @returns {MessageEmbed}
*/
setAuthor(options, deprecatedIconURL, deprecatedURL) {
if (options === null) {
this.author = {};
return this;
}
if (typeof options === 'string') {
if (!deprecationEmittedForSetAuthor) {
process.emitWarning(
'Passing strings for MessageEmbed#setAuthor is deprecated. Pass a sole object instead.',
'DeprecationWarning',
);
deprecationEmittedForSetAuthor = true;
}
options = { name: options, url: deprecatedURL, iconURL: deprecatedIconURL };
}
const { name, url, iconURL } = options;
this.author = { name: Util.verifyString(name, RangeError, 'EMBED_AUTHOR_NAME'), url, iconURL };
return this;
}
/**
* Sets the color of this embed.
* @param {ColorResolvable} color The color of the embed
* @returns {MessageEmbed}
*/
setColor(color) {
this.color = Util.resolveColor(color);
return this;
}
/**
* Sets the description of this embed.
* @param {string} description The description
* @returns {MessageEmbed}
*/
setDescription(description) {
this.description = Util.verifyString(description, RangeError, 'EMBED_DESCRIPTION');
return this;
}
/**
* The options to provide for setting a footer for a {@link MessageEmbed}.
* @typedef {Object} EmbedFooterData
* @property {string} text The text of the footer.
* @property {string} [iconURL] The icon URL of the footer.
*/
/**
* Sets the footer of this embed.
* @param {string|EmbedFooterData|null} options The options to provide for the footer.
* Provide `null` to remove the footer data.
* @param {string} [deprecatedIconURL] The icon URL of this footer.
* <warn>This parameter is **deprecated**. Use the `options` parameter instead.</warn>
* @returns {MessageEmbed}
*/
setFooter(options, deprecatedIconURL) {
if (options === null) {
this.footer = {};
return this;
}
if (typeof options === 'string') {
if (!deprecationEmittedForSetFooter) {
process.emitWarning(
'Passing strings for MessageEmbed#setFooter is deprecated. Pass a sole object instead.',
'DeprecationWarning',
);
deprecationEmittedForSetFooter = true;
}
options = { text: options, iconURL: deprecatedIconURL };
}
const { text, iconURL } = options;
this.footer = { text: Util.verifyString(text, RangeError, 'EMBED_FOOTER_TEXT'), iconURL };
return this;
}
/**
* Sets the image of this embed.
* @param {string} url The URL of the image
* @returns {MessageEmbed}
*/
setImage(url) {
this.image = { url };
return this;
}
/**
* Sets the thumbnail of this embed.
* @param {string} url The URL of the thumbnail
* @returns {MessageEmbed}
*/
setThumbnail(url) {
this.thumbnail = { url };
return this;
}
/**
* Sets the timestamp of this embed.
* @param {Date|number|null} [timestamp=Date.now()] The timestamp or date.
* If `null` then the timestamp will be unset (i.e. when editing an existing {@link MessageEmbed})
* @returns {MessageEmbed}
*/
setTimestamp(timestamp = Date.now()) {
if (timestamp instanceof Date) timestamp = timestamp.getTime();
this.timestamp = timestamp;
return this;
}
/**
* Sets the title of this embed.
* @param {string} title The title
* @returns {MessageEmbed}
*/
setTitle(title) {
this.title = Util.verifyString(title, RangeError, 'EMBED_TITLE');
return this;
}
/**
* Sets the URL of this embed.
* @param {string} url The URL
* @returns {MessageEmbed}
*/
setURL(url) {
this.url = url;
return this;
}
/**
* Transforms the embed to a plain object.
* @returns {APIEmbed} The raw data of this embed
*/
toJSON() {
return {
title: this.title,
type: 'rich',
description: this.description,
url: this.url,
timestamp: this.timestamp && new Date(this.timestamp),
color: this.color,
fields: this.fields,
thumbnail: this.thumbnail,
image: this.image,
author: this.author && {
name: this.author.name,
url: this.author.url,
icon_url: this.author.iconURL,
},
footer: this.footer && {
text: this.footer.text,
icon_url: this.footer.iconURL,
},
};
}
/**
* Normalizes field input and verifies strings.
* @param {string} name The name of the field
* @param {string} value The value of the field
* @param {boolean} [inline=false] Set the field to display inline
* @returns {EmbedField}
*/
static normalizeField(name, value, inline = false) {
return {
name: Util.verifyString(name, RangeError, 'EMBED_FIELD_NAME', false),
value: Util.verifyString(value, RangeError, 'EMBED_FIELD_VALUE', false),
inline,
};
}
/**
* @typedef {Object} EmbedFieldData
* @property {string} name The name of this field
* @property {string} value The value of this field
* @property {boolean} [inline] If this field will be displayed inline
*/
/**
* Normalizes field input and resolves strings.
* @param {...EmbedFieldData|EmbedFieldData[]} fields Fields to normalize
* @returns {EmbedField[]}
*/
static normalizeFields(...fields) {
return fields
.flat(2)
.map(field =>
this.normalizeField(field.name, field.value, typeof field.inline === 'boolean' ? field.inline : false),
);
}
}
module.exports = MessageEmbed;
/**
* @external APIEmbed
* @see {@link https://discord.com/developers/docs/resources/channel#embed-object}
*/

View File

@@ -1,6 +1,7 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { ChannelTypes } = require('../util/Constants');
const Util = require('../util/Util');
/**
@@ -111,11 +112,13 @@ class MessageMentions {
this.crosspostedChannels = new Collection(crosspostedChannels);
} else {
this.crosspostedChannels = new Collection();
const channelTypes = Object.keys(ChannelTypes);
for (const d of crosspostedChannels) {
const type = channelTypes[d.type];
this.crosspostedChannels.set(d.id, {
channelId: d.id,
guildId: d.guild_id,
type: d.type,
type: type ?? 'UNKNOWN',
name: d.name,
});
}
@@ -170,35 +173,28 @@ class MessageMentions {
* @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
* @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.
* Checks if a user, guild member, role, or channel is mentioned.
* Takes into account user mentions, role mentions, and `@everyone`/`@here` mentions.
* @param {UserResolvable|RoleResolvable|ChannelResolvable} data The User/Role/Channel to check for
* @param {MessageMentionsHasOptions} [options] The options for the check
* @returns {boolean}
*/
has(data, { ignoreDirect = false, ignoreRoles = false, 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;
has(data, { ignoreDirect = false, ignoreRoles = false, ignoreEveryone = false } = {}) {
if (!ignoreEveryone && this.everyone) return true;
const { GuildMember } = require('./GuildMember');
if (!ignoreRoles && data instanceof GuildMember) {
for (const role of this.roles.values()) if (data.roles.cache.has(role.id)) return true;
}
if (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;
}
if (!ignoreDirect) {
const id =
this.guild?.roles.resolveId(data) ?? this.client.channels.resolveId(data) ?? this.client.users.resolveId(data);
return typeof id === 'string' && (this.users.has(id) || this.channels.has(id) || this.roles.has(id));
}
return false;

View File

@@ -1,304 +1,270 @@
'use strict';
const { Buffer } = require('node:buffer');
const { BaseMessageComponent, MessageEmbed } = require('discord.js');
const { MessageFlags } = require('discord-api-types/v9');
const BaseMessageComponent = require('./BaseMessageComponent');
const MessageEmbed = require('./MessageEmbed');
const { RangeError } = require('../errors');
const DataResolver = require('../util/DataResolver');
const MessageFlagsBitField = require('../util/MessageFlagsBitField');
const MessageFlags = require('../util/MessageFlags');
const Util = require('../util/Util');
/**
* Represents a message to be sent to the API.
*/
class MessagePayload {
/**
* @param {MessageTarget} target The target for this message to be sent to
* @param {MessageOptions|WebhookMessageOptions} options Options passed in from send
*/
constructor(target, options) {
/**
* The target for this message to be sent to
* @type {MessageTarget}
*/
this.target = target;
/**
* @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;
/**
* Options passed in from send
* @type {MessageOptions|WebhookMessageOptions}
*/
this.options = options;
/**
* Body sendable to the API
* @type {?APIMessage}
*/
this.body = null;
/**
* Data sendable to the API
* @type {?APIMessage}
*/
this.data = null;
/**
* Files sendable to the API
* @type {?RawFile[]}
*/
this.files = null;
}
/**
* @typedef {Object} MessageFile
* @property {Buffer|string|Stream} attachment The original attachment that generated this file
* @property {string} name The name of this file
* @property {Buffer|Stream} file The file to be sent to the API
*/
/**
* 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
);
}
/**
* Files sendable to the API
* @type {?MessageFile[]}
*/
this.files = null;
}
/**
* 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 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 Message}
* @type {boolean}
* @readonly
*/
get isMessage() {
const { Message } = require('./Message');
return this.target instanceof Message;
}
/**
* 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 MessageManager}
* @type {boolean}
* @readonly
*/
get isMessageManager() {
const MessageManager = require('../managers/MessageManager');
return this.target instanceof MessageManager;
}
/**
* 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 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
);
}
/**
* Whether or not the target is a {@link MessageManager}
* @type {boolean}
* @readonly
*/
get isMessageManager() {
const MessageManager = require('../managers/MessageManager');
return this.target instanceof MessageManager;
}
/**
* 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,
);
}
/**
* 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;
}
return content;
}
/**
* Resolves the body.
* @returns {MessagePayload}
*/
resolveBody() {
if (this.data) return this;
const isInteraction = this.isInteraction;
const isWebhook = this.isWebhook;
/**
* 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);
}
const content = this.makeContent();
const tts = Boolean(this.options.tts);
return content;
}
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');
}
}
/**
* Resolves data.
* @returns {MessagePayload}
*/
resolveData() {
if (this.data) return this;
const isInteraction = this.isInteraction;
const isWebhook = this.isWebhook;
const components = this.options.components?.map((c) =>
BaseMessageComponent.create(c).toJSON(),
);
const content = this.makeContent();
const tts = Boolean(this.options.tts);
let username;
let avatarURL;
if (isWebhook) {
username = this.options.username ?? this.target.name;
if (this.options.avatarURL) avatarURL = this.options.avatarURL;
}
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');
}
}
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;
}
const components = this.options.components?.map(c => BaseMessageComponent.create(c).toJSON());
if (isInteraction && this.options.ephemeral) {
flags |= MessageFlags.Ephemeral;
}
let username;
let avatarURL;
if (isWebhook) {
username = this.options.username ?? this.target.name;
if (this.options.avatarURL) avatarURL = this.options.avatarURL;
}
let allowedMentions =
typeof this.options.allowedMentions === 'undefined'
? this.target.client.options.allowedMentions
: this.options.allowedMentions;
let flags;
if (this.isMessage || this.isMessageManager) {
// eslint-disable-next-line eqeqeq
flags = this.options.flags != null ? new MessageFlags(this.options.flags).bitfield : this.target.flags?.bitfield;
} else if (isInteraction && this.options.ephemeral) {
flags = MessageFlags.FLAGS.EPHEMERAL;
}
if (allowedMentions) {
allowedMentions = Util.cloneObject(allowedMentions);
allowedMentions.replied_user = allowedMentions.repliedUser;
delete allowedMentions.repliedUser;
}
let allowedMentions =
typeof this.options.allowedMentions === 'undefined'
? this.target.client.options.allowedMentions
: this.options.allowedMentions;
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,
};
}
}
if (allowedMentions) {
allowedMentions = Util.cloneObject(allowedMentions);
allowedMentions.replied_user = allowedMentions.repliedUser;
delete allowedMentions.repliedUser;
}
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;
}
let message_reference;
if (typeof this.options.reply === 'object') {
const reference = this.options.reply.messageReference;
const message_id = this.isMessage ? reference.id ?? reference : this.target.messages.resolveId(reference);
if (message_id) {
message_reference = {
message_id,
fail_if_not_exists: this.options.reply.failIfNotExists ?? this.target.client.options.failIfNotExists,
};
}
}
this.body = {
content,
tts,
nonce,
embeds: this.options.embeds?.map((embed) =>
new MessageEmbed(embed).toJSON(),
),
components,
username,
avatar_url: avatarURL,
allowed_mentions:
typeof content === 'undefined' &&
typeof message_reference === 'undefined'
? undefined
: allowedMentions,
flags,
message_reference,
attachments: this.options.attachments,
sticker_ids: this.options.stickers?.map(
(sticker) => sticker.id ?? sticker,
),
};
return this;
}
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;
}
/**
* Resolves files.
* @returns {Promise<MessagePayload>}
*/
async resolveFiles() {
if (this.files) return this;
this.data = {
content,
tts,
nonce,
embeds: this.options.embeds?.map(embed => new MessageEmbed(embed).toJSON()),
components,
username,
avatar_url: avatarURL,
allowed_mentions:
typeof content === 'undefined' && typeof message_reference === 'undefined' ? undefined : allowedMentions,
flags,
message_reference,
attachments: this.options.attachments,
sticker_ids: this.options.stickers?.map(sticker => sticker.id ?? sticker),
};
return this;
}
this.files = await Promise.all(
this.options.files?.map((file) => this.constructor.resolveFile(file)) ??
[],
);
return this;
}
/**
* Resolves files.
* @returns {Promise<MessagePayload>}
*/
async resolveFiles() {
if (this.files) 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;
this.files = await Promise.all(this.options.files?.map(file => this.constructor.resolveFile(file)) ?? []);
return this;
}
const findName = (thing) => {
if (typeof thing === 'string') {
return Util.basename(thing);
}
/**
* 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<MessageFile>}
*/
static async resolveFile(fileLike) {
let attachment;
let name;
if (thing.path) {
return Util.basename(thing.path);
}
const findName = thing => {
if (typeof thing === 'string') {
return Util.basename(thing);
}
return 'file.jpg';
};
if (thing.path) {
return Util.basename(thing.path);
}
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);
}
return 'file.jpg';
};
const resource = await DataResolver.resolveFile(attachment);
return { attachment, name, file: resource };
}
/**
* Creates a {@link MessagePayload} from user-level arguments.
* @param {MessageTarget} target Target to send to
* @param {string|MessageOptions|WebhookMessageOptions} options Options or content to use
* @param {MessageOptions|WebhookMessageOptions} [extra={}] Extra options to add onto specified options
* @returns {MessagePayload}
*/
static create(target, options, extra = {}) {
return new this(
target,
typeof options !== 'object' || options === null
? { content: options, ...extra }
: { ...options, ...extra },
);
}
const ownAttachment =
typeof fileLike === 'string' || fileLike instanceof Buffer || typeof fileLike.pipe === 'function';
if (ownAttachment) {
attachment = fileLike;
name = findName(attachment);
} else {
attachment = fileLike.attachment;
name = fileLike.name ?? findName(attachment);
}
const resource = await DataResolver.resolveFile(attachment);
return { attachment, name, file: resource };
}
/**
* Creates a {@link MessagePayload} from user-level arguments.
* @param {MessageTarget} target Target to send to
* @param {string|MessageOptions|WebhookMessageOptions} options Options or content to use
* @param {MessageOptions|WebhookMessageOptions} [extra={}] Extra options to add onto specified options
* @returns {MessagePayload}
*/
static create(target, options, extra = {}) {
return new this(
target,
typeof options !== 'object' || options === null ? { content: options, ...extra } : { ...options, ...extra },
);
}
}
module.exports = MessagePayload;
@@ -313,8 +279,3 @@ module.exports = MessagePayload;
* @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}
*/

View File

@@ -1,6 +1,5 @@
'use strict';
const { Routes } = require('discord-api-types/v9');
const GuildEmoji = require('./GuildEmoji');
const ReactionEmoji = require('./ReactionEmoji');
const ReactionUserManager = require('../managers/ReactionUserManager');
@@ -57,7 +56,11 @@ class MessageReaction {
* @returns {Promise<MessageReaction>}
*/
async remove() {
await this.client.api.channels(this.message.channelId).messages(this.message.id).reactions(this._emoji.identifier).delete()
await this.client.api
.channels(this.message.channelId)
.messages(this.message.id)
.reactions(this._emoji.identifier)
.delete();
return this;
}
@@ -111,7 +114,7 @@ class MessageReaction {
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;
this.me ??= user.id === this.message.client.user.id;
}
_remove(user) {

View File

@@ -0,0 +1,212 @@
'use strict';
const BaseMessageComponent = require('./BaseMessageComponent');
const { MessageComponentTypes } = require('../util/Constants');
const Util = require('../util/Util');
/**
* Represents a select menu message component
* @extends {BaseMessageComponent}
*/
class MessageSelectMenu extends BaseMessageComponent {
/**
* @typedef {BaseMessageComponentOptions} MessageSelectMenuOptions
* @property {string} [customId] A unique string to be sent in the interaction when clicked
* @property {string} [placeholder] Custom placeholder text to display when nothing is selected
* @property {number} [minValues] The minimum number of selections required
* @property {number} [maxValues] The maximum number of selections allowed
* @property {MessageSelectOption[]} [options] Options for the select menu
* @property {boolean} [disabled=false] Disables the select menu to prevent interactions
*/
/**
* @typedef {Object} MessageSelectOption
* @property {string} label The text to be displayed on this option
* @property {string} value The value to be sent for this option
* @property {?string} description Optional description to show for this option
* @property {?RawEmoji} emoji Emoji to display for this option
* @property {boolean} default Render this option as the default selection
*/
/**
* @typedef {Object} MessageSelectOptionData
* @property {string} label The text to be displayed on this option
* @property {string} value The value to be sent for this option
* @property {string} [description] Optional description to show for this option
* @property {EmojiIdentifierResolvable} [emoji] Emoji to display for this option
* @property {boolean} [default] Render this option as the default selection
*/
/**
* @param {MessageSelectMenu|MessageSelectMenuOptions} [data={}] MessageSelectMenu to clone or raw data
*/
constructor(data = {}) {
super({ type: 'SELECT_MENU' });
this.setup(data);
}
setup(data) {
/**
* A unique string to be sent in the interaction when clicked
* @type {?string}
*/
this.customId = data.custom_id ?? data.customId ?? null;
/**
* Custom placeholder text to display when nothing is selected
* @type {?string}
*/
this.placeholder = data.placeholder ?? null;
/**
* The minimum number of selections required
* @type {?number}
*/
this.minValues = data.min_values ?? data.minValues ?? null;
/**
* The maximum number of selections allowed
* @type {?number}
*/
this.maxValues = data.max_values ?? data.maxValues ?? null;
/**
* Options for the select menu
* @type {MessageSelectOption[]}
*/
this.options = this.constructor.normalizeOptions(data.options ?? []);
/**
* Whether this select menu is currently disabled
* @type {boolean}
*/
this.disabled = data.disabled ?? false;
}
/**
* Sets the custom id of this select menu
* @param {string} customId A unique string to be sent in the interaction when clicked
* @returns {MessageSelectMenu}
*/
setCustomId(customId) {
this.customId = Util.verifyString(customId, RangeError, 'SELECT_MENU_CUSTOM_ID');
return this;
}
/**
* Sets the interactive status of the select menu
* @param {boolean} [disabled=true] Whether this select menu should be disabled
* @returns {MessageSelectMenu}
*/
setDisabled(disabled = true) {
this.disabled = disabled;
return this;
}
/**
* Sets the maximum number of selections allowed for this select menu
* @param {number} maxValues Number of selections to be allowed
* @returns {MessageSelectMenu}
*/
setMaxValues(maxValues) {
this.maxValues = maxValues;
return this;
}
/**
* Sets the minimum number of selections required for this select menu
* <info>This will default the maxValues to the number of options, unless manually set</info>
* @param {number} minValues Number of selections to be required
* @returns {MessageSelectMenu}
*/
setMinValues(minValues) {
this.minValues = minValues;
return this;
}
/**
* Sets the placeholder of this select menu
* @param {string} placeholder Custom placeholder text to display when nothing is selected
* @returns {MessageSelectMenu}
*/
setPlaceholder(placeholder) {
this.placeholder = Util.verifyString(placeholder, RangeError, 'SELECT_MENU_PLACEHOLDER');
return this;
}
/**
* Adds options to the select menu.
* @param {...MessageSelectOptionData|MessageSelectOptionData[]} options The options to add
* @returns {MessageSelectMenu}
*/
addOptions(...options) {
this.options.push(...this.constructor.normalizeOptions(options));
return this;
}
/**
* Sets the options of the select menu.
* @param {...MessageSelectOptionData|MessageSelectOptionData[]} options The options to set
* @returns {MessageSelectMenu}
*/
setOptions(...options) {
this.spliceOptions(0, this.options.length, options);
return this;
}
/**
* Removes, replaces, and inserts options in the select menu.
* @param {number} index The index to start at
* @param {number} deleteCount The number of options to remove
* @param {...MessageSelectOptionData|MessageSelectOptionData[]} [options] The replacing option objects
* @returns {MessageSelectMenu}
*/
spliceOptions(index, deleteCount, ...options) {
this.options.splice(index, deleteCount, ...this.constructor.normalizeOptions(...options));
return this;
}
/**
* Transforms the select menu into a plain object
* @returns {APIMessageSelectMenu} The raw data of this select menu
*/
toJSON() {
return {
custom_id: this.customId,
disabled: this.disabled,
placeholder: this.placeholder,
min_values: this.minValues,
max_values: this.maxValues ?? (this.minValues ? this.options.length : undefined),
options: this.options,
type: typeof this.type === 'string' ? MessageComponentTypes[this.type] : this.type,
};
}
/**
* Normalizes option input and resolves strings and emojis.
* @param {MessageSelectOptionData} option The select menu option to normalize
* @returns {MessageSelectOption}
*/
static normalizeOption(option) {
let { label, value, description, emoji } = option;
label = Util.verifyString(label, RangeError, 'SELECT_OPTION_LABEL');
value = Util.verifyString(value, RangeError, 'SELECT_OPTION_VALUE');
emoji = emoji ? Util.resolvePartialEmoji(emoji) : null;
description = description ? Util.verifyString(description, RangeError, 'SELECT_OPTION_DESCRIPTION', true) : null;
return { label, value, description, emoji, default: option.default ?? false };
}
/**
* Normalizes option input and resolves strings and emojis.
* @param {...MessageSelectOptionData|MessageSelectOptionData[]} options The select menu options to normalize
* @returns {MessageSelectOption[]}
*/
static normalizeOptions(...options) {
return options.flat(Infinity).map(option => this.normalizeOption(option));
}
}
module.exports = MessageSelectMenu;

View File

@@ -1,6 +1,5 @@
'use strict';
const { Routes } = require('discord-api-types/v9');
const BaseGuildTextChannel = require('./BaseGuildTextChannel');
const { Error } = require('../errors');
@@ -15,7 +14,7 @@ class NewsChannel extends BaseGuildTextChannel {
* @param {string} [reason] Reason for creating the webhook
* @returns {Promise<NewsChannel>}
* @example
* if (channel.type === ChannelType.GuildNews) {
* if (channel.type === 'GUILD_NEWS') {
* channel.addFollower('222197033908436994', 'Important announcements')
* .then(() => console.log('Added follower'))
* .catch(console.error);
@@ -24,7 +23,7 @@ class NewsChannel extends BaseGuildTextChannel {
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 });
await this.client.api.channels(this.id).followers.post({ data: { webhook_channel_id: channelId }, reason });
return this;
}
}

View File

@@ -1,7 +1,7 @@
'use strict';
const BaseGuild = require('./BaseGuild');
const PermissionsBitField = require('../util/PermissionsBitField');
const Permissions = require('../util/Permissions');
/**
* A partial guild received when using {@link GuildManager#fetch} to fetch multiple guilds.
@@ -19,9 +19,9 @@ class OAuth2Guild extends BaseGuild {
/**
* The permissions that the client user has in this guild
* @type {Readonly<PermissionsBitField>}
* @type {Readonly<Permissions>}
*/
this.permissions = new PermissionsBitField(BigInt(data.permissions)).freeze();
this.permissions = new Permissions(BigInt(data.permissions)).freeze();
}
}

View File

@@ -38,11 +38,11 @@ class PartialGroupDMChannel extends Channel {
/**
* The URL to this channel's icon.
* @param {ImageURLOptions} [options={}] Options for the image URL
* @param {StaticImageURLOptions} [options={}] Options for the Image URL
* @returns {?string}
*/
iconURL(options = {}) {
return this.icon && this.client.rest.cdn.channelIcon(this.id, this.icon, options);
iconURL({ format, size } = {}) {
return this.icon && this.client.rest.cdn.GDMIcon(this.id, this.icon, format, size);
}
delete() {

View File

@@ -1,10 +1,10 @@
'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');
const { OverwriteTypes } = require('../util/Constants');
const Permissions = require('../util/Permissions');
/**
* Represents a permission overwrite for a role or member in a guild channel.
@@ -37,23 +37,23 @@ class PermissionOverwrites extends Base {
* The type of this overwrite
* @type {OverwriteType}
*/
this.type = data.type;
this.type = typeof data.type === 'number' ? OverwriteTypes[data.type] : data.type;
}
if ('deny' in data) {
/**
* The permissions that are denied for the user or role.
* @type {Readonly<PermissionsBitField>}
* @type {Readonly<Permissions>}
*/
this.deny = new PermissionsBitField(BigInt(data.deny)).freeze();
this.deny = new Permissions(BigInt(data.deny)).freeze();
}
if ('allow' in data) {
/**
* The permissions that are allowed for the user or role.
* @type {Readonly<PermissionsBitField>}
* @type {Readonly<Permissions>}
*/
this.allow = new PermissionsBitField(BigInt(data.allow)).freeze();
this.allow = new Permissions(BigInt(data.allow)).freeze();
}
}
@@ -71,7 +71,7 @@ class PermissionOverwrites extends Base {
* .catch(console.error);
*/
async edit(options, reason) {
await this.channel.permissionOverwrites.upsert(this.id, options, { type: this.type, reason }, this);
await this.channel.permissionOverwrites.upsert(this.id, options, { type: OverwriteTypes[this.type], reason }, this);
return this;
}
@@ -88,7 +88,7 @@ class PermissionOverwrites extends Base {
toJSON() {
return {
id: this.id,
type: this.type,
type: OverwriteTypes[this.type],
allow: this.allow,
deny: this.deny,
};
@@ -98,9 +98,9 @@ class PermissionOverwrites extends Base {
* An object mapping permission flags to `true` (enabled), `null` (unset) or `false` (disabled).
* ```js
* {
* 'SendMessages': true,
* 'EmbedLinks': null,
* 'AttachFiles': false,
* 'SEND_MESSAGES': true,
* 'EMBED_LINKS': null,
* 'ATTACH_FILES': false,
* }
* ```
* @typedef {Object} PermissionOverwriteOptions
@@ -108,8 +108,8 @@ class PermissionOverwrites extends Base {
/**
* @typedef {Object} ResolvedOverwriteOptions
* @property {PermissionsBitField} allow The allowed permissions
* @property {PermissionsBitField} deny The denied permissions
* @property {Permissions} allow The allowed permissions
* @property {Permissions} deny The denied permissions
*/
/**
@@ -119,8 +119,8 @@ class PermissionOverwrites extends Base {
* @returns {ResolvedOverwriteOptions}
*/
static resolveOverwriteOptions(options, { allow, deny } = {}) {
allow = new PermissionsBitField(allow);
deny = new PermissionsBitField(deny);
allow = new Permissions(allow);
deny = new Permissions(deny);
for (const [perm, value] of Object.entries(options)) {
if (value === true) {
@@ -171,24 +171,24 @@ class PermissionOverwrites extends Base {
*/
static resolve(overwrite, guild) {
if (overwrite instanceof this) return overwrite.toJSON();
if (typeof overwrite.id === 'string' && overwrite.type in OverwriteType) {
if (typeof overwrite.id === 'string' && overwrite.type in OverwriteTypes) {
return {
id: overwrite.id,
type: overwrite.type,
allow: PermissionsBitField.resolve(overwrite.allow ?? PermissionsBitField.defaultBit).toString(),
deny: PermissionsBitField.resolve(overwrite.deny ?? PermissionsBitField.defaultBit).toString(),
type: OverwriteTypes[overwrite.type],
allow: Permissions.resolve(overwrite.allow ?? Permissions.defaultBit).toString(),
deny: Permissions.resolve(overwrite.deny ?? Permissions.defaultBit).toString(),
};
}
const userOrRole = guild.roles.resolve(overwrite.id) ?? guild.client.users.resolve(overwrite.id);
if (!userOrRole) throw new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role');
const type = userOrRole instanceof Role ? OverwriteType.Role : OverwriteType.Member;
const type = userOrRole instanceof Role ? OverwriteTypes.role : OverwriteTypes.member;
return {
id: userOrRole.id,
type,
allow: PermissionsBitField.resolve(overwrite.allow ?? PermissionsBitField.defaultBit).toString(),
deny: PermissionsBitField.resolve(overwrite.deny ?? PermissionsBitField.defaultBit).toString(),
allow: Permissions.resolve(overwrite.allow ?? Permissions.defaultBit).toString(),
deny: Permissions.resolve(overwrite.deny ?? Permissions.defaultBit).toString(),
};
}
}

View File

@@ -2,7 +2,8 @@
const Base = require('./Base');
const { Emoji } = require('./Emoji');
const ActivityFlagsBitField = require('../util/ActivityFlagsBitField');
const ActivityFlags = require('../util/ActivityFlags');
const { ActivityTypes } = require('../util/Constants');
const Util = require('../util/Util');
/**
@@ -167,7 +168,7 @@ class Activity {
* The activity status's type
* @type {ActivityType}
*/
this.type = data.type;
this.type = typeof data.type === 'number' ? ActivityTypes[data.type] : data.type;
/**
* If the activity is being streamed, a link to the stream
@@ -244,9 +245,9 @@ class Activity {
/**
* Flags that describe the activity
* @type {Readonly<ActivityFlagsBitField>}
* @type {Readonly<ActivityFlags>}
*/
this.flags = new ActivityFlagsBitField(data.flags).freeze();
this.flags = new ActivityFlags(data.flags).freeze();
/**
* Emoji for a custom activity
@@ -270,7 +271,7 @@ class Activity {
* Creation date of the activity
* @type {number}
*/
this.createdTimestamp = Date.parse(data.created_at);
this.createdTimestamp = new Date(data.created_at).getTime();
}
/**
@@ -346,48 +347,35 @@ class RichPresenceAssets {
/**
* Gets the URL of the small image asset
* @param {ImageURLOptions} [options={}] Options for the image URL
* @param {StaticImageURLOptions} [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);
smallImageURL({ format, size } = {}) {
return (
this.smallImage &&
this.activity.presence.client.rest.cdn.AppAsset(this.activity.applicationId, this.smallImage, {
format,
size,
})
);
}
/**
* Gets the URL of the large image asset
* @param {ImageURLOptions} [options={}] Options for the image URL
* @param {StaticImageURLOptions} [options] Options for the image URL
* @returns {?string}
*/
largeImageURL(options = {}) {
largeImageURL({ format, size } = {}) {
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;
}
if (/^spotify:/.test(this.largeImage)) {
return `https://i.scdn.co/image/${this.largeImage.slice(8)}`;
} else if (/^twitch:/.test(this.largeImage)) {
return `https://static-cdn.jtvnw.net/previews-ttv/live_user_${this.largeImage.slice(7)}.png`;
}
return this.activity.presence.client.rest.cdn.appAsset(this.activity.applicationId, this.largeImage, options);
return this.activity.presence.client.rest.cdn.AppAsset(this.activity.applicationId, this.largeImage, {
format,
size,
});
}
}

View File

@@ -2,7 +2,7 @@
const { Collection } = require('@discordjs/collection');
const Collector = require('./interfaces/Collector');
const Events = require('../util/Events');
const { Events } = require('../util/Constants');
/**
* @typedef {CollectorOptions} ReactionCollectorOptions
@@ -57,24 +57,24 @@ class ReactionCollector extends Collector {
};
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.client.on(Events.MESSAGE_REACTION_ADD, this.handleCollect);
this.client.on(Events.MESSAGE_REACTION_REMOVE, this.handleDispose);
this.client.on(Events.MESSAGE_REACTION_REMOVE_ALL, this.empty);
this.client.on(Events.MESSAGE_DELETE, this._handleMessageDeletion);
this.client.on(Events.MESSAGE_BULK_DELETE, bulkDeleteListener);
this.client.on(Events.CHANNEL_DELETE, this._handleChannelDeletion);
this.client.on(Events.THREAD_DELETE, this._handleThreadDeletion);
this.client.on(Events.GUILD_DELETE, 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.removeListener(Events.MESSAGE_REACTION_ADD, this.handleCollect);
this.client.removeListener(Events.MESSAGE_REACTION_REMOVE, this.handleDispose);
this.client.removeListener(Events.MESSAGE_REACTION_REMOVE_ALL, this.empty);
this.client.removeListener(Events.MESSAGE_DELETE, this._handleMessageDeletion);
this.client.removeListener(Events.MESSAGE_BULK_DELETE, bulkDeleteListener);
this.client.removeListener(Events.CHANNEL_DELETE, this._handleChannelDeletion);
this.client.removeListener(Events.THREAD_DELETE, this._handleThreadDeletion);
this.client.removeListener(Events.GUILD_DELETE, this._handleGuildDeletion);
this.client.decrementMaxListeners();
});

View File

@@ -1,10 +1,21 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { PermissionFlagsBits } = require('discord-api-types/v9');
const process = require('node:process');
const Base = require('./Base');
const { Error } = require('../errors');
const PermissionsBitField = require('../util/PermissionsBitField');
const Permissions = require('../util/Permissions');
const SnowflakeUtil = require('../util/SnowflakeUtil');
const Util = require('../util/Util');
let deprecationEmittedForComparePositions = false;
/**
* @type {WeakSet<Role>}
* @private
* @internal
*/
const deletedRoles = new WeakSet();
let deprecationEmittedForDeleted = false;
/**
* Represents a role on Discord.
@@ -76,9 +87,9 @@ class Role extends Base {
if ('permissions' in data) {
/**
* The permissions of the role
* @type {Readonly<PermissionsBitField>}
* @type {Readonly<Permissions>}
*/
this.permissions = new PermissionsBitField(BigInt(data.permissions)).freeze();
this.permissions = new Permissions(BigInt(data.permissions)).freeze();
}
if ('managed' in data) {
@@ -128,7 +139,7 @@ class Role extends Base {
* @readonly
*/
get createdTimestamp() {
return DiscordSnowflake.timestampFrom(this.id);
return SnowflakeUtil.timestampFrom(this.id);
}
/**
@@ -140,6 +151,36 @@ class Role extends Base {
return new Date(this.createdTimestamp);
}
/**
* Whether or not the role has been deleted
* @type {boolean}
* @deprecated This will be removed in the next major version, see https://github.com/discordjs/discord.js/issues/7091
*/
get deleted() {
if (!deprecationEmittedForDeleted) {
deprecationEmittedForDeleted = true;
process.emitWarning(
'Role#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.',
'DeprecationWarning',
);
}
return deletedRoles.has(this);
}
set deleted(value) {
if (!deprecationEmittedForDeleted) {
deprecationEmittedForDeleted = true;
process.emitWarning(
'Role#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.',
'DeprecationWarning',
);
}
if (value) deletedRoles.add(this);
else deletedRoles.delete(this);
}
/**
* The hexadecimal version of the role color, with a leading hashtag
* @type {string}
@@ -166,7 +207,7 @@ class Role extends Base {
get editable() {
if (this.managed) return false;
const clientMember = this.guild.members.resolve(this.client.user);
if (!clientMember.permissions.has(PermissionFlagsBits.ManageRoles)) return false;
if (!clientMember.permissions.has(Permissions.FLAGS.MANAGE_ROLES)) return false;
return clientMember.roles.highest.comparePositionTo(this) > 0;
}
@@ -225,7 +266,7 @@ class Role extends Base {
* 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>}
* @returns {Readonly<Permissions>}
*/
permissionsIn(channel, checkAdmin = true) {
channel = this.guild.channels.resolve(channel);
@@ -285,7 +326,7 @@ class Role extends Base {
* @returns {Promise<Role>}
* @example
* // Set the permissions of the role
* role.setPermissions([PermissionFlagsBits.KickMembers, PermissionFlagsBits.BanMembers])
* role.setPermissions([Permissions.FLAGS.KICK_MEMBERS, Permissions.FLAGS.BAN_MEMBERS])
* .then(updated => console.log(`Updated permissions to ${updated.permissions.bitfield}`))
* .catch(console.error);
* @example
@@ -358,8 +399,20 @@ class Role extends Base {
* .then(updated => console.log(`Role position: ${updated.position}`))
* .catch(console.error);
*/
setPosition(position, options = {}) {
return this.guild.roles.setPosition(this, position, options);
async setPosition(position, { relative, reason } = {}) {
const updatedRoles = await Util.setPosition(
this,
position,
relative,
this.guild._sortedRoles(),
this.client.api.guilds(this.guild.id).roles,
reason,
);
this.client.actions.GuildRolesPositionUpdate.handle({
guild_id: this.guild.id,
roles: updatedRoles,
});
return this;
}
/**
@@ -379,11 +432,12 @@ class Role extends Base {
/**
* A link to the role's icon
* @param {ImageURLOptions} [options={}] Options for the image URL
* @param {StaticImageURLOptions} [options={}] Options for the image URL
* @returns {?string}
*/
iconURL(options = {}) {
return this.icon && this.client.rest.cdn.roleIcon(this.id, this.icon, options);
iconURL({ format, size } = {}) {
if (!this.icon) return null;
return this.client.rest.cdn.RoleIcon(this.id, this.icon, format, size);
}
/**
@@ -426,9 +480,31 @@ class Role extends Base {
permissions: this.permissions.toJSON(),
};
}
/**
* Compares the positions of two roles.
* @param {Role} role1 First role to compare
* @param {Role} role2 Second role to compare
* @returns {number} Negative number if the first role's position is lower (second role's is higher),
* positive number if the first's is higher (second's is lower), 0 if equal
* @deprecated Use {@link RoleManager#comparePositions} instead.
*/
static comparePositions(role1, role2) {
if (!deprecationEmittedForComparePositions) {
process.emitWarning(
'The Role.comparePositions method is deprecated. Use RoleManager#comparePositions instead.',
'DeprecationWarning',
);
deprecationEmittedForComparePositions = true;
}
return role1.guild.roles.comparePositions(role1, role2);
}
}
exports.Role = Role;
exports.deletedRoles = deletedRoles;
/**
* @external APIRole

View File

@@ -1,12 +0,0 @@
'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;

View File

@@ -1,7 +1,17 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const process = require('node:process');
const Base = require('./Base');
const { PrivacyLevels } = require('../util/Constants');
const SnowflakeUtil = require('../util/SnowflakeUtil');
/**
* @type {WeakSet<StageInstance>}
* @private
* @internal
*/
const deletedStageInstances = new WeakSet();
let deprecationEmittedForDeleted = false;
/**
* Represents a stage instance.
@@ -48,16 +58,15 @@ class StageInstance extends Base {
if ('privacy_level' in data) {
/**
* The privacy level of the stage instance
* @type {StageInstancePrivacyLevel}
* @type {PrivacyLevel}
*/
this.privacyLevel = data.privacy_level;
this.privacyLevel = PrivacyLevels[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 {
@@ -74,6 +83,36 @@ class StageInstance extends Base {
return this.client.channels.resolve(this.channelId);
}
/**
* Whether or not the stage instance has been deleted
* @type {boolean}
* @deprecated This will be removed in the next major version, see https://github.com/discordjs/discord.js/issues/7091
*/
get deleted() {
if (!deprecationEmittedForDeleted) {
deprecationEmittedForDeleted = true;
process.emitWarning(
'StageInstance#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.',
'DeprecationWarning',
);
}
return deletedStageInstances.has(this);
}
set deleted(value) {
if (!deprecationEmittedForDeleted) {
deprecationEmittedForDeleted = true;
process.emitWarning(
'StageInstance#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.',
'DeprecationWarning',
);
}
if (value) deletedStageInstances.add(this);
else deletedStageInstances.delete(this);
}
/**
* The guild this stage instance belongs to
* @type {?Guild}
@@ -109,6 +148,7 @@ class StageInstance extends Base {
async delete() {
await this.guild.stageInstances.delete(this.channelId);
const clone = this._clone();
deletedStageInstances.add(clone);
return clone;
}
@@ -132,7 +172,7 @@ class StageInstance extends Base {
* @readonly
*/
get createdTimestamp() {
return DiscordSnowflake.timestampFrom(this.id);
return SnowflakeUtil.timestampFrom(this.id);
}
/**
@@ -146,3 +186,4 @@ class StageInstance extends Base {
}
exports.StageInstance = StageInstance;
exports.deletedStageInstances = deletedStageInstances;

View File

@@ -1,8 +1,17 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { Routes, StickerFormatType } = require('discord-api-types/v9');
const process = require('node:process');
const Base = require('./Base');
const { StickerFormatTypes, StickerTypes } = require('../util/Constants');
const SnowflakeUtil = require('../util/SnowflakeUtil');
/**
* @type {WeakSet<StageInstance>}
* @private
* @internal
*/
const deletedStickers = new WeakSet();
let deprecationEmittedForDeleted = false;
/**
* Represents a Sticker.
@@ -37,7 +46,7 @@ class Sticker extends Base {
* The type of the sticker
* @type {?StickerType}
*/
this.type = sticker.type;
this.type = StickerTypes[sticker.type];
} else {
this.type ??= null;
}
@@ -47,7 +56,7 @@ class Sticker extends Base {
* The format of the sticker
* @type {StickerFormatType}
*/
this.format = sticker.format_type;
this.format = StickerFormatTypes[sticker.format_type];
}
if ('name' in sticker) {
@@ -125,7 +134,7 @@ class Sticker extends Base {
* @readonly
*/
get createdTimestamp() {
return DiscordSnowflake.timestampFrom(this.id);
return SnowflakeUtil.timestampFrom(this.id);
}
/**
@@ -137,6 +146,36 @@ class Sticker extends Base {
return new Date(this.createdTimestamp);
}
/**
* Whether or not the sticker has been deleted
* @type {boolean}
* @deprecated This will be removed in the next major version, see https://github.com/discordjs/discord.js/issues/7091
*/
get deleted() {
if (!deprecationEmittedForDeleted) {
deprecationEmittedForDeleted = true;
process.emitWarning(
'Sticker#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.',
'DeprecationWarning',
);
}
return deletedStickers.has(this);
}
set deleted(value) {
if (!deprecationEmittedForDeleted) {
deprecationEmittedForDeleted = true;
process.emitWarning(
'Sticker#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.',
'DeprecationWarning',
);
}
if (value) deletedStickers.add(this);
else deletedStickers.delete(this);
}
/**
* Whether this sticker is partial
* @type {boolean}
@@ -157,13 +196,11 @@ class Sticker extends Base {
/**
* 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>
* <info>If the sticker's format is 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');
return this.client.rest.cdn.Sticker(this.id, this.format);
}
/**
@@ -191,7 +228,10 @@ class Sticker extends Base {
async fetchUser() {
if (this.partial) await this.fetch();
if (!this.guildId) throw new Error('NOT_GUILD_STICKER');
return this.guild.stickers.fetchUser(this);
const data = await this.client.api.guilds(this.guildId).stickers(this.id).get();
this._patch(data);
return this.user;
}
/**
@@ -264,6 +304,7 @@ class Sticker extends Base {
}
exports.Sticker = Sticker;
exports.deletedStickers = deletedStickers;
/**
* @external APISticker

View File

@@ -1,9 +1,9 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { DiscordSnowflake } = require('@sapphire/snowflake');
const Base = require('./Base');
const { Sticker } = require('./Sticker');
const SnowflakeUtil = require('../util/SnowflakeUtil');
/**
* Represents a pack of standard stickers.
@@ -61,7 +61,7 @@ class StickerPack extends Base {
* @readonly
*/
get createdTimestamp() {
return DiscordSnowflake.timestampFrom(this.id);
return SnowflakeUtil.timestampFrom(this.id);
}
/**
@@ -84,11 +84,11 @@ class StickerPack extends Base {
/**
* The URL to this sticker pack's banner.
* @param {ImageURLOptions} [options={}] Options for the image URL
* @param {StaticImageURLOptions} [options={}] Options for the Image URL
* @returns {?string}
*/
bannerURL(options = {}) {
return this.bannerId && this.client.rest.cdn.stickerPackBanner(this.bannerId, options);
bannerURL({ format, size } = {}) {
return this.bannerId && this.client.rest.cdn.StickerPackBanner(this.bannerId, format, size);
}
}

View File

@@ -1,9 +1,9 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { DiscordSnowflake } = require('@sapphire/snowflake');
const Base = require('./Base');
const TeamMember = require('./TeamMember');
const SnowflakeUtil = require('../util/SnowflakeUtil');
/**
* Represents a Client OAuth2 Application Team.
@@ -76,7 +76,7 @@ class Team extends Base {
* @readonly
*/
get createdTimestamp() {
return DiscordSnowflake.timestampFrom(this.id);
return SnowflakeUtil.timestampFrom(this.id);
}
/**
@@ -90,11 +90,12 @@ class Team extends Base {
/**
* A link to the team's icon.
* @param {ImageURLOptions} [options={}] Options for the image URL
* @param {StaticImageURLOptions} [options={}] Options for the Image URL
* @returns {?string}
*/
iconURL(options = {}) {
return this.icon && this.client.rest.cdn.teamIcon(this.id, this.icon, options);
iconURL({ format, size } = {}) {
if (!this.icon) return null;
return this.client.rest.cdn.TeamIcon(this.id, this.icon, { format, size });
}
/**

View File

@@ -1,6 +1,7 @@
'use strict';
const Base = require('./Base');
const { MembershipStates } = require('../util/Constants');
/**
* Represents a Client OAuth2 Application Team Member.
@@ -31,9 +32,9 @@ class TeamMember extends Base {
if ('membership_state' in data) {
/**
* The permissions this Team Member has with regard to the team
* @type {TeamMemberMembershipState}
* @type {MembershipState}
*/
this.membershipState = data.membership_state;
this.membershipState = MembershipStates[data.membership_state];
}
if ('user' in data) {

View File

@@ -1,11 +1,11 @@
'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');
const Permissions = require('../util/Permissions');
/**
* Represents a thread channel on Discord.
@@ -79,7 +79,7 @@ class ThreadChannel extends Channel {
* <info>Always `null` in public threads</info>
* @type {?boolean}
*/
this.invitable = this.type === ChannelType.GuildPrivateThread ? data.thread_metadata.invitable ?? false : null;
this.invitable = this.type === 'GUILD_PRIVATE_THREAD' ? data.thread_metadata.invitable ?? false : null;
/**
* Whether the thread is archived
@@ -99,12 +99,7 @@ class ThreadChannel extends Channel {
* 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);
}
this.archiveTimestamp = new Date(data.thread_metadata.archive_timestamp).getTime();
} else {
this.locked ??= null;
this.archived ??= null;
@@ -113,8 +108,6 @@ class ThreadChannel extends Channel {
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
@@ -140,7 +133,7 @@ class ThreadChannel extends Channel {
* 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;
this.lastPinTimestamp = data.last_pin_timestamp ? new Date(data.last_pin_timestamp).getTime() : null;
} else {
this.lastPinTimestamp ??= null;
}
@@ -183,16 +176,6 @@ class ThreadChannel extends Channel {
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>}
@@ -209,16 +192,8 @@ class ThreadChannel extends Channel {
* @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);
if (!this.archiveTimestamp) return null;
return new Date(this.archiveTimestamp);
}
/**
@@ -253,7 +228,7 @@ class ThreadChannel extends Channel {
* 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>}
* @returns {?Readonly<Permissions>}
*/
permissionsFor(memberOrRole, checkAdmin) {
return this.parent?.permissionsFor(memberOrRole, checkAdmin) ?? null;
@@ -297,7 +272,7 @@ class ThreadChannel extends Channel {
* @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>
* <info>Can only be edited on `GUILD_PRIVATE_THREAD`</info>
*/
/**
@@ -322,13 +297,13 @@ class ThreadChannel extends Channel {
}
}
const newData = await this.client.api.channels(this.id).patch({
body: {
data: {
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,
invitable: this.type === 'GUILD_PRIVATE_THREAD' ? data.invitable : undefined,
},
reason,
});
@@ -377,9 +352,7 @@ class ThreadChannel extends Channel {
* @returns {Promise<ThreadChannel>}
*/
setInvitable(invitable = true, reason) {
if (this.type !== ChannelType.GuildPrivateThread) {
return Promise.reject(new RangeError('THREAD_INVITABLE_TYPE', this.type));
}
if (this.type !== 'GUILD_PRIVATE_THREAD') return Promise.reject(new RangeError('THREAD_INVITABLE_TYPE', this.type));
return this.edit({ invitable }, reason);
}
@@ -440,8 +413,7 @@ class ThreadChannel extends Channel {
*/
get editable() {
return (
(this.ownerId === this.client.user.id && (this.type !== ChannelType.GuildPrivateThread || this.joined)) ||
this.manageable
(this.ownerId === this.client.user.id && (this.type !== 'GUILD_PRIVATE_THREAD' || this.joined)) || this.manageable
);
}
@@ -455,9 +427,7 @@ class ThreadChannel extends Channel {
!this.archived &&
!this.joined &&
this.permissionsFor(this.client.user)?.has(
this.type === ChannelType.GuildPrivateThread
? PermissionFlagsBits.ManageThreads
: PermissionFlagsBits.ViewChannel,
this.type === 'GUILD_PRIVATE_THREAD' ? Permissions.FLAGS.MANAGE_THREADS : Permissions.FLAGS.VIEW_CHANNEL,
false,
)
);
@@ -472,11 +442,11 @@ class ThreadChannel extends Channel {
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 (permissions.has(Permissions.FLAGS.ADMINISTRATOR, false)) return true;
return (
this.guild.me.communicationDisabledUntilTimestamp < Date.now() &&
permissions.has(PermissionFlagsBits.ManageThreads, false)
permissions.has(Permissions.FLAGS.MANAGE_THREADS, false)
);
}
@@ -489,7 +459,7 @@ class ThreadChannel extends Channel {
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);
return permissions.has(Permissions.FLAGS.VIEW_CHANNEL, false);
}
/**
@@ -501,12 +471,12 @@ class ThreadChannel extends Channel {
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;
if (permissions.has(Permissions.FLAGS.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.type !== 'GUILD_PRIVATE_THREAD' || this.joined || this.manageable) &&
permissions.has(Permissions.FLAGS.SEND_MESSAGES_IN_THREADS, false) &&
this.guild.me.communicationDisabledUntilTimestamp < Date.now()
);
}
@@ -517,15 +487,7 @@ class ThreadChannel extends Channel {
* @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;
return this.archived && (this.locked ? this.manageable : this.sendable);
}
/**
@@ -539,7 +501,7 @@ class ThreadChannel extends Channel {
* .catch(console.error);
*/
async delete(reason) {
await this.guild.channels.delete(this.id, reason);
await this.client.api.channels(this.id).delete({ reason });
return this;
}

View File

@@ -1,7 +1,7 @@
'use strict';
const Base = require('./Base');
const ThreadMemberFlagsBitField = require('../util/ThreadMemberFlagsBitField');
const ThreadMemberFlags = require('../util/ThreadMemberFlags');
/**
* Represents a Member for a Thread.
@@ -33,14 +33,14 @@ class ThreadMember extends Base {
}
_patch(data) {
if ('join_timestamp' in data) this.joinedTimestamp = Date.parse(data.join_timestamp);
if ('join_timestamp' in data) this.joinedTimestamp = new Date(data.join_timestamp).getTime();
if ('flags' in data) {
/**
* The flags for this thread member
* @type {ThreadMemberFlagsBitField}
* @type {ThreadMemberFlags}
*/
this.flags = new ThreadMemberFlagsBitField(data.flags).freeze();
this.flags = new ThreadMemberFlags(data.flags).freeze();
}
}
@@ -59,7 +59,7 @@ class ThreadMember extends Base {
* @readonly
*/
get joinedAt() {
return this.joinedTimestamp && new Date(this.joinedTimestamp);
return this.joinedTimestamp ? new Date(this.joinedTimestamp) : null;
}
/**

View File

@@ -1,11 +1,11 @@
'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');
const { Error } = require('../errors');
const SnowflakeUtil = require('../util/SnowflakeUtil');
const UserFlags = require('../util/UserFlags');
const { default: Collection } = require('@discordjs/collection');
/**
* Represents a user on Discord.
@@ -13,22 +13,22 @@ const TextBasedChannel = require('./interfaces/TextBasedChannel');
* @extends {Base}
*/
class User extends Base {
constructor(client, data) {
super(client);
constructor(client, data) {
super(client);
/**
* The user's id
* @type {Snowflake}
*/
this.id = data.id;
/**
* The user's id
* @type {Snowflake}
*/
this.id = data.id;
this.bot = null;
this.bot = null;
this.system = null;
this.system = null;
this.flags = null;
this.flags = null;
this.friend = client.friends.cache.has(this.id);
this.friend = client.friends.cache.has(this.id);
this.blocked = client.blocked.cache.has(this.id);
@@ -38,91 +38,92 @@ class User extends Base {
this.premiumGuildSince = null;
this.mutualGuilds = new Collection();
this._patch(data);
}
this._patch(data);
}
_patch(data) {
if ('username' in data) {
/**
* The username of the user
* @type {?string}
*/
this.username = data.username;
} else {
this.username ??= null;
}
_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 ('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 ('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 ('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 ('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 ('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 ('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);
}
}
if ('public_flags' in data) {
/**
* The flags for this user
* @type {?UserFlags}
*/
this.flags = new UserFlags(data.public_flags);
}
}
// Code written by https://github.com/aiko-chan-ai
_ProfilePatch(data) {
if (!data) return;
@@ -217,224 +218,209 @@ class User extends Base {
.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);
}
/**
* Whether this User is a partial
* @type {boolean}
* @readonly
*/
get partial() {
return typeof this.username !== 'string';
}
/**
* The time the user was created at
* @type {Date}
* @readonly
*/
get createdAt() {
return new Date(this.createdTimestamp);
}
/**
* The timestamp the user was created at
* @type {number}
* @readonly
*/
get createdTimestamp() {
return SnowflakeUtil.timestampFrom(this.id);
}
/**
* A link to the user's avatar.
* @param {ImageURLOptions} [options={}] Options for the image URL
* @returns {?string}
*/
avatarURL({ format, size, dynamic } = {}) {
if (!this.avatar) return null;
return this.client.rest.cdn.Avatar(
this.id,
this.avatar,
format,
size,
dynamic,
);
}
/**
* The time the user was created at
* @type {Date}
* @readonly
*/
get createdAt() {
return new Date(this.createdTimestamp);
}
/**
* 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 avatar.
* @param {ImageURLOptions} [options={}] Options for the Image URL
* @returns {?string}
*/
avatarURL({ format, size, dynamic } = {}) {
if (!this.avatar) return null;
return this.client.rest.cdn.Avatar(this.id, this.avatar, format, size, dynamic);
}
/**
* A link to the user's default avatar
* @type {string}
* @readonly
*/
get defaultAvatarURL() {
return this.client.rest.cdn.defaultAvatar(this.discriminator % 5);
}
/**
* A link to the user's 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;
}
/**
* 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')}`;
}
/**
* 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)
);
}
/**
* A link to the user's banner.
* <info>This method will throw an error if called before the user is force fetched.
* See {@link User#banner} for more info</info>
* @param {ImageURLOptions} [options={}] Options for the Image URL
* @returns {?string}
*/
bannerURL({ format, size, dynamic } = {}) {
if (typeof this.banner === 'undefined') throw new Error('USER_BANNER_NOT_FETCHED');
if (!this.banner) return null;
return this.client.rest.cdn.Banner(this.id, this.banner, format, size, dynamic);
}
/**
* 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 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);
}
/**
* 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);
}
/**
* 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);
}
/**
* 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
);
}
/**
* 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)
);
}
/**
* 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's flags.
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<UserFlags>}
*/
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 });
}
/**
* 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}>`;
}
/**
* 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;
}
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() {}
// These are here only for documentation purposes - they are implemented by TextBasedChannel
/* eslint-disable no-empty-function */
send() {}
}
TextBasedChannel.applyToClass(User);

View File

@@ -1,12 +1,12 @@
'use strict';
const ContextMenuCommandInteraction = require('./ContextMenuCommandInteraction');
const ContextMenuInteraction = require('./ContextMenuInteraction');
/**
* Represents a user context menu interaction.
* @extends {ContextMenuCommandInteraction}
* @extends {ContextMenuInteraction}
*/
class UserContextMenuCommandInteraction extends ContextMenuCommandInteraction {
class UserContextMenuInteraction extends ContextMenuInteraction {
/**
* The user this interaction was sent from
* @type {User}
@@ -26,4 +26,4 @@ class UserContextMenuCommandInteraction extends ContextMenuCommandInteraction {
}
}
module.exports = UserContextMenuCommandInteraction;
module.exports = UserContextMenuInteraction;

View File

@@ -1,13 +1,35 @@
'use strict';
const { PermissionFlagsBits } = require('discord-api-types/v9');
const process = require('node:process');
const BaseGuildVoiceChannel = require('./BaseGuildVoiceChannel');
const Permissions = require('../util/Permissions');
let deprecationEmittedForEditable = false;
/**
* Represents a guild voice channel on Discord.
* @extends {BaseGuildVoiceChannel}
*/
class VoiceChannel extends BaseGuildVoiceChannel {
/**
* Whether the channel is editable by the client user
* @type {boolean}
* @readonly
* @deprecated Use {@link VoiceChannel#manageable} instead
*/
get editable() {
if (!deprecationEmittedForEditable) {
process.emitWarning(
'The VoiceChannel#editable getter is deprecated. Use VoiceChannel#manageable instead.',
'DeprecationWarning',
);
deprecationEmittedForEditable = true;
}
return this.manageable;
}
/**
* Whether the channel is joinable by the client user
* @type {boolean}
@@ -15,7 +37,7 @@ class VoiceChannel extends BaseGuildVoiceChannel {
*/
get joinable() {
if (!super.joinable) return false;
if (this.full && !this.permissionsFor(this.client.user).has(PermissionFlagsBits.MoveMembers, false)) return false;
if (this.full && !this.permissionsFor(this.client.user).has(Permissions.FLAGS.MOVE_MEMBERS, false)) return false;
return true;
}
@@ -28,11 +50,10 @@ class VoiceChannel extends BaseGuildVoiceChannel {
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;
if (permissions.has(Permissions.FLAGS.ADMINISTRATOR, false)) return true;
return (
this.guild.me.communicationDisabledUntilTimestamp < Date.now() &&
permissions.has(PermissionFlagsBits.Speak, false)
this.guild.me.communicationDisabledUntilTimestamp < Date.now() && permissions.has(Permissions.FLAGS.SPEAK, false)
);
}

View File

@@ -19,6 +19,12 @@ class VoiceRegion {
*/
this.name = data.name;
/**
* Whether the region is VIP-only
* @type {boolean}
*/
this.vip = data.vip;
/**
* Whether the region is deprecated
* @type {boolean}

View File

@@ -1,6 +1,5 @@
'use strict';
const { ChannelType, Routes } = require('discord-api-types/v9');
const Base = require('./Base');
const { Error, TypeError } = require('../errors');
@@ -89,7 +88,7 @@ class VoiceState extends Base {
if ('self_video' in data) {
/**
* Whether this member is streaming using "Screen Share"
* @type {?boolean}
* @type {boolean}
*/
this.streaming = data.self_stream ?? false;
} else {
@@ -109,11 +108,9 @@ class VoiceState extends Base {
if ('suppress' in data) {
/**
* Whether this member is suppressed from speaking. This property is specific to stage channels only.
* @type {?boolean}
* @type {boolean}
*/
this.suppress = data.suppress;
} else {
this.suppress ??= null;
}
if ('request_to_speak_timestamp' in data) {
@@ -121,7 +118,7 @@ class VoiceState extends Base {
* 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);
this.requestToSpeakTimestamp = new Date(data.request_to_speak_timestamp).getTime();
} else {
this.requestToSpeakTimestamp ??= null;
}
@@ -218,16 +215,16 @@ class VoiceState extends Base {
* @returns {Promise<void>}
*/
async setRequestToSpeak(request = true) {
if (this.channel?.type !== ChannelType.GuildStageVoice) throw new Error('VOICE_NOT_STAGE_CHANNEL');
if (this.channel?.type !== 'GUILD_STAGE_VOICE') throw new Error('VOICE_NOT_STAGE_CHANNEL');
if (this.client.user.id !== this.id) throw new Error('VOICE_STATE_NOT_OWN');
await this.client.api.guilds(this.guild.id, 'voice-states', '@me').patch({
body: {
data: {
channel_id: this.channelId,
request_to_speak_timestamp: request ? new Date().toISOString() : null,
}
})
},
});
}
/**
@@ -250,15 +247,15 @@ class VoiceState extends Base {
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');
if (this.channel?.type !== 'GUILD_STAGE_VOICE') throw new Error('VOICE_NOT_STAGE_CHANNEL');
const target = this.client.user.id === this.id ? '@me' : this.id;
await this.client.api.guilds(this.guild.id, 'voice-states', target).patch({
body: {
data: {
channel_id: this.channelId,
suppress: suppressed,
}
},
});
}

View File

@@ -1,499 +1,449 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { Routes, WebhookType } = require('discord-api-types/v9');
const process = require('node:process');
const MessagePayload = require('./MessagePayload');
const { Error } = require('../errors');
const { WebhookTypes } = require('../util/Constants');
const DataResolver = require('../util/DataResolver');
const DiscordAPIError = require('../rest/DiscordAPIError');
const SnowflakeUtil = require('../util/SnowflakeUtil');
let deprecationEmittedForFetchMessage = false;
/**
* 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);
}
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;
}
_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,
});
/**
* 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;
}
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;
/**
* 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 ('type' in data) {
/**
* The type of the webhook
* @type {WebhookType}
*/
this.type = WebhookTypes[data.type];
}
if ('guild_id' in data) {
/**
* The guild the webhook belongs to
* @type {Snowflake}
*/
this.guildId = data.guild_id;
}
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 ('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 ('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_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;
}
}
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>
*/
/**
* 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 {MessageEmbed[]|APIEmbed[]} [embeds] See {@link WebhookMessageOptions#embeds}
* @property {string} [content] See {@link BaseMessageOptions#content}
* @property {FileOptions[]|BufferResolvable[]|MessageAttachment[]} [files] See {@link BaseMessageOptions#files}
* @property {MessageMentionOptions} [allowedMentions] See {@link BaseMessageOptions#allowedMentions}
* @property {MessageAttachment[]} [attachments] Attachments to send with the message
* @property {MessageActionRow[]|MessageActionRowOptions[]} [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>
*/
/**
* 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');
/**
* 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;
let messagePayload;
if (options instanceof MessagePayload) {
messagePayload = options.resolveData();
} else {
messagePayload = MessagePayload.create(this, options).resolveData();
}
if (options instanceof MessagePayload) {
messagePayload = options.resolveBody();
} else {
messagePayload = MessagePayload.create(this, options).resolveBody();
}
const { data, files } = await messagePayload.resolveFiles();
const d = await this.client.api.webhooks(this.id, this.token).post({
data,
files,
query: { thread_id: messagePayload.options.threadId, wait: true },
auth: false,
});
return this.client.channels?.cache.get(d.channel_id)?.messages._add(d, false) ?? d;
}
const query = new URLSearchParams({ wait: true });
/**
* 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');
if (messagePayload.options.threadId) {
query.set('thread_id', messagePayload.options.threadId);
}
const data = await this.client.api.webhooks(this.id, this.token).slack.post({
query: { wait: true },
auth: false,
data: body,
});
return data.toString() === 'ok';
}
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
);
}
/**
* 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
*/
/**
* 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');
/**
* 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),
});
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';
}
this.name = data.name;
this.avatar = data.avatar;
this.channelId = data.channel_id;
return this;
}
/**
* 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
*/
/**
* 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>
*/
/**
* 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),
});
/**
* Gets a message that was sent by this webhook.
* @param {Snowflake|'@original'} message The id of the message to fetch
* @param {WebhookFetchMessageOptions|boolean} [cacheOrOptions={}] The options to provide to fetch the message.
* <warn>A **deprecated** boolean may be passed instead to specify whether to cache the message.</warn>
* @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, cacheOrOptions = { cache: true }) {
if (typeof cacheOrOptions === 'boolean') {
if (!deprecationEmittedForFetchMessage) {
process.emitWarning(
'Passing a boolean to cache the message in Webhook#fetchMessage is deprecated. Pass an object instead.',
'DeprecationWarning',
);
this.name = data.name;
this.avatar = data.avatar;
this.channelId = data.channel_id;
return this;
}
deprecationEmittedForFetchMessage = true;
}
/**
* 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>
*/
cacheOrOptions = { cache: cacheOrOptions };
}
/**
* 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');
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
);
}
const data = await this.client.api
.webhooks(this.id, this.token)
.messages(message)
.get({
query: {
thread_id: cacheOrOptions.threadId,
},
auth: false,
});
return this.client.channels?.cache.get(data.channel_id)?.messages._add(data, cacheOrOptions.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');
/**
* 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;
let messagePayload;
if (options instanceof MessagePayload) messagePayload = options;
else messagePayload = MessagePayload.create(this, options);
if (options instanceof MessagePayload) messagePayload = options;
else messagePayload = MessagePayload.create(this, options);
const { body, files } = await messagePayload.resolveBody().resolveFiles();
const { data, files } = await messagePayload.resolveData().resolveFiles();
const d = await this.client.api
.webhooks(this.id, this.token)
.messages(typeof message === 'string' ? message : message.id)
.patch({
body,
files,
query: messagePayload.options.threadId
? new URLSearchParams({ thread_id: messagePayload.options.threadId })
: undefined,
auth: false,
});
const d = await this.client.api
.webhooks(this.id, this.token)
.messages(typeof message === 'string' ? message : message.id)
.patch({
data,
files,
query: {
thread_id: messagePayload.options.threadId,
},
auth: false,
});
const messageManager = this.client.channels?.cache.get(
d.channel_id,
)?.messages;
if (!messageManager) return d;
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 existing = messageManager.cache.get(d.id);
if (!existing) return messageManager._add(d);
const clone = existing._clone();
clone._patch(d);
return clone;
}
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 });
}
/**
* 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');
/**
* 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,
});
}
await this.client.api
.webhooks(this.id, this.token)
.messages(typeof message === 'string' ? message : message.id)
.delete({
query: {
thread_id: threadId,
},
auth: false,
});
}
/**
* The timestamp the webhook was created at
* @type {number}
* @readonly
*/
get createdTimestamp() {
return DiscordSnowflake.timestampFrom(this.id);
}
/**
* The timestamp the webhook was created at
* @type {number}
* @readonly
*/
get createdTimestamp() {
return SnowflakeUtil.timestampFrom(this.id);
}
/**
* The time the webhook was created at
* @type {Date}
* @readonly
*/
get createdAt() {
return new Date(this.createdTimestamp);
}
/**
* 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);
}
/**
* The URL of this webhook
* @type {string}
* @readonly
*/
get url() {
return this.client.options.http.api + this.client.api.webhooks(this.id, this.token);
}
/**
* A link to the webhook's avatar.
* @param {ImageURLOptions} [options={}] Options for the image URL
* @returns {?string}
*/
avatarURL(options = {}) {
return (
this.avatar && this.client.rest.cdn.avatar(this.id, this.avatar, options)
);
}
/**
* A link to the webhook's avatar.
* @param {StaticImageURLOptions} [options={}] Options for the Image URL
* @returns {?string}
*/
avatarURL({ format, size } = {}) {
if (!this.avatar) return null;
return this.client.rest.cdn.Avatar(this.id, this.avatar, format, size);
}
/**
* Whether this webhook is created by a user.
* @returns {boolean}
*/
isUserCreated() {
return Boolean(
this.type === WebhookType.Incoming && this.owner && !this.owner.bot,
);
}
/**
* Whether or not this webhook is a channel follower webhook.
* @returns {boolean}
*/
isChannelFollower() {
return this.type === 'Channel Follower';
}
/**
* Whether this webhook is created by an application.
* @returns {boolean}
*/
isApplicationCreated() {
return this.type === WebhookType.Application;
}
/**
* Whether or not this webhook is an incoming webhook.
* @returns {boolean}
*/
isIncoming() {
return this.type === 'Incoming';
}
/**
* 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),
);
}
}
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;

View File

@@ -1,7 +1,6 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v9');
const Base = require('./Base');
const WidgetMember = require('./WidgetMember');

View File

@@ -1,8 +1,11 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { ClientApplicationAssetTypes, Endpoints } = require('../../util/Constants');
const SnowflakeUtil = require('../../util/SnowflakeUtil');
const Base = require('../Base');
const AssetTypes = Object.keys(ClientApplicationAssetTypes);
/**
* Represents an OAuth2 Application.
* @abstract
@@ -10,12 +13,13 @@ const Base = require('../Base');
class Application extends Base {
constructor(client, data) {
super(client);
this._patch(data);
if (data) {
this._patch(data);
}
}
_patch(data) {
if(!data) return;
/**
* The application's id
* @type {Snowflake}
@@ -59,7 +63,7 @@ class Application extends Base {
* @readonly
*/
get createdTimestamp() {
return DiscordSnowflake.timestampFrom(this.id);
return SnowflakeUtil.timestampFrom(this.id);
}
/**
@@ -73,20 +77,43 @@ class Application extends Base {
/**
* A link to the application's icon.
* @param {ImageURLOptions} [options={}] Options for the image URL
* @param {StaticImageURLOptions} [options={}] Options for the Image URL
* @returns {?string}
*/
iconURL(options = {}) {
return this.icon && this.client.rest.cdn.appIcon(this.id, this.icon, options);
iconURL({ format, size } = {}) {
if (!this.icon) return null;
return this.client.rest.cdn.AppIcon(this.id, this.icon, { format, size });
}
/**
* A link to this application's cover image.
* @param {ImageURLOptions} [options={}] Options for the image URL
* @param {StaticImageURLOptions} [options={}] Options for the Image URL
* @returns {?string}
*/
coverURL(options = {}) {
return this.cover && this.client.rest.cdn.appIcon(this.id, this.cover, options);
coverURL({ format, size } = {}) {
if (!this.cover) return null;
return Endpoints.CDN(this.client.options.http.cdn).AppIcon(this.id, this.cover, { format, size });
}
/**
* Asset data.
* @typedef {Object} ApplicationAsset
* @property {Snowflake} id The asset's id
* @property {string} name The asset's name
* @property {string} type The asset's type
*/
/**
* Gets the application's rich presence assets.
* @returns {Promise<Array<ApplicationAsset>>}
*/
async fetchAssets() {
const assets = await this.client.api.oauth2.applications(this.id).assets.get();
return assets.map(a => ({
id: a.id,
name: a.name,
type: AssetTypes[a.type - 1],
}));
}
/**

View File

@@ -1,7 +1,7 @@
'use strict';
const EventEmitter = require('node:events');
const { setTimeout, clearTimeout } = require('node:timers');
const { setTimeout } = require('node:timers');
const { Collection } = require('@discordjs/collection');
const { TypeError } = require('../../errors');
const Util = require('../../util/Util');
@@ -236,7 +236,7 @@ class Collector extends EventEmitter {
*/
async *[Symbol.asyncIterator]() {
const queue = [];
const onCollect = (...item) => queue.push(item);
const onCollect = item => queue.push(item);
this.on('collect', onCollect);
try {

View File

@@ -1,7 +1,8 @@
'use strict';
const { InteractionResponseType, MessageFlags, Routes } = require('discord-api-types/v9');
const { Error } = require('../../errors');
const { InteractionResponseTypes } = require('../../util/Constants');
const MessageFlags = require('../../util/MessageFlags');
const MessagePayload = require('../MessagePayload');
/**
@@ -27,8 +28,6 @@ class InteractionResponses {
* @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.
*/
/**
@@ -56,14 +55,14 @@ class InteractionResponses {
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: {
type: InteractionResponseTypes.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE,
data: {
flags: options.ephemeral ? MessageFlags.Ephemeral : undefined,
flags: options.ephemeral ? MessageFlags.FLAGS.EPHEMERAL : undefined,
},
},
auth: false,
})
});
this.deferred = true;
return options.fetchReply ? this.fetchReply() : undefined;
@@ -81,7 +80,7 @@ class InteractionResponses {
* .catch(console.error);
* @example
* // Create an ephemeral reply with an embed
* const embed = new Embed().setDescription('Pong!');
* const embed = new MessageEmbed().setDescription('Pong!');
*
* interaction.reply({ embeds: [embed], ephemeral: true })
* .then(() => console.log('Reply sent.'))
@@ -95,11 +94,11 @@ class InteractionResponses {
if (options instanceof MessagePayload) messagePayload = options;
else messagePayload = MessagePayload.create(this, options);
const { body: data, files } = await messagePayload.resolveBody().resolveFiles();
const { data, files } = await messagePayload.resolveData().resolveFiles();
await this.client.api.interactions(this.id, this.token).callback.post({
body: {
type: InteractionResponseType.ChannelMessageWithSource,
data: {
type: InteractionResponseTypes.CHANNEL_MESSAGE_WITH_SOURCE,
data,
},
files,
@@ -180,8 +179,8 @@ class InteractionResponses {
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,
data: {
type: InteractionResponseTypes.DEFERRED_MESSAGE_UPDATE,
},
auth: false,
});
@@ -210,11 +209,11 @@ class InteractionResponses {
if (options instanceof MessagePayload) messagePayload = options;
else messagePayload = MessagePayload.create(this, options);
const { body: data, files } = await messagePayload.resolveBody().resolveFiles();
const { data, files } = await messagePayload.resolveData().resolveFiles();
await this.client.api.interactions(this.id, this.token).callback.post({
body: {
type: InteractionResponseType.UpdateMessage,
data: {
type: InteractionResponseTypes.UPDATE_MESSAGE,
data,
},
files,

View File

@@ -1,377 +1,360 @@
'use strict';
const FormData = require('form-data');
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');
/* eslint-disable import/order */
const MessageCollector = require('../MessageCollector');
const MessagePayload = require('../MessagePayload');
const DiscordAPIError = require('../../rest/DiscordAPIError');
const SnowflakeUtil = require('../../util/SnowflakeUtil');
const { Collection } = require('@discordjs/collection');
const { InteractionTypes } = require('../../util/Constants');
const { TypeError, Error } = require('../../errors');
const InteractionCollector = require('../InteractionCollector');
/**
* Interface for classes that have text-channel-like features.
* @interface
*/
class TextBasedChannel {
constructor() {
/**
* A manager of the messages sent to this channel
* @type {MessageManager}
*/
this.messages = new MessageManager(this);
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 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 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 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);
}
/**
* The date when the last pinned message was pinned, if there was one
* @type {?Date}
* @readonly
*/
get lastPinAt() {
return this.lastPinTimestamp ? new Date(this.lastPinTimestamp) : null;
}
/**
* Base options provided when sending.
* @typedef {Object} BaseMessageOptions
* @property {boolean} [tts=false] Whether or not the message should be spoken aloud
* @property {string} [nonce=''] The nonce for the message
* @property {string} [content=''] The content for the message
* @property {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
*/
/**
* Base options provided when sending.
* @typedef {Object} BaseMessageOptions
* @property {boolean} [tts=false] Whether or not the message should be spoken aloud
* @property {string} [nonce=''] The nonce for the message
* @property {string} [content=''] The content for the message
* @property {MessageEmbed[]|APIEmbed[]} [embeds] The embeds for the message
* (see [here](https://discord.com/developers/docs/resources/channel#embed-object) for more details)
* @property {MessageMentionOptions} [allowedMentions] Which mentions should be parsed from the message content
* (see [here](https://discord.com/developers/docs/resources/channel#allowed-mentions-object) for more details)
* @property {FileOptions[]|BufferResolvable[]|MessageAttachment[]} [files] Files to send with the message
* @property {MessageActionRow[]|MessageActionRowOptions[]} [components]
* Action rows containing interactive components for the message (buttons, select menus)
* @property {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 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
*/
/**
* 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
*/
/**
* 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
*/
/**
* 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
*/
/**
* @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)
*/
/**
* 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=true] Whether to error if the referenced message
* does not exist (creates a standard message in this case when false)
*/
/**
* Sends a message to this channel.
* @param {string|MessagePayload|MessageOptions} options The options to provide
* @returns {Promise<Message>}
* @example
* // Send a basic message
* channel.send('hello!')
* .then(message => console.log(`Sent message: ${message.content}`))
* .catch(console.error);
* @example
* // Send a remote file
* channel.send({
* files: ['https://cdn.discordapp.com/icons/222078108977594368/6e1019b3179d71046e463a75915e7244.png?size=2048']
* })
* .then(console.log)
* .catch(console.error);
* @example
* // Send a local file
* channel.send({
* files: [{
* attachment: 'entire/path/to/file.jpg',
* name: 'file.jpg',
* 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');
/**
* 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) {
const User = require('../User');
const { GuildMember } = require('../GuildMember');
if (this instanceof User || this instanceof GuildMember) {
const dm = await this.createDM();
return dm.send(options);
}
if (this instanceof User || this instanceof GuildMember) {
const dm = await this.createDM();
return dm.send(options);
}
let messagePayload;
let messagePayload;
if (options instanceof MessagePayload) {
messagePayload = options.resolveBody();
} else {
messagePayload = MessagePayload.create(this, options).resolveBody();
}
if (options instanceof MessagePayload) {
messagePayload = options.resolveData();
} else {
messagePayload = MessagePayload.create(this, options).resolveData();
}
const { body, files } = await messagePayload.resolveFiles();
console.log(body)
const d = await this.client.api.channels[this.id].messages.post({ body, files });
return this.messages.cache.get(d.id) ?? this.messages._add(d);
}
const { data, files } = await messagePayload.resolveFiles();
const d = await this.client.api.channels[this.id].messages.post({ data, files });
// Patch send message [fck :(]
/**
* 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();
}
return this.messages.cache.get(d.id) ?? this.messages._add(d);
}
/**
* 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);
}
/**
* 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();
}
/**
* 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
*/
/**
* 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);
}
/**
* 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);
}
});
});
}
/**
* 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
*/
/**
* 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,
});
}
/**
* 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);
}
});
});
}
/**
* 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));
});
});
}
/**
* Creates a button interaction collector.
* @param {MessageComponentCollectorOptions} [options={}] Options to send to the collector
* @returns {InteractionCollector}
* @example
* // Create a button interaction collector
* const filter = (interaction) => interaction.customId === 'button' && interaction.user.id === 'someId';
* const collector = channel.createMessageComponentCollector({ filter, time: 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: InteractionTypes.MESSAGE_COMPONENT,
channel: this,
});
}
/**
* 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');
}
/**
* 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));
});
});
}
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),
);
}
}
/**
* Bulk deletes given messages that are newer than two weeks.
* @param {Collection<Snowflake, Message>|MessageResolvable[]|number} messages
* Messages or number of messages to delete
* @param {boolean} [filterOld=false] Filter messages to remove those which are older than two weeks automatically
* @returns {Promise<Collection<Snowflake, Message>>} Returns the deleted messages
* @example
* // Bulk delete messages
* channel.bulkDelete(5)
* .then(messages => console.log(`Bulk deleted ${messages.size} messages`))
* .catch(console.error);
*/
async bulkDelete(messages, filterOld = false) {
if (Array.isArray(messages) || messages instanceof Collection) {
let messageIds = messages instanceof Collection ? [...messages.keys()] : messages.map(m => m.id ?? m);
if (filterOld) {
messageIds = messageIds.filter(id => Date.now() - SnowflakeUtil.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({ data: { messages: messageIds } });
return messageIds.reduce(
(col, id) =>
col.set(
id,
this.client.actions.MessageDeleteBulk.getMessage(
{
message_id: id,
},
this,
),
),
new Collection(),
);
}
if (!isNaN(messages)) {
const msgs = await this.messages.fetch({ limit: messages });
return this.bulkDelete(msgs, filterOld);
}
throw new TypeError('MESSAGE_BULK_DELETE_TYPE');
}
static applyToClass(structure, full = false, ignore = []) {
const props = ['send'];
if (full) {
props.push(
'lastMessage',
'lastPinAt',
'bulkDelete',
'sendTyping',
'createMessageCollector',
'awaitMessages',
'createMessageComponentCollector',
'awaitMessageComponent',
);
}
for (const prop of props) {
if (ignore.includes(prop)) continue;
Object.defineProperty(
structure.prototype,
prop,
Object.getOwnPropertyDescriptor(TextBasedChannel.prototype, prop),
);
}
}
}
module.exports = TextBasedChannel;
// Fixes Circular
// eslint-disable-next-line import/order
const MessageManager = require('../../managers/MessageManager');