Downgrade to v13
[vi] cảm giác đau khổ
This commit is contained in:
@@ -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;
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
195
src/structures/BaseCommandInteraction.js
Normal file
195
src/structures/BaseCommandInteraction.js
Normal 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}
|
||||
*/
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
*/
|
||||
|
||||
|
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
103
src/structures/BaseMessageComponent.js
Normal file
103
src/structures/BaseMessageComponent.js
Normal 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;
|
@@ -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;
|
@@ -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;
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
@@ -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) {
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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 });
|
||||
|
@@ -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}
|
||||
*/
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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;
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
@@ -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
@@ -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;
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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) {
|
||||
|
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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();
|
||||
});
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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) {
|
||||
|
@@ -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;
|
||||
|
101
src/structures/MessageActionRow.js
Normal file
101
src/structures/MessageActionRow.js
Normal 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}
|
||||
*/
|
@@ -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;
|
||||
|
165
src/structures/MessageButton.js
Normal file
165
src/structures/MessageButton.js
Normal 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;
|
@@ -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();
|
||||
});
|
||||
}
|
||||
|
@@ -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}
|
||||
*/
|
||||
|
@@ -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;
|
20
src/structures/MessageContextMenuInteraction.js
Normal file
20
src/structures/MessageContextMenuInteraction.js
Normal 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;
|
575
src/structures/MessageEmbed.js
Normal file
575
src/structures/MessageEmbed.js
Normal 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}
|
||||
*/
|
@@ -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;
|
||||
|
@@ -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}
|
||||
*/
|
@@ -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) {
|
||||
|
212
src/structures/MessageSelectMenu.js
Normal file
212
src/structures/MessageSelectMenu.js
Normal 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;
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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() {
|
||||
|
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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();
|
||||
});
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
@@ -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;
|
||||
|
@@ -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
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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 });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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) {
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -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}
|
||||
|
@@ -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,
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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');
|
||||
|
||||
|
@@ -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],
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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 {
|
||||
|
@@ -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,
|
||||
|
@@ -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');
|
||||
|
Reference in New Issue
Block a user