* health check

* Update Dockerfile

* simplifying the deployment

* Update Bot.js

makes the find team command public

* test (#9)

* Dev (#7)

* health check

* Update Dockerfile

* simplifying the deployment

* Dev (#8)

* health check

* Update Dockerfile

* simplifying the deployment

* Update Bot.js

makes the find team command public

* Update PlayerService.js

* massive update????

could break stuff

* Update Bot.js

update
This commit is contained in:
VinceC
2025-07-07 21:38:19 -05:00
committed by GitHub
parent 0c86148835
commit 3453be6947
1742 changed files with 28844 additions and 67711 deletions

View File

@@ -18,6 +18,7 @@ const ClientPresence = require('../structures/ClientPresence');
const GuildPreview = require('../structures/GuildPreview');
const GuildTemplate = require('../structures/GuildTemplate');
const Invite = require('../structures/Invite');
const { SoundboardSound } = require('../structures/SoundboardSound');
const { Sticker } = require('../structures/Sticker');
const StickerPack = require('../structures/StickerPack');
const VoiceRegion = require('../structures/VoiceRegion');
@@ -107,20 +108,20 @@ class Client extends BaseClient {
: null;
/**
* All of the {@link User} objects that have been cached at any point, mapped by their ids
* The user manager of this client
* @type {UserManager}
*/
this.users = new UserManager(this);
/**
* All of the guilds the client is currently handling, mapped by their ids -
* A manager of all the guilds the client is currently handling -
* as long as sharding isn't being used, this will be *every* guild the bot is a member of
* @type {GuildManager}
*/
this.guilds = new GuildManager(this);
/**
* All of the {@link BaseChannel}s that the client is currently handling, mapped by their ids -
* All of the {@link BaseChannel}s that the client is currently handling -
* as long as sharding isn't being used, this will be *every* channel in *every* guild the bot
* is a member of. Note that DM channels will not be initially cached, and thus not be present
* in the Manager without their explicit fetching or use.
@@ -174,7 +175,7 @@ class Client extends BaseClient {
}
/**
* All custom emojis that the client has access to, mapped by their ids
* A manager of all the custom emojis that the client has access to
* @type {BaseGuildEmojiManager}
* @readonly
*/
@@ -276,7 +277,6 @@ class Client extends BaseClient {
const code = resolveInviteCode(invite);
const query = makeURLSearchParams({
with_counts: true,
with_expiration: true,
guild_scheduled_event_id: options?.guildScheduledEventId,
});
const data = await this.rest.get(Routes.invite(code), { query });
@@ -390,6 +390,19 @@ class Client extends BaseClient {
return this.fetchStickerPacks();
}
/**
* Obtains the list of default soundboard sounds.
* @returns {Promise<Collection<string, SoundboardSound>>}
* @example
* client.fetchDefaultSoundboardSounds()
* .then(sounds => console.log(`Available soundboard sounds are: ${sounds.map(sound => sound.name).join(', ')}`))
* .catch(console.error);
*/
async fetchDefaultSoundboardSounds() {
const data = await this.rest.get(Routes.soundboardDefaultSounds());
return new Collection(data.map(sound => [sound.sound_id, new SoundboardSound(this, sound)]));
}
/**
* Obtains a guild preview from Discord, available for all guilds the bot is in and all Discoverable guilds.
* @param {GuildResolvable} guild The guild to fetch the preview for

View File

@@ -112,6 +112,10 @@ class GenericAction {
return this.getPayload({ user_id: id }, manager, id, Partials.ThreadMember, false);
}
getSoundboardSound(data, guild) {
return this.getPayload(data, guild.soundboardSounds, data.sound_id, Partials.SoundboardSound);
}
spreadInjectedData(data) {
return Object.fromEntries(Object.getOwnPropertySymbols(data).map(symbol => [symbol, data[symbol]]));
}

View File

@@ -43,6 +43,7 @@ class ActionsManager {
this.register(require('./GuildScheduledEventUpdate'));
this.register(require('./GuildScheduledEventUserAdd'));
this.register(require('./GuildScheduledEventUserRemove'));
this.register(require('./GuildSoundboardSoundDelete.js'));
this.register(require('./GuildStickerCreate'));
this.register(require('./GuildStickerDelete'));
this.register(require('./GuildStickerUpdate'));

View File

@@ -9,6 +9,7 @@ const ChatInputCommandInteraction = require('../../structures/ChatInputCommandIn
const MentionableSelectMenuInteraction = require('../../structures/MentionableSelectMenuInteraction');
const MessageContextMenuCommandInteraction = require('../../structures/MessageContextMenuCommandInteraction');
const ModalSubmitInteraction = require('../../structures/ModalSubmitInteraction');
const PrimaryEntryPointCommandInteraction = require('../../structures/PrimaryEntryPointCommandInteraction');
const RoleSelectMenuInteraction = require('../../structures/RoleSelectMenuInteraction');
const StringSelectMenuInteraction = require('../../structures/StringSelectMenuInteraction');
const UserContextMenuCommandInteraction = require('../../structures/UserContextMenuCommandInteraction');
@@ -38,6 +39,9 @@ class InteractionCreateAction extends Action {
if (channel && !channel.isTextBased()) return;
InteractionClass = MessageContextMenuCommandInteraction;
break;
case ApplicationCommandType.PrimaryEntryPoint:
InteractionClass = PrimaryEntryPointCommandInteraction;
break;
default:
client.emit(
Events.Debug,

View File

@@ -6,7 +6,11 @@ const Events = require('../../util/Events');
class MessageCreateAction extends Action {
handle(data) {
const client = this.client;
const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id, author: data.author });
const channel = this.getChannel({
id: data.channel_id,
author: data.author,
...('guild_id' in data && { guild_id: data.guild_id }),
});
if (channel) {
if (!channel.isTextBased()) return {};

View File

@@ -6,7 +6,7 @@ const Events = require('../../util/Events');
class MessageDeleteAction extends Action {
handle(data) {
const client = this.client;
const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id });
const channel = this.getChannel({ id: data.channel_id, ...('guild_id' in data && { guild_id: data.guild_id }) });
let message;
if (channel) {
if (!channel.isTextBased()) return {};

View File

@@ -5,7 +5,7 @@ const Events = require('../../util/Events');
class MessagePollVoteAddAction extends Action {
handle(data) {
const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id });
const channel = this.getChannel({ id: data.channel_id, ...('guild_id' in data && { guild_id: data.guild_id }) });
if (!channel?.isTextBased()) return false;
const message = this.getMessage(data, channel);

View File

@@ -5,7 +5,7 @@ const Events = require('../../util/Events');
class MessagePollVoteRemoveAction extends Action {
handle(data) {
const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id });
const channel = this.getChannel({ id: data.channel_id, ...('guild_id' in data && { guild_id: data.guild_id }) });
if (!channel?.isTextBased()) return false;
const message = this.getMessage(data, channel);

View File

@@ -25,7 +25,7 @@ class MessageReactionAdd extends Action {
// Verify channel
const channel = this.getChannel({
id: data.channel_id,
guild_id: data.guild_id,
...('guild_id' in data && { guild_id: data.guild_id }),
user_id: data.user_id,
...this.spreadInjectedData(data),
});
@@ -51,6 +51,7 @@ class MessageReactionAdd extends Action {
/**
* Provides additional information about altered reaction
* @typedef {Object} MessageReactionEventDetails
* @property {ReactionType} type The type of the reaction
* @property {boolean} burst Determines whether a super reaction was used
*/
/**
@@ -60,7 +61,7 @@ class MessageReactionAdd extends Action {
* @param {User} user The user that applied the guild or reaction emoji
* @param {MessageReactionEventDetails} details Details of adding the reaction
*/
this.client.emit(Events.MessageReactionAdd, reaction, user, { burst: data.burst });
this.client.emit(Events.MessageReactionAdd, reaction, user, { type: data.type, burst: data.burst });
return { message, reaction, user };
}

View File

@@ -19,7 +19,11 @@ class MessageReactionRemove extends Action {
if (!user) return false;
// Verify channel
const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id, user_id: data.user_id });
const channel = this.getChannel({
id: data.channel_id,
...('guild_id' in data && { guild_id: data.guild_id }),
user_id: data.user_id,
});
if (!channel?.isTextBased()) return false;
// Verify message
@@ -37,7 +41,7 @@ class MessageReactionRemove extends Action {
* @param {User} user The user whose emoji or reaction emoji was removed
* @param {MessageReactionEventDetails} details Details of removing the reaction
*/
this.client.emit(Events.MessageReactionRemove, reaction, user, { burst: data.burst });
this.client.emit(Events.MessageReactionRemove, reaction, user, { type: data.type, burst: data.burst });
return { message, reaction, user };
}

View File

@@ -6,7 +6,7 @@ const Events = require('../../util/Events');
class MessageReactionRemoveAll extends Action {
handle(data) {
// Verify channel
const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id });
const channel = this.getChannel({ id: data.channel_id, ...('guild_id' in data && { guild_id: data.guild_id }) });
if (!channel?.isTextBased()) return false;
// Verify message

View File

@@ -5,7 +5,7 @@ const Events = require('../../util/Events');
class MessageReactionRemoveEmoji extends Action {
handle(data) {
const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id });
const channel = this.getChannel({ id: data.channel_id, ...('guild_id' in data && { guild_id: data.guild_id }) });
if (!channel?.isTextBased()) return false;
const message = this.getMessage(data, channel);

View File

@@ -4,7 +4,7 @@ const Action = require('./Action');
class MessageUpdateAction extends Action {
handle(data) {
const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id });
const channel = this.getChannel({ id: data.channel_id, ...('guild_id' in data && { guild_id: data.guild_id }) });
if (channel) {
if (!channel.isTextBased()) return {};

View File

@@ -2,11 +2,14 @@
const Action = require('./Action');
const Events = require('../../util/Events');
const Partials = require('../../util/Partials');
class PresenceUpdateAction extends Action {
handle(data) {
let user = this.client.users.cache.get(data.user.id);
if (!user && data.user.username) user = this.client.users._add(data.user);
if (!user && ('username' in data.user || this.client.options.partials.includes(Partials.User))) {
user = this.client.users._add(data.user);
}
if (!user) return;
if (data.user.username) {

View File

@@ -13,7 +13,7 @@ class ThreadListSyncAction extends Action {
if (data.channel_ids) {
for (const id of data.channel_ids) {
const channel = client.channels.resolve(id);
const channel = client.channels.cache.get(id);
if (channel) this.removeStale(channel);
}
} else {

View File

@@ -6,7 +6,7 @@ const Events = require('../../util/Events');
class TypingStart extends Action {
handle(data) {
const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id });
const channel = this.getChannel({ id: data.channel_id, ...('guild_id' in data && { guild_id: data.guild_id }) });
if (!channel) return;
if (!channel.isTextBased()) {

View File

@@ -36,10 +36,13 @@ const BeforeReadyWhitelist = [
const WaitingForGuildEvents = [GatewayDispatchEvents.GuildCreate, GatewayDispatchEvents.GuildDelete];
const UNRESUMABLE_CLOSE_CODES = [
CloseCodes.Normal,
GatewayCloseCodes.AlreadyAuthenticated,
GatewayCloseCodes.InvalidSeq,
const UNRECOVERABLE_CLOSE_CODES = [
GatewayCloseCodes.AuthenticationFailed,
GatewayCloseCodes.InvalidShard,
GatewayCloseCodes.ShardingRequired,
GatewayCloseCodes.InvalidAPIVersion,
GatewayCloseCodes.InvalidIntents,
GatewayCloseCodes.DisallowedIntents,
];
const reasonIsDeprecated = 'the reason property is deprecated, use the code property to determine the reason';
@@ -242,7 +245,7 @@ class WebSocketManager extends EventEmitter {
this._ws.on(WSWebSocketShardEvents.Closed, ({ code, shardId }) => {
const shard = this.shards.get(shardId);
shard.emit(WebSocketShardEvents.Close, { code, reason: reasonIsDeprecated, wasClean: true });
if (UNRESUMABLE_CLOSE_CODES.includes(code) && this.destroyed) {
if (UNRECOVERABLE_CLOSE_CODES.includes(code)) {
shard.status = Status.Disconnected;
/**
* Emitted when a shard's WebSocket disconnects and will no longer reconnect.
@@ -251,7 +254,7 @@ class WebSocketManager extends EventEmitter {
* @param {number} id The shard id that disconnected
*/
this.client.emit(Events.ShardDisconnect, { code, reason: reasonIsDeprecated, wasClean: true }, shardId);
this.debug([`Shard not resumable: ${code} (${GatewayCloseCodes[code] ?? CloseCodes[code]})`], shardId);
this.debug([`Shard not recoverable: ${code} (${GatewayCloseCodes[code] ?? CloseCodes[code]})`], shardId);
return;
}

View File

@@ -32,6 +32,10 @@ const handlers = Object.fromEntries([
['GUILD_SCHEDULED_EVENT_UPDATE', require('./GUILD_SCHEDULED_EVENT_UPDATE')],
['GUILD_SCHEDULED_EVENT_USER_ADD', require('./GUILD_SCHEDULED_EVENT_USER_ADD')],
['GUILD_SCHEDULED_EVENT_USER_REMOVE', require('./GUILD_SCHEDULED_EVENT_USER_REMOVE')],
['GUILD_SOUNDBOARD_SOUNDS_UPDATE', require('./GUILD_SOUNDBOARD_SOUNDS_UPDATE.js')],
['GUILD_SOUNDBOARD_SOUND_CREATE', require('./GUILD_SOUNDBOARD_SOUND_CREATE.js')],
['GUILD_SOUNDBOARD_SOUND_DELETE', require('./GUILD_SOUNDBOARD_SOUND_DELETE.js')],
['GUILD_SOUNDBOARD_SOUND_UPDATE', require('./GUILD_SOUNDBOARD_SOUND_UPDATE.js')],
['GUILD_STICKERS_UPDATE', require('./GUILD_STICKERS_UPDATE')],
['GUILD_UPDATE', require('./GUILD_UPDATE')],
['INTERACTION_CREATE', require('./INTERACTION_CREATE')],
@@ -50,9 +54,13 @@ const handlers = Object.fromEntries([
['PRESENCE_UPDATE', require('./PRESENCE_UPDATE')],
['READY', require('./READY')],
['RESUMED', require('./RESUMED')],
['SOUNDBOARD_SOUNDS', require('./SOUNDBOARD_SOUNDS.js')],
['STAGE_INSTANCE_CREATE', require('./STAGE_INSTANCE_CREATE')],
['STAGE_INSTANCE_DELETE', require('./STAGE_INSTANCE_DELETE')],
['STAGE_INSTANCE_UPDATE', require('./STAGE_INSTANCE_UPDATE')],
['SUBSCRIPTION_CREATE', require('./SUBSCRIPTION_CREATE')],
['SUBSCRIPTION_DELETE', require('./SUBSCRIPTION_DELETE')],
['SUBSCRIPTION_UPDATE', require('./SUBSCRIPTION_UPDATE')],
['THREAD_CREATE', require('./THREAD_CREATE')],
['THREAD_DELETE', require('./THREAD_DELETE')],
['THREAD_LIST_SYNC', require('./THREAD_LIST_SYNC')],
@@ -61,6 +69,7 @@ const handlers = Object.fromEntries([
['THREAD_UPDATE', require('./THREAD_UPDATE')],
['TYPING_START', require('./TYPING_START')],
['USER_UPDATE', require('./USER_UPDATE')],
['VOICE_CHANNEL_EFFECT_SEND', require('./VOICE_CHANNEL_EFFECT_SEND')],
['VOICE_SERVER_UPDATE', require('./VOICE_SERVER_UPDATE')],
['VOICE_STATE_UPDATE', require('./VOICE_STATE_UPDATE')],
['WEBHOOKS_UPDATE', require('./WEBHOOKS_UPDATE')],

View File

@@ -106,6 +106,7 @@
* @property {'GuildChannelUnowned'} GuildChannelUnowned
* @property {'GuildOwned'} GuildOwned
* @property {'GuildMembersTimeout'} GuildMembersTimeout
* @property {'GuildSoundboardSoundsTimeout'} GuildSoundboardSoundsTimeout
* @property {'GuildUncachedMe'} GuildUncachedMe
* @property {'ChannelNotCached'} ChannelNotCached
* @property {'StageChannelResolve'} StageChannelResolve
@@ -131,6 +132,8 @@
* @property {'MissingManageEmojisAndStickersPermission'} MissingManageEmojisAndStickersPermission
* <warn>This property is deprecated. Use `MissingManageGuildExpressionsPermission` instead.</warn>
*
* @property {'NotGuildSoundboardSound'} NotGuildSoundboardSound
* @property {'NotGuildSticker'} NotGuildSticker
* @property {'ReactionResolveUser'} ReactionResolveUser
@@ -266,6 +269,7 @@ const keys = [
'GuildChannelUnowned',
'GuildOwned',
'GuildMembersTimeout',
'GuildSoundboardSoundsTimeout',
'GuildUncachedMe',
'ChannelNotCached',
'StageChannelResolve',
@@ -290,6 +294,7 @@ const keys = [
'MissingManageGuildExpressionsPermission',
'MissingManageEmojisAndStickersPermission',
'NotGuildSoundboardSound',
'NotGuildSticker',
'ReactionResolveUser',

View File

@@ -91,11 +91,13 @@ const Messages = {
[DjsErrorCodes.GuildChannelUnowned]: "The fetched channel does not belong to this manager's guild.",
[DjsErrorCodes.GuildOwned]: 'Guild is owned by the client.',
[DjsErrorCodes.GuildMembersTimeout]: "Members didn't arrive in time.",
[DjsErrorCodes.GuildSoundboardSoundsTimeout]: "Soundboard sounds didn't arrive in time.",
[DjsErrorCodes.GuildUncachedMe]: 'The client user as a member of this guild is uncached.',
[DjsErrorCodes.ChannelNotCached]: 'Could not find the channel where this message came from in the cache!',
[DjsErrorCodes.StageChannelResolve]: 'Could not resolve channel to a stage channel.',
[DjsErrorCodes.GuildScheduledEventResolve]: 'Could not resolve the guild scheduled event.',
[DjsErrorCodes.FetchOwnerId]: type => `Couldn't resolve the ${type} ownerId to fetch the ${type} member.`,
[DjsErrorCodes.FetchOwnerId]: type =>
`Couldn't resolve the ${type} ownerId to fetch the ${type} ${type === 'group DM' ? 'owner' : 'member'}.`,
[DjsErrorCodes.InvalidType]: (name, expected, an = false) => `Supplied ${name} is not a${an ? 'n' : ''} ${expected}.`,
[DjsErrorCodes.InvalidElement]: (type, name, elem) => `Supplied ${type} ${name} includes an invalid element: ${elem}`,
@@ -117,6 +119,8 @@ const Messages = {
[DjsErrorCodes.MissingManageEmojisAndStickersPermission]: guild =>
`Client must have Manage Emojis and Stickers permission in guild ${guild} to see emoji authors.`,
[DjsErrorCodes.NotGuildSoundboardSound]: action =>
`Soundboard sound is a default (non-guild) soundboard sound and can't be ${action}.`,
[DjsErrorCodes.NotGuildSticker]: 'Sticker is a standard (non-guild) sticker and has no author.',
[DjsErrorCodes.ReactionResolveUser]: "Couldn't resolve the user id to remove from the reaction.",

19
node_modules/discord.js/src/index.js generated vendored
View File

@@ -29,7 +29,6 @@ exports.ChannelFlagsBitField = require('./util/ChannelFlagsBitField');
exports.Collection = require('@discordjs/collection').Collection;
exports.Constants = require('./util/Constants');
exports.Colors = require('./util/Colors');
__exportStar(require('./util/DataResolver.js'), exports);
exports.Events = require('./util/Events');
exports.Formatters = require('./util/Formatters');
exports.GuildMemberFlagsBitField = require('./util/GuildMemberFlagsBitField').GuildMemberFlagsBitField;
@@ -76,6 +75,7 @@ exports.GuildMemberManager = require('./managers/GuildMemberManager');
exports.GuildMemberRoleManager = require('./managers/GuildMemberRoleManager');
exports.GuildMessageManager = require('./managers/GuildMessageManager');
exports.GuildScheduledEventManager = require('./managers/GuildScheduledEventManager');
exports.GuildSoundboardSoundManager = require('./managers/GuildSoundboardSoundManager.js').GuildSoundboardSoundManager;
exports.GuildStickerManager = require('./managers/GuildStickerManager');
exports.GuildTextThreadManager = require('./managers/GuildTextThreadManager');
exports.MessageManager = require('./managers/MessageManager');
@@ -85,6 +85,7 @@ exports.ReactionManager = require('./managers/ReactionManager');
exports.ReactionUserManager = require('./managers/ReactionUserManager');
exports.RoleManager = require('./managers/RoleManager');
exports.StageInstanceManager = require('./managers/StageInstanceManager');
exports.SubscriptionManager = require('./managers/SubscriptionManager').SubscriptionManager;
exports.ThreadManager = require('./managers/ThreadManager');
exports.ThreadMemberManager = require('./managers/ThreadMemberManager');
exports.UserManager = require('./managers/UserManager');
@@ -123,12 +124,14 @@ exports.CommandInteraction = require('./structures/CommandInteraction');
exports.Collector = require('./structures/interfaces/Collector');
exports.CommandInteractionOptionResolver = require('./structures/CommandInteractionOptionResolver');
exports.Component = require('./structures/Component');
exports.ContainerComponent = require('./structures/ContainerComponent');
exports.ContextMenuCommandInteraction = require('./structures/ContextMenuCommandInteraction');
exports.DMChannel = require('./structures/DMChannel');
exports.Embed = require('./structures/Embed');
exports.EmbedBuilder = require('./structures/EmbedBuilder');
exports.Emoji = require('./structures/Emoji').Emoji;
exports.Entitlement = require('./structures/Entitlement').Entitlement;
exports.FileComponent = require('./structures/FileComponent');
exports.ForumChannel = require('./structures/ForumChannel');
exports.Guild = require('./structures/Guild').Guild;
exports.GuildAuditLogs = require('./structures/GuildAuditLogs');
@@ -146,6 +149,9 @@ exports.GuildScheduledEvent = require('./structures/GuildScheduledEvent').GuildS
exports.GuildTemplate = require('./structures/GuildTemplate');
exports.Integration = require('./structures/Integration');
exports.IntegrationApplication = require('./structures/IntegrationApplication');
exports.InteractionCallback = require('./structures/InteractionCallback');
exports.InteractionCallbackResource = require('./structures/InteractionCallbackResource');
exports.InteractionCallbackResponse = require('./structures/InteractionCallbackResponse');
exports.BaseInteraction = require('./structures/BaseInteraction');
exports.InteractionCollector = require('./structures/InteractionCollector');
exports.InteractionResponse = require('./structures/InteractionResponse');
@@ -158,6 +164,8 @@ exports.Attachment = require('./structures/Attachment');
exports.AttachmentBuilder = require('./structures/AttachmentBuilder');
exports.ModalBuilder = require('./structures/ModalBuilder');
exports.MediaChannel = require('./structures/MediaChannel');
exports.MediaGalleryComponent = require('./structures/MediaGalleryComponent');
exports.MediaGalleryItem = require('./structures/MediaGalleryItem');
exports.MessageCollector = require('./structures/MessageCollector');
exports.MessageComponentInteraction = require('./structures/MessageComponentInteraction');
exports.MessageContextMenuCommandInteraction = require('./structures/MessageContextMenuCommandInteraction');
@@ -172,11 +180,13 @@ exports.PartialGroupDMChannel = require('./structures/PartialGroupDMChannel');
exports.PermissionOverwrites = require('./structures/PermissionOverwrites');
exports.Poll = require('./structures/Poll').Poll;
exports.PollAnswer = require('./structures/PollAnswer').PollAnswer;
exports.PrimaryEntryPointCommandInteraction = require('./structures/PrimaryEntryPointCommandInteraction');
exports.Presence = require('./structures/Presence').Presence;
exports.ReactionCollector = require('./structures/ReactionCollector');
exports.ReactionEmoji = require('./structures/ReactionEmoji');
exports.RichPresenceAssets = require('./structures/Presence').RichPresenceAssets;
exports.Role = require('./structures/Role').Role;
exports.SectionComponent = require('./structures/SectionComponent');
exports.SelectMenuBuilder = require('./structures/SelectMenuBuilder');
exports.ChannelSelectMenuBuilder = require('./structures/ChannelSelectMenuBuilder');
exports.MentionableSelectMenuBuilder = require('./structures/MentionableSelectMenuBuilder');
@@ -198,23 +208,30 @@ exports.RoleSelectMenuInteraction = require('./structures/RoleSelectMenuInteract
exports.StringSelectMenuInteraction = require('./structures/StringSelectMenuInteraction');
exports.UserSelectMenuInteraction = require('./structures/UserSelectMenuInteraction');
exports.SelectMenuOptionBuilder = require('./structures/SelectMenuOptionBuilder');
exports.SeparatorComponent = require('./structures/SeparatorComponent');
exports.SKU = require('./structures/SKU').SKU;
exports.SoundboardSound = require('./structures/SoundboardSound.js').SoundboardSound;
exports.StringSelectMenuOptionBuilder = require('./structures/StringSelectMenuOptionBuilder');
exports.StageChannel = require('./structures/StageChannel');
exports.StageInstance = require('./structures/StageInstance').StageInstance;
exports.Subscription = require('./structures/Subscription').Subscription;
exports.Sticker = require('./structures/Sticker').Sticker;
exports.StickerPack = require('./structures/StickerPack');
exports.Team = require('./structures/Team');
exports.TeamMember = require('./structures/TeamMember');
exports.TextChannel = require('./structures/TextChannel');
exports.TextDisplayComponent = require('./structures/TextDisplayComponent');
exports.TextInputBuilder = require('./structures/TextInputBuilder');
exports.TextInputComponent = require('./structures/TextInputComponent');
exports.ThreadChannel = require('./structures/ThreadChannel');
exports.ThreadMember = require('./structures/ThreadMember');
exports.ThreadOnlyChannel = require('./structures/ThreadOnlyChannel');
exports.ThumbnailComponent = require('./structures/ThumbnailComponent');
exports.Typing = require('./structures/Typing');
exports.UnfurledMediaItem = require('./structures/UnfurledMediaItem');
exports.User = require('./structures/User');
exports.UserContextMenuCommandInteraction = require('./structures/UserContextMenuCommandInteraction');
exports.VoiceChannelEffect = require('./structures/VoiceChannelEffect');
exports.VoiceChannel = require('./structures/VoiceChannel');
exports.VoiceRegion = require('./structures/VoiceRegion');
exports.VoiceState = require('./structures/VoiceState');

View File

@@ -82,7 +82,7 @@ class ApplicationCommandManager extends CachedManager {
* Options used to fetch Application Commands from Discord
* @typedef {BaseFetchOptions} FetchApplicationCommandOptions
* @property {Snowflake} [guildId] The guild's id to fetch commands for, for when the guild is not cached
* @property {LocaleString} [locale] The locale to use when fetching this command
* @property {Locale} [locale] The locale to use when fetching this command
* @property {boolean} [withLocalizations] Whether to fetch all localization data
*/
@@ -261,6 +261,7 @@ class ApplicationCommandManager extends CachedManager {
dm_permission: command.dmPermission ?? command.dm_permission,
integration_types: command.integrationTypes ?? command.integration_types,
contexts: command.contexts,
handler: command.handler,
};
}
}

View File

@@ -1,6 +1,7 @@
'use strict';
const CachedManager = require('./CachedManager');
const ApplicationEmoji = require('../structures/ApplicationEmoji');
const GuildEmoji = require('../structures/GuildEmoji');
const ReactionEmoji = require('../structures/ReactionEmoji');
const { parseEmoji } = require('../util/Util');
@@ -25,7 +26,8 @@ class BaseGuildEmojiManager extends CachedManager {
* * A Snowflake
* * A GuildEmoji object
* * A ReactionEmoji object
* @typedef {Snowflake|GuildEmoji|ReactionEmoji} EmojiResolvable
* * An ApplicationEmoji object
* @typedef {Snowflake|GuildEmoji|ReactionEmoji|ApplicationEmoji} EmojiResolvable
*/
/**
@@ -34,7 +36,8 @@ class BaseGuildEmojiManager extends CachedManager {
* @returns {?GuildEmoji}
*/
resolve(emoji) {
if (emoji instanceof ReactionEmoji) return super.resolve(emoji.id);
if (emoji instanceof ReactionEmoji) return this.cache.get(emoji.id) ?? null;
if (emoji instanceof ApplicationEmoji) return this.cache.get(emoji.id) ?? null;
return super.resolve(emoji);
}
@@ -45,6 +48,7 @@ class BaseGuildEmojiManager extends CachedManager {
*/
resolveId(emoji) {
if (emoji instanceof ReactionEmoji) return emoji.id;
if (emoji instanceof ApplicationEmoji) return emoji.id;
return super.resolveId(emoji);
}
@@ -65,6 +69,7 @@ class BaseGuildEmojiManager extends CachedManager {
const emojiResolvable = this.resolve(emoji);
if (emojiResolvable) return emojiResolvable.identifier;
if (emoji instanceof ReactionEmoji) return emoji.identifier;
if (emoji instanceof ApplicationEmoji) return emoji.identifier;
if (typeof emoji === 'string') {
const res = parseEmoji(emoji);
if (res?.name.length) {

View File

@@ -69,6 +69,13 @@ class ChannelManager extends CachedManager {
channel?.parent?.threads?.cache.delete(id);
this.cache.delete(id);
if (channel?.threads) {
for (const threadId of channel.threads.cache.keys()) {
this.cache.delete(threadId);
channel.guild?.channels.cache.delete(threadId);
}
}
}
/**

View File

@@ -37,6 +37,12 @@ class EntitlementManager extends CachedManager {
* @typedef {SKU|Snowflake} SKUResolvable
*/
/**
* Options used to fetch an entitlement
* @typedef {BaseFetchOptions} FetchEntitlementOptions
* @property {EntitlementResolvable} entitlement The entitlement to fetch
*/
/**
* Options used to fetch entitlements
* @typedef {Object} FetchEntitlementsOptions
@@ -45,6 +51,7 @@ class EntitlementManager extends CachedManager {
* @property {UserResolvable} [user] The user to fetch entitlements for
* @property {SKUResolvable[]} [skus] The SKUs to fetch entitlements for
* @property {boolean} [excludeEnded] Whether to exclude ended entitlements
* @property {boolean} [excludeDeleted] Whether to exclude deleted entitlements
* @property {boolean} [cache=true] Whether to cache the fetched entitlements
* @property {Snowflake} [before] Consider only entitlements before this entitlement id
* @property {Snowflake} [after] Consider only entitlements after this entitlement id
@@ -53,21 +60,49 @@ class EntitlementManager extends CachedManager {
/**
* Fetches entitlements for this application
* @param {FetchEntitlementsOptions} [options={}] Options for fetching the entitlements
* @returns {Promise<Collection<Snowflake, Entitlement>>}
* @param {EntitlementResolvable|FetchEntitlementOptions|FetchEntitlementsOptions} [options]
* Options for fetching the entitlements
* @returns {Promise<Entitlement|Collection<Snowflake, Entitlement>>}
*/
async fetch({ limit, guild, user, skus, excludeEnded, cache = true, before, after } = {}) {
async fetch(options) {
if (!options) return this._fetchMany(options);
const { entitlement, cache, force } = options;
const resolvedEntitlement = this.resolveId(entitlement ?? options);
if (resolvedEntitlement) {
return this._fetchSingle({ entitlement: resolvedEntitlement, cache, force });
}
return this._fetchMany(options);
}
async _fetchSingle({ entitlement, cache, force = false }) {
if (!force) {
const existing = this.cache.get(entitlement);
if (existing) {
return existing;
}
}
const data = await this.client.rest.get(Routes.entitlement(this.client.application.id, entitlement));
return this._add(data, cache);
}
async _fetchMany({ limit, guild, user, skus, excludeEnded, excludeDeleted, cache, before, after } = {}) {
const query = makeURLSearchParams({
limit,
guild_id: guild && this.client.guilds.resolveId(guild),
user_id: user && this.client.users.resolveId(user),
sku_ids: skus?.map(sku => resolveSKUId(sku)).join(','),
exclude_ended: excludeEnded,
exclude_deleted: excludeDeleted,
before,
after,
});
const entitlements = await this.client.rest.get(Routes.entitlements(this.client.application.id), { query });
return entitlements.reduce(
(coll, entitlement) => coll.set(entitlement.id, this._add(entitlement, cache)),
new Collection(),

View File

@@ -97,14 +97,14 @@ class GuildBanManager extends CachedManager {
* .then(console.log)
* .catch(console.error);
*/
fetch(options) {
async fetch(options) {
if (!options) return this._fetchMany();
const { user, cache, force, limit, before, after } = options;
const resolvedUser = this.client.users.resolveId(user ?? options);
if (resolvedUser) return this._fetchSingle({ user: resolvedUser, cache, force });
if (!before && !after && !limit && cache === undefined) {
return Promise.reject(new DiscordjsError(ErrorCodes.FetchBanResolveId));
throw new DiscordjsError(ErrorCodes.FetchBanResolveId);
}
return this._fetchMany(options);
@@ -175,7 +175,7 @@ class GuildBanManager extends CachedManager {
reason: options.reason,
});
if (user instanceof GuildMember) return user;
const _user = this.client.users.resolve(id);
const _user = this.client.users.cache.get(id);
if (_user) {
return this.guild.members.resolve(_user) ?? _user;
}

View File

@@ -84,7 +84,7 @@ class GuildChannelManager extends CachedManager {
* @returns {?(GuildChannel|ThreadChannel)}
*/
resolve(channel) {
if (channel instanceof ThreadChannel) return super.resolve(channel.id);
if (channel instanceof ThreadChannel) return this.cache.get(channel.id) ?? null;
return super.resolve(channel);
}
@@ -287,7 +287,7 @@ class GuildChannelManager extends CachedManager {
const resolvedChannel = this.resolve(channel);
if (!resolvedChannel) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'channel', 'GuildChannelResolvable');
const parent = options.parent && this.client.channels.resolveId(options.parent);
const parentId = options.parent && this.client.channels.resolveId(options.parent);
if (options.position !== undefined) {
await this.setPosition(resolvedChannel, options.position, { position: options.position, reason: options.reason });
@@ -298,8 +298,8 @@ class GuildChannelManager extends CachedManager {
);
if (options.lockPermissions) {
if (parent) {
const newParent = this.guild.channels.resolve(parent);
if (parentId) {
const newParent = this.cache.get(parentId);
if (newParent?.type === ChannelType.GuildCategory) {
permission_overwrites = newParent.permissionOverwrites.cache.map(overwrite =>
PermissionOverwrites.resolve(overwrite, this.guild),
@@ -322,7 +322,7 @@ class GuildChannelManager extends CachedManager {
user_limit: options.userLimit,
rtc_region: options.rtcRegion,
video_quality_mode: options.videoQualityMode,
parent_id: parent,
parent_id: parentId,
lock_permissions: options.lockPermissions,
rate_limit_per_user: options.rateLimitPerUser,
default_auto_archive_duration: options.defaultAutoArchiveDuration,

View File

@@ -39,14 +39,14 @@ class GuildEmojiRoleManager extends DataManager {
* @param {RoleResolvable|RoleResolvable[]|Collection<Snowflake, Role>} roleOrRoles The role or roles to add
* @returns {Promise<GuildEmoji>}
*/
add(roleOrRoles) {
async add(roleOrRoles) {
if (!Array.isArray(roleOrRoles) && !(roleOrRoles instanceof Collection)) roleOrRoles = [roleOrRoles];
const resolvedRoles = [];
for (const role of roleOrRoles.values()) {
const resolvedRole = this.guild.roles.resolveId(role);
if (!resolvedRole) {
return Promise.reject(new DiscordjsTypeError(ErrorCodes.InvalidElement, 'Array or Collection', 'roles', role));
throw new DiscordjsTypeError(ErrorCodes.InvalidElement, 'Array or Collection', 'roles', role);
}
resolvedRoles.push(resolvedRole);
}
@@ -60,14 +60,14 @@ class GuildEmojiRoleManager extends DataManager {
* @param {RoleResolvable|RoleResolvable[]|Collection<Snowflake, Role>} roleOrRoles The role or roles to remove
* @returns {Promise<GuildEmoji>}
*/
remove(roleOrRoles) {
async remove(roleOrRoles) {
if (!Array.isArray(roleOrRoles) && !(roleOrRoles instanceof Collection)) roleOrRoles = [roleOrRoles];
const resolvedRoleIds = [];
for (const role of roleOrRoles.values()) {
const roleId = this.guild.roles.resolveId(role);
if (!roleId) {
return Promise.reject(new DiscordjsTypeError(ErrorCodes.InvalidElement, 'Array or Collection', 'roles', role));
throw new DiscordjsTypeError(ErrorCodes.InvalidElement, 'Array or Collection', 'roles', role);
}
resolvedRoleIds.push(roleId);
}

View File

@@ -121,22 +121,22 @@ class GuildInviteManager extends CachedManager {
* .then(console.log)
* .catch(console.error);
*/
fetch(options) {
async fetch(options) {
if (!options) return this._fetchMany();
if (typeof options === 'string') {
const code = resolveInviteCode(options);
if (!code) return Promise.reject(new DiscordjsError(ErrorCodes.InviteResolveCode));
if (!code) throw new DiscordjsError(ErrorCodes.InviteResolveCode);
return this._fetchSingle({ code, cache: true });
}
if (!options.code) {
if (options.channelId) {
const id = this.guild.channels.resolveId(options.channelId);
if (!id) return Promise.reject(new DiscordjsError(ErrorCodes.GuildChannelResolve));
if (!id) throw new DiscordjsError(ErrorCodes.GuildChannelResolve);
return this._fetchChannelMany(id, options.cache);
}
if ('cache' in options) return this._fetchMany(options.cache);
return Promise.reject(new DiscordjsError(ErrorCodes.InviteResolveCode));
throw new DiscordjsError(ErrorCodes.InviteResolveCode);
}
return this._fetchSingle({
...options,

View File

@@ -4,8 +4,9 @@ const process = require('node:process');
const { setTimeout, clearTimeout } = require('node:timers');
const { Collection } = require('@discordjs/collection');
const { makeURLSearchParams } = require('@discordjs/rest');
const { Routes, RouteBases } = require('discord-api-types/v10');
const { GatewayOpcodes, Routes, RouteBases } = require('discord-api-types/v10');
const CachedManager = require('./CachedManager');
const { ErrorCodes, DiscordjsError } = require('../errors/index.js');
const ShardClientUtil = require('../sharding/ShardClientUtil');
const { Guild } = require('../structures/Guild');
const GuildChannel = require('../structures/GuildChannel');
@@ -18,6 +19,7 @@ const { resolveImage } = require('../util/DataResolver');
const Events = require('../util/Events');
const PermissionsBitField = require('../util/PermissionsBitField');
const SystemChannelFlagsBitField = require('../util/SystemChannelFlagsBitField');
const { _transformAPIIncidentsData } = require('../util/Transformers.js');
const { resolveColor } = require('../util/Util');
let cacheWarningEmitted = false;
@@ -281,6 +283,112 @@ class GuildManager extends CachedManager {
return data.reduce((coll, guild) => coll.set(guild.id, new OAuth2Guild(this.client, guild)), new Collection());
}
/**
* @typedef {Object} FetchSoundboardSoundsOptions
* @param {Snowflake[]} guildIds The ids of the guilds to fetch soundboard sounds for
* @param {number} [time=10_000] The timeout for receipt of the soundboard sounds
*/
/**
* Fetches soundboard sounds for the specified guilds.
* @param {FetchSoundboardSoundsOptions} options The options for fetching soundboard sounds
* @returns {Promise<Collection<Snowflake, Collection<Snowflake, SoundboardSound>>>}
* @example
* // Fetch soundboard sounds for multiple guilds
* const soundboardSounds = await client.guilds.fetchSoundboardSounds({
* guildIds: ['123456789012345678', '987654321098765432'],
* })
*
* console.log(soundboardSounds.get('123456789012345678'));
*/
async fetchSoundboardSounds({ guildIds, time = 10_000 }) {
const shardCount = this.client.options.shardCount;
const shardIds = new Map();
for (const guildId of guildIds) {
const shardId = ShardClientUtil.shardIdForGuildId(guildId, shardCount);
const group = shardIds.get(shardId);
if (group) group.push(guildId);
else shardIds.set(shardId, [guildId]);
}
for (const [shardId, shardGuildIds] of shardIds) {
this.client.ws.shards.get(shardId).send({
op: GatewayOpcodes.RequestSoundboardSounds,
d: {
guild_ids: shardGuildIds,
},
});
}
return new Promise((resolve, reject) => {
const remainingGuildIds = new Set(guildIds);
const fetchedSoundboardSounds = new Collection();
const handler = (soundboardSounds, guild) => {
timeout.refresh();
if (!remainingGuildIds.has(guild.id)) return;
fetchedSoundboardSounds.set(guild.id, soundboardSounds);
remainingGuildIds.delete(guild.id);
if (remainingGuildIds.size === 0) {
clearTimeout(timeout);
this.client.removeListener(Events.SoundboardSounds, handler);
this.client.decrementMaxListeners();
resolve(fetchedSoundboardSounds);
}
};
const timeout = setTimeout(() => {
this.client.removeListener(Events.SoundboardSounds, handler);
this.client.decrementMaxListeners();
reject(new DiscordjsError(ErrorCodes.GuildSoundboardSoundsTimeout));
}, time).unref();
this.client.incrementMaxListeners();
this.client.on(Events.SoundboardSounds, handler);
});
}
/**
* Options used to set incident actions. Supplying `null` to any option will disable the action.
* @typedef {Object} IncidentActionsEditOptions
* @property {?DateResolvable} [invitesDisabledUntil] When invites should be enabled again
* @property {?DateResolvable} [dmsDisabledUntil] When direct messages should be enabled again
*/
/**
* Sets the incident actions for a guild.
* @param {GuildResolvable} guild The guild
* @param {IncidentActionsEditOptions} incidentActions The incident actions to set
* @returns {Promise<IncidentActions>}
*/
async setIncidentActions(guild, { invitesDisabledUntil, dmsDisabledUntil }) {
const guildId = this.resolveId(guild);
const data = await this.client.rest.put(Routes.guildIncidentActions(guildId), {
body: {
invites_disabled_until: invitesDisabledUntil && new Date(invitesDisabledUntil).toISOString(),
dms_disabled_until: dmsDisabledUntil && new Date(dmsDisabledUntil).toISOString(),
},
});
const parsedData = _transformAPIIncidentsData(data);
const resolvedGuild = this.resolve(guild);
if (resolvedGuild) {
resolvedGuild.incidentsData = parsedData;
}
return parsedData;
}
/**
* Returns a URL for the PNG widget of a guild.
* @param {GuildResolvable} guild The guild of the widget image

View File

@@ -54,8 +54,8 @@ class GuildMemberManager extends CachedManager {
resolve(member) {
const memberResolvable = super.resolve(member);
if (memberResolvable) return memberResolvable;
const userResolvable = this.client.users.resolveId(member);
if (userResolvable) return super.resolve(userResolvable);
const userId = this.client.users.resolveId(member);
if (userId) return this.cache.get(userId) ?? null;
return null;
}
@@ -67,8 +67,8 @@ class GuildMemberManager extends CachedManager {
resolveId(member) {
const memberResolvable = super.resolveId(member);
if (memberResolvable) return memberResolvable;
const userResolvable = this.client.users.resolveId(member);
return this.cache.has(userResolvable) ? userResolvable : null;
const userId = this.client.users.resolveId(member);
return this.cache.has(userId) ? userId : null;
}
/**
@@ -144,7 +144,7 @@ class GuildMemberManager extends CachedManager {
*/
get me() {
return (
this.resolve(this.client.user.id) ??
this.cache.get(this.client.user.id) ??
(this.client.options.partials.includes(Partials.GuildMember)
? this._add({ user: { id: this.client.user.id } }, true)
: null)
@@ -223,7 +223,7 @@ class GuildMemberManager extends CachedManager {
return this._add(data, cache);
}
_fetchMany({
async _fetchMany({
limit = 0,
withPresences: presences,
users,
@@ -231,7 +231,7 @@ class GuildMemberManager extends CachedManager {
time = 120e3,
nonce = DiscordSnowflake.generate().toString(),
} = {}) {
if (nonce.length > 32) return Promise.reject(new DiscordjsRangeError(ErrorCodes.MemberFetchNonceLength));
if (nonce.length > 32) throw new DiscordjsRangeError(ErrorCodes.MemberFetchNonceLength);
return new Promise((resolve, reject) => {
if (!query && !users) query = '';
@@ -461,7 +461,7 @@ class GuildMemberManager extends CachedManager {
*/
async kick(user, reason) {
const id = this.client.users.resolveId(user);
if (!id) return Promise.reject(new DiscordjsTypeError(ErrorCodes.InvalidType, 'user', 'UserResolvable'));
if (!id) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'user', 'UserResolvable');
await this.client.rest.delete(Routes.guildMember(this.guild.id, id), { reason });
@@ -535,7 +535,7 @@ class GuildMemberManager extends CachedManager {
*/
async addRole(options) {
const { user, role, reason } = options;
const userId = this.guild.members.resolveId(user);
const userId = this.resolveId(user);
const roleId = this.guild.roles.resolveId(role);
await this.client.rest.put(Routes.guildMemberRole(this.guild.id, userId, roleId), { reason });
@@ -549,7 +549,7 @@ class GuildMemberManager extends CachedManager {
*/
async removeRole(options) {
const { user, role, reason } = options;
const userId = this.guild.members.resolveId(user);
const userId = this.resolveId(user);
const roleId = this.guild.roles.resolveId(role);
await this.client.rest.delete(Routes.guildMemberRole(this.guild.id, userId, roleId), { reason });

View File

@@ -101,6 +101,8 @@ class GuildMemberRoleManager extends DataManager {
/**
* Adds a role (or multiple roles) to the member.
*
* <info>Uses the idempotent PUT route for singular roles, otherwise PATCHes the underlying guild member</info>
* @param {RoleResolvable|RoleResolvable[]|Collection<Snowflake, Role>} roleOrRoles The role or roles to add
* @param {string} [reason] Reason for adding the role(s)
* @returns {Promise<GuildMember>}
@@ -138,6 +140,8 @@ class GuildMemberRoleManager extends DataManager {
/**
* Removes a role (or multiple roles) from the member.
*
* <info>Uses the idempotent DELETE route for singular roles, otherwise PATCHes the underlying guild member</info>
* @param {RoleResolvable|RoleResolvable[]|Collection<Snowflake, Role>} roleOrRoles The role or roles to remove
* @param {string} [reason] Reason for removing the role(s)
* @returns {Promise<GuildMember>}

View File

@@ -7,6 +7,7 @@ const CachedManager = require('./CachedManager');
const { DiscordjsTypeError, DiscordjsError, ErrorCodes } = require('../errors');
const { GuildScheduledEvent } = require('../structures/GuildScheduledEvent');
const { resolveImage } = require('../util/DataResolver');
const { _transformGuildScheduledEventRecurrenceRule } = require('../util/Transformers');
/**
* Manages API methods for GuildScheduledEvents and stores their cache.
@@ -36,6 +37,18 @@ class GuildScheduledEventManager extends CachedManager {
* @typedef {Snowflake|GuildScheduledEvent} GuildScheduledEventResolvable
*/
/**
* Options for setting a recurrence rule for a guild scheduled event.
* @typedef {Object} GuildScheduledEventRecurrenceRuleOptions
* @property {DateResolvable} startAt The time the recurrence rule interval starts at
* @property {GuildScheduledEventRecurrenceRuleFrequency} frequency How often the event occurs
* @property {number} interval The spacing between the events
* @property {?GuildScheduledEventRecurrenceRuleWeekday[]} byWeekday The days within a week to recur on
* @property {?GuildScheduledEventRecurrenceRuleNWeekday[]} byNWeekday The days within a week to recur on
* @property {?GuildScheduledEventRecurrenceRuleMonth[]} byMonth The months to recur on
* @property {?number[]} byMonthDay The days within a month to recur on
*/
/**
* Options used to create a guild scheduled event.
* @typedef {Object} GuildScheduledEventCreateOptions
@@ -54,6 +67,8 @@ class GuildScheduledEventManager extends CachedManager {
* <warn>This is required if `entityType` is {@link GuildScheduledEventEntityType.External}</warn>
* @property {?(BufferResolvable|Base64Resolvable)} [image] The cover image of the guild scheduled event
* @property {string} [reason] The reason for creating the guild scheduled event
* @property {GuildScheduledEventRecurrenceRuleOptions} [recurrenceRule]
* The recurrence rule of the guild scheduled event
*/
/**
@@ -81,6 +96,7 @@ class GuildScheduledEventManager extends CachedManager {
entityMetadata,
reason,
image,
recurrenceRule,
} = options;
let entity_metadata, channel_id;
@@ -104,6 +120,7 @@ class GuildScheduledEventManager extends CachedManager {
entity_type: entityType,
entity_metadata,
image: image && (await resolveImage(image)),
recurrence_rule: recurrenceRule && _transformGuildScheduledEventRecurrenceRule(recurrenceRule),
},
reason,
});
@@ -153,10 +170,7 @@ class GuildScheduledEventManager extends CachedManager {
return data.reduce(
(coll, rawGuildScheduledEventData) =>
coll.set(
rawGuildScheduledEventData.id,
this.guild.scheduledEvents._add(rawGuildScheduledEventData, options.cache),
),
coll.set(rawGuildScheduledEventData.id, this._add(rawGuildScheduledEventData, options.cache)),
new Collection(),
);
}
@@ -178,6 +192,8 @@ class GuildScheduledEventManager extends CachedManager {
* {@link GuildScheduledEventEntityType.External}</warn>
* @property {?(BufferResolvable|Base64Resolvable)} [image] The cover image of the guild scheduled event
* @property {string} [reason] The reason for editing the guild scheduled event
* @property {?GuildScheduledEventRecurrenceRuleOptions} [recurrenceRule]
* The recurrence rule of the guild scheduled event
*/
/**
@@ -203,6 +219,7 @@ class GuildScheduledEventManager extends CachedManager {
entityMetadata,
reason,
image,
recurrenceRule,
} = options;
let entity_metadata;
@@ -224,6 +241,7 @@ class GuildScheduledEventManager extends CachedManager {
status,
image: image && (await resolveImage(image)),
entity_metadata,
recurrence_rule: recurrenceRule && _transformGuildScheduledEventRecurrenceRule(recurrenceRule),
},
reason,
});

View File

@@ -62,15 +62,13 @@ class PermissionOverwriteManager extends CachedManager {
* },
* ], 'Needed to change permissions');
*/
set(overwrites, reason) {
async set(overwrites, reason) {
if (!Array.isArray(overwrites) && !(overwrites instanceof Collection)) {
return Promise.reject(
new DiscordjsTypeError(
ErrorCodes.InvalidType,
'overwrites',
'Array or Collection of Permission Overwrites',
true,
),
throw new DiscordjsTypeError(
ErrorCodes.InvalidType,
'overwrites',
'Array or Collection of Permission Overwrites',
true,
);
}
return this.channel.edit({ permissionOverwrites: overwrites, reason });

View File

@@ -38,8 +38,8 @@ class PresenceManager extends CachedManager {
resolve(presence) {
const presenceResolvable = super.resolve(presence);
if (presenceResolvable) return presenceResolvable;
const UserResolvable = this.client.users.resolveId(presence);
return super.resolve(UserResolvable);
const userId = this.client.users.resolveId(presence);
return super.cache.get(userId) ?? null;
}
/**
@@ -50,8 +50,8 @@ class PresenceManager extends CachedManager {
resolveId(presence) {
const presenceResolvable = super.resolveId(presence);
if (presenceResolvable) return presenceResolvable;
const userResolvable = this.client.users.resolveId(presence);
return this.cache.has(userResolvable) ? userResolvable : null;
const userId = this.client.users.resolveId(presence);
return this.cache.has(userId) ? userId : null;
}
}

View File

@@ -97,7 +97,7 @@ class ThreadManager extends CachedManager {
* Data that can be resolved to a Date object. This can be:
* * A Date object
* * A number representing a timestamp
* * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) string
* * An {@link https://en.wikipedia.org/wiki/ISO_8601 ISO 8601} string
* @typedef {Date|number|string} DateResolvable
*/

View File

@@ -1,11 +1,15 @@
'use strict';
const process = require('node:process');
const { Collection } = require('@discordjs/collection');
const { makeURLSearchParams } = require('@discordjs/rest');
const { Routes } = require('discord-api-types/v10');
const CachedManager = require('./CachedManager');
const { DiscordjsTypeError, ErrorCodes } = require('../errors');
const ThreadMember = require('../structures/ThreadMember');
const { emitDeprecationWarningForRemoveThreadMember } = require('../util/Util');
let deprecationEmittedForAdd = false;
/**
* Manages API methods for GuildMembers and stores their cache.
@@ -53,7 +57,7 @@ class ThreadMemberManager extends CachedManager {
* @readonly
*/
get me() {
return this.resolve(this.client.user.id);
return this.cache.get(this.client.user.id) ?? null;
}
/**
@@ -71,8 +75,8 @@ class ThreadMemberManager extends CachedManager {
resolve(member) {
const memberResolvable = super.resolve(member);
if (memberResolvable) return memberResolvable;
const userResolvable = this.client.users.resolveId(member);
if (userResolvable) return super.resolve(userResolvable);
const userId = this.client.users.resolveId(member);
if (userId) return super.cache.get(userId) ?? null;
return null;
}
@@ -92,9 +96,20 @@ class ThreadMemberManager extends CachedManager {
* Adds a member to the thread.
* @param {UserResolvable|'@me'} member The member to add
* @param {string} [reason] The reason for adding this member
* <warn>This parameter is **deprecated**. Reasons cannot be used.</warn>
* @returns {Promise<Snowflake>}
*/
async add(member, reason) {
if (reason !== undefined && !deprecationEmittedForAdd) {
process.emitWarning(
// eslint-disable-next-line max-len
'The reason parameter of ThreadMemberManager#add() is deprecated as Discord does not parse them. It will be removed in the next major version.',
'DeprecationWarning',
);
deprecationEmittedForAdd = true;
}
const id = member === '@me' ? member : this.client.users.resolveId(member);
if (!id) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'member', 'UserResolvable');
await this.client.rest.put(Routes.threadMembers(this.thread.id, id), { reason });
@@ -105,9 +120,14 @@ class ThreadMemberManager extends CachedManager {
* Remove a user from the thread.
* @param {UserResolvable|'@me'} member The member to remove
* @param {string} [reason] The reason for removing this member from the thread
* <warn>This parameter is **deprecated**. Reasons cannot be used.</warn>
* @returns {Promise<Snowflake>}
*/
async remove(member, reason) {
if (reason !== undefined) {
emitDeprecationWarningForRemoveThreadMember(this.constructor.name);
}
const id = member === '@me' ? member : this.client.users.resolveId(member);
if (!id) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'member', 'UserResolvable');
await this.client.rest.delete(Routes.threadMembers(this.thread.id, id), { reason });

View File

@@ -7,6 +7,7 @@ const { GuildMember } = require('../structures/GuildMember');
const { Message } = require('../structures/Message');
const ThreadMember = require('../structures/ThreadMember');
const User = require('../structures/User');
const { emitDeprecationWarningForUserFetchFlags } = require('../util/Util');
/**
* Manages API methods for users and stores their cache.
@@ -100,8 +101,11 @@ class UserManager extends CachedManager {
* @param {UserResolvable} user The UserResolvable to identify
* @param {BaseFetchOptions} [options] Additional options for this fetch
* @returns {Promise<UserFlagsBitField>}
* @deprecated <warn>This method is deprecated and will be removed in the next major version.
* Flags may still be retrieved via {@link UserManager#fetch}.</warn>
*/
async fetchFlags(user, options) {
emitDeprecationWarningForUserFetchFlags(this.constructor.name);
return (await this.fetch(user, options)).flags;
}

View File

@@ -120,7 +120,7 @@ class Shard extends EventEmitter {
* before resolving (`-1` or `Infinity` for no wait)
* @returns {Promise<ChildProcess>}
*/
spawn(timeout = 30_000) {
async spawn(timeout = 30_000) {
if (this.process) throw new DiscordjsError(ErrorCodes.ShardingProcessExists, this.id);
if (this.worker) throw new DiscordjsError(ErrorCodes.ShardingWorkerExists, this.id);
@@ -161,7 +161,7 @@ class Shard extends EventEmitter {
*/
this.emit(ShardEvents.Spawn, child);
if (timeout === -1 || timeout === Infinity) return Promise.resolve(child);
if (timeout === -1 || timeout === Infinity) return child;
return new Promise((resolve, reject) => {
const cleanup = () => {
clearTimeout(spawnTimeoutTimer);
@@ -260,10 +260,10 @@ class Shard extends EventEmitter {
* .then(count => console.log(`${count} guilds in shard ${shard.id}`))
* .catch(console.error);
*/
fetchClientValue(prop) {
async fetchClientValue(prop) {
// Shard is dead (maybe respawning), don't cache anything and error immediately
if (!this.process && !this.worker) {
return Promise.reject(new DiscordjsError(ErrorCodes.ShardingNoChildExists, this.id));
throw new DiscordjsError(ErrorCodes.ShardingNoChildExists, this.id);
}
// Cached promise from previous call
@@ -302,13 +302,13 @@ class Shard extends EventEmitter {
* @param {*} [context] The context for the eval
* @returns {Promise<*>} Result of the script execution
*/
eval(script, context) {
async eval(script, context) {
// Stringify the script if it's a Function
const _eval = typeof script === 'function' ? `(${script})(this, ${JSON.stringify(context)})` : script;
// Shard is dead (maybe respawning), don't cache anything and error immediately
if (!this.process && !this.worker) {
return Promise.reject(new DiscordjsError(ErrorCodes.ShardingNoChildExists, this.id));
throw new DiscordjsError(ErrorCodes.ShardingNoChildExists, this.id);
}
// Cached promise from previous call

View File

@@ -8,7 +8,7 @@ const { makeError, makePlainError } = require('../util/Util');
/**
* Helper class for sharded clients spawned as a child process/worker, such as from a {@link ShardingManager}.
* Utilises IPC to send and receive data to/from the master process and other shards.
* Utilizes IPC to send and receive data to/from the master process and other shards.
*/
class ShardClientUtil {
constructor(client, mode) {
@@ -225,7 +225,8 @@ class ShardClientUtil {
* Emitted when the client encounters an error.
* <warn>Errors thrown within this event do not have a catch handler, it is
* recommended to not use async functions as `error` event handlers. See the
* [Node.js docs](https://nodejs.org/api/events.html#capture-rejections-of-promises) for details.</warn>
* {@link https://nodejs.org/api/events.html#capture-rejections-of-promises Node.js documentation}
* for details.)</warn>
* @event Client#error
* @param {Error} error The error encountered
*/

View File

@@ -14,7 +14,7 @@ const { fetchRecommendedShardCount } = require('../util/Util');
* This is a utility class that makes multi-process sharding of a bot an easy and painless experience.
* It works by spawning a self-contained {@link ChildProcess} or {@link Worker} for each individual shard, each
* containing its own instance of your bot's {@link Client}. They all have a line of communication with the master
* process, and there are several useful methods that utilise it in order to simplify tasks that are normally difficult
* process, and there are several useful methods that utilize it in order to simplify tasks that are normally difficult
* with sharding. It can spawn a specific number of shards or the amount that Discord suggests for the bot, and takes a
* path to your main bot script to launch for each one.
* @extends {EventEmitter}
@@ -256,9 +256,9 @@ class ShardingManager extends EventEmitter {
* @param {BroadcastEvalOptions} [options={}] The options for the broadcast
* @returns {Promise<*|Array<*>>} Results of the script execution
*/
broadcastEval(script, options = {}) {
async broadcastEval(script, options = {}) {
if (typeof script !== 'function') {
return Promise.reject(new DiscordjsTypeError(ErrorCodes.ShardingInvalidEvalBroadcast));
throw new DiscordjsTypeError(ErrorCodes.ShardingInvalidEvalBroadcast);
}
return this._performOnShards('eval', [`(${script})(this, ${JSON.stringify(options.context)})`], options.shard);
}
@@ -285,16 +285,16 @@ class ShardingManager extends EventEmitter {
* @returns {Promise<*|Array<*>>} Results of the method execution
* @private
*/
_performOnShards(method, args, shard) {
if (this.shards.size === 0) return Promise.reject(new DiscordjsError(ErrorCodes.ShardingNoShards));
async _performOnShards(method, args, shard) {
if (this.shards.size === 0) throw new DiscordjsError(ErrorCodes.ShardingNoShards);
if (typeof shard === 'number') {
if (this.shards.has(shard)) return this.shards.get(shard)[method](...args);
return Promise.reject(new DiscordjsError(ErrorCodes.ShardingShardNotFound, shard));
throw new DiscordjsError(ErrorCodes.ShardingShardNotFound, shard);
}
if (this.shards.size !== this.shardList.length) {
return Promise.reject(new DiscordjsError(ErrorCodes.ShardingInProcess));
throw new DiscordjsError(ErrorCodes.ShardingInProcess);
}
const promises = [];

View File

@@ -174,6 +174,18 @@ class ApplicationCommand extends Base {
this.contexts ??= null;
}
if ('handler' in data) {
/**
* Determines whether the interaction is handled by the app's interactions handler or by Discord.
* <info>Only available for {@link ApplicationCommandType.PrimaryEntryPoint} commands on
* applications with the {@link ApplicationFlags.Embedded} flag (i.e, those that have an Activity)</info>
* @type {?EntryPointCommandHandlerType}
*/
this.handler = data.handler;
} else {
this.handler ??= null;
}
if ('version' in data) {
/**
* Autoincrementing version identifier updated during substantial record changes
@@ -216,15 +228,20 @@ class ApplicationCommand extends Base {
* @property {string} name The name of the command, must be in all lowercase if type is
* {@link ApplicationCommandType.ChatInput}
* @property {Object<Locale, string>} [nameLocalizations] The localizations for the command name
* @property {string} description The description of the command, if type is {@link ApplicationCommandType.ChatInput}
* @property {string} description The description of the command,
* if type is {@link ApplicationCommandType.ChatInput} or {@link ApplicationCommandType.PrimaryEntryPoint}
* @property {boolean} [nsfw] Whether the command is age-restricted
* @property {Object<Locale, string>} [descriptionLocalizations] The localizations for the command description,
* if type is {@link ApplicationCommandType.ChatInput}
* if type is {@link ApplicationCommandType.ChatInput} or {@link ApplicationCommandType.PrimaryEntryPoint}
* @property {ApplicationCommandType} [type=ApplicationCommandType.ChatInput] The type of the command
* @property {ApplicationCommandOptionData[]} [options] Options for the command
* @property {?PermissionResolvable} [defaultMemberPermissions] The bitfield used to determine the default permissions
* a member needs in order to run the command
* @property {boolean} [dmPermission] Whether the command is enabled in DMs
* @property {ApplicationIntegrationType[]} [integrationTypes] Installation contexts where the command is available
* @property {InteractionContextType[]} [contexts] Interaction contexts where the command can be used
* @property {EntryPointCommandHandlerType} [handler] Whether the interaction is handled by the app's
* interactions handler or by Discord.
*/
/**
@@ -419,7 +436,8 @@ class ApplicationCommand extends Base {
this.descriptionLocalizations ?? {},
) ||
!isEqual(command.integrationTypes ?? command.integration_types ?? [], this.integrationTypes ?? []) ||
!isEqual(command.contexts ?? [], this.contexts ?? [])
!isEqual(command.contexts ?? [], this.contexts ?? []) ||
('handler' in command && command.handler !== this.handler)
) {
return false;
}

View File

@@ -107,6 +107,27 @@ class BaseInteraction extends Base {
(coll, entitlement) => coll.set(entitlement.id, this.client.application.entitlements._add(entitlement)),
new Collection(),
);
/* eslint-disable max-len */
/**
* Mapping of installation contexts that the interaction was authorized for the related user or guild ids
* @type {APIAuthorizingIntegrationOwnersMap}
* @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-authorizing-integration-owners-object}
*/
this.authorizingIntegrationOwners = data.authorizing_integration_owners;
/* eslint-enable max-len */
/**
* Context where the interaction was triggered from
* @type {?InteractionContextType}
*/
this.context = data.context ?? null;
/**
* Attachment size limit in bytes
* @type {number}
*/
this.attachmentSizeLimit = data.attachment_size_limit;
}
/**
@@ -204,6 +225,16 @@ class BaseInteraction extends Base {
);
}
/**
* Indicates whether this interaction is a {@link PrimaryEntryPointCommandInteraction}
* @returns {boolean}
*/
isPrimaryEntryPointCommand() {
return (
this.type === InteractionType.ApplicationCommand && this.commandType === ApplicationCommandType.PrimaryEntryPoint
);
}
/**
* Indicates whether this interaction is a {@link MessageComponentInteraction}
* @returns {boolean}

View File

@@ -9,6 +9,7 @@ const Application = require('./interfaces/Application');
const ApplicationCommandManager = require('../managers/ApplicationCommandManager');
const ApplicationEmojiManager = require('../managers/ApplicationEmojiManager');
const { EntitlementManager } = require('../managers/EntitlementManager');
const { SubscriptionManager } = require('../managers/SubscriptionManager');
const ApplicationFlagsBitField = require('../util/ApplicationFlagsBitField');
const { resolveImage } = require('../util/DataResolver');
const PermissionsBitField = require('../util/PermissionsBitField');
@@ -44,6 +45,12 @@ class ClientApplication extends Application {
* @type {EntitlementManager}
*/
this.entitlements = new EntitlementManager(this.client);
/**
* The subscription manager for this application
* @type {SubscriptionManager}
*/
this.subscriptions = new SubscriptionManager(this.client);
}
_patch(data) {
@@ -156,6 +163,17 @@ class ClientApplication extends Application {
this.approximateUserInstallCount ??= null;
}
if ('approximate_user_authorization_count' in data) {
/**
* An approximate amount of users that have OAuth2 authorizations for this application.
*
* @type {?number}
*/
this.approximateUserAuthorizationCount = data.approximate_user_authorization_count;
} else {
this.approximateUserAuthorizationCount ??= null;
}
if ('guild_id' in data) {
/**
* The id of the guild associated with this application.
@@ -166,26 +184,6 @@ class ClientApplication extends Application {
this.guildId ??= null;
}
if ('cover_image' in data) {
/**
* The hash of the application's cover image
* @type {?string}
*/
this.cover = data.cover_image;
} else {
this.cover ??= null;
}
if ('rpc_origins' in data) {
/**
* The application's RPC origins, if enabled
* @type {string[]}
*/
this.rpcOrigins = data.rpc_origins;
} else {
this.rpcOrigins ??= [];
}
if ('bot_require_code_grant' in data) {
/**
* If this application's bot requires a code grant when using the OAuth2 flow
@@ -236,6 +234,36 @@ class ClientApplication extends Application {
this.roleConnectionsVerificationURL ??= null;
}
if ('event_webhooks_url' in data) {
/**
* This application's URL to receive event webhooks
* @type {?string}
*/
this.eventWebhooksURL = data.event_webhooks_url;
} else {
this.eventWebhooksURL ??= null;
}
if ('event_webhooks_status' in data) {
/**
* This application's event webhooks status
* @type {?ApplicationWebhookEventStatus}
*/
this.eventWebhooksStatus = data.event_webhooks_status;
} else {
this.eventWebhooksStatus ??= null;
}
if ('event_webhooks_types' in data) {
/**
* List of event webhooks types this application subscribes to
* @type {?ApplicationWebhookEventType[]}
*/
this.eventWebhooksTypes = data.event_webhooks_types;
} else {
this.eventWebhooksTypes ??= null;
}
/**
* The owner of this OAuth application
* @type {?(User|Team)}
@@ -277,6 +305,10 @@ class ClientApplication extends Application {
* @property {?(BufferResolvable|Base64Resolvable)} [icon] The application's icon
* @property {?(BufferResolvable|Base64Resolvable)} [coverImage] The application's cover image
* @property {string} [interactionsEndpointURL] The application's interaction endpoint URL
* @property {string} [eventWebhooksURL] The application's event webhooks URL
* @property {ApplicationWebhookEventStatus.Enabled|ApplicationWebhookEventStatus.Disabled} [eventWebhooksStatus]
* The application's event webhooks status.
* @property {ApplicationWebhookEventType[]} [eventWebhooksTypes] The application's event webhooks types
* @property {string[]} [tags] The application's tags
*/
@@ -294,6 +326,9 @@ class ClientApplication extends Application {
icon,
coverImage,
interactionsEndpointURL,
eventWebhooksURL,
eventWebhooksStatus,
eventWebhooksTypes,
tags,
} = {}) {
const data = await this.client.rest.patch(Routes.currentApplication(), {
@@ -306,6 +341,9 @@ class ClientApplication extends Application {
icon: icon && (await resolveImage(icon)),
cover_image: coverImage && (await resolveImage(coverImage)),
interactions_endpoint_url: interactionsEndpointURL,
event_webhooks_url: eventWebhooksURL,
event_webhooks_status: eventWebhooksStatus,
event_webhooks_types: eventWebhooksTypes,
tags,
},
});

View File

@@ -64,8 +64,6 @@ class ClientUser extends User {
},
});
this.client.token = data.token;
this.client.rest.setToken(data.token);
const { updated } = this.client.actions.UserUpdate.handle(data);
return updated ?? this;
}

View File

@@ -45,21 +45,6 @@ class CommandInteraction extends BaseInteraction {
*/
this.commandGuildId = data.data.guild_id ?? null;
/* eslint-disable max-len */
/**
* Mapping of installation contexts that the interaction was authorized for the related user or guild ids
* @type {APIAuthorizingIntegrationOwnersMap}
* @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-authorizing-integration-owners-object}
*/
this.authorizingIntegrationOwners = data.authorizing_integration_owners;
/* eslint-enable max-len */
/**
* Context where the interaction was triggered from
* @type {?InteractionContextType}
*/
this.context = data.context ?? null;
/**
* Whether the reply to this interaction has been deferred
* @type {boolean}
@@ -167,6 +152,7 @@ class CommandInteraction extends BaseInteraction {
editReply() {}
deleteReply() {}
followUp() {}
launchActivity() {}
showModal() {}
sendPremiumRequired() {}
awaitModalSubmit() {}

View File

@@ -14,6 +14,15 @@ class Component {
this.data = data;
}
/**
* The id of this component
* @type {number}
* @readonly
*/
get id() {
return this.data.id;
}
/**
* The type of the component
* @type {ComponentType}

View File

@@ -73,10 +73,9 @@ class Entitlement extends Base {
if ('starts_at' in data) {
/**
* The timestamp at which this entitlement is valid
* <info>This is only `null` for test entitlements</info>
* @type {?number}
*/
this.startsTimestamp = Date.parse(data.starts_at);
this.startsTimestamp = data.starts_at ? Date.parse(data.starts_at) : null;
} else {
this.startsTimestamp ??= null;
}
@@ -84,10 +83,9 @@ class Entitlement extends Base {
if ('ends_at' in data) {
/**
* The timestamp at which this entitlement is no longer valid
* <info>This is only `null` for test entitlements</info>
* @type {?number}
*/
this.endsTimestamp = Date.parse(data.ends_at);
this.endsTimestamp = data.ends_at ? Date.parse(data.ends_at) : null;
} else {
this.endsTimestamp ??= null;
}
@@ -114,7 +112,6 @@ class Entitlement extends Base {
/**
* The start date at which this entitlement is valid
* <info>This is only `null` for test entitlements</info>
* @type {?Date}
*/
get startsAt() {
@@ -123,7 +120,6 @@ class Entitlement extends Base {
/**
* The end date at which this entitlement is no longer valid
* <info>This is only `null` for test entitlements</info>
* @type {?Date}
*/
get endsAt() {

View File

@@ -21,6 +21,7 @@ const GuildEmojiManager = require('../managers/GuildEmojiManager');
const GuildInviteManager = require('../managers/GuildInviteManager');
const GuildMemberManager = require('../managers/GuildMemberManager');
const GuildScheduledEventManager = require('../managers/GuildScheduledEventManager');
const { GuildSoundboardSoundManager } = require('../managers/GuildSoundboardSoundManager');
const GuildStickerManager = require('../managers/GuildStickerManager');
const PresenceManager = require('../managers/PresenceManager');
const RoleManager = require('../managers/RoleManager');
@@ -29,6 +30,7 @@ const VoiceStateManager = require('../managers/VoiceStateManager');
const { resolveImage } = require('../util/DataResolver');
const Status = require('../util/Status');
const SystemChannelFlagsBitField = require('../util/SystemChannelFlagsBitField');
const { _transformAPIIncidentsData } = require('../util/Transformers.js');
const { discordSort, getSortableGroupTypes, resolvePartialEmoji } = require('../util/Util');
/**
@@ -107,6 +109,12 @@ class Guild extends AnonymousGuild {
*/
this.autoModerationRules = new AutoModerationRuleManager(this);
/**
* A manager of the soundboard sounds of this guild.
* @type {GuildSoundboardSoundManager}
*/
this.soundboardSounds = new GuildSoundboardSoundManager(this);
if (!data) return;
if (data.unavailable) {
/**
@@ -470,6 +478,34 @@ class Guild extends AnonymousGuild {
stickers: data.stickers,
});
}
if ('incidents_data' in data) {
/**
* Incident actions of a guild.
* @typedef {Object} IncidentActions
* @property {?Date} invitesDisabledUntil When invites would be enabled again
* @property {?Date} dmsDisabledUntil When direct messages would be enabled again
* @property {?Date} dmSpamDetectedAt When direct message spam was detected
* @property {?Date} raidDetectedAt When a raid was detected
*/
/**
* The incidents data of this guild.
* <info>You will need to fetch the guild using {@link BaseGuild#fetch} if you want to receive
* this property.</info>
* @type {?IncidentActions}
*/
this.incidentsData = data.incidents_data && _transformAPIIncidentsData(data.incidents_data);
} else {
this.incidentsData ??= null;
}
if (data.soundboard_sounds) {
this.soundboardSounds.cache.clear();
for (const soundboardSound of data.soundboard_sounds) {
this.soundboardSounds._add(soundboardSound);
}
}
}
/**
@@ -1365,6 +1401,15 @@ class Guild extends AnonymousGuild {
return this.edit({ features });
}
/**
* Sets the incident actions for a guild.
* @param {IncidentActionsEditOptions} incidentActions The incident actions to set
* @returns {Promise<IncidentActions>}
*/
async setIncidentActions(incidentActions) {
return this.client.guilds.setIncidentActions(this.id, incidentActions);
}
/**
* Whether this guild equals another guild. It compares all properties, so for most operations
* it is advisable to just compare `guild.id === guild2.id` as it is much faster and is often

View File

@@ -32,6 +32,7 @@ const Targets = {
AutoModeration: 'AutoModeration',
GuildOnboarding: 'GuildOnboarding',
GuildOnboardingPrompt: 'GuildOnboardingPrompt',
SoundboardSound: 'SoundboardSound',
Unknown: 'Unknown',
};
@@ -53,10 +54,11 @@ const Targets = {
* * An application command
* * An auto moderation rule
* * A guild onboarding prompt
* * A soundboard sound
* * An object with an id key if target was deleted or fake entity
* * An object where the keys represent either the new value or the old value
* @typedef {?(Object|Guild|BaseChannel|User|Role|Invite|Webhook|GuildEmoji|Message|Integration|StageInstance|Sticker|
* GuildScheduledEvent|ApplicationCommand|AutoModerationRule|GuildOnboardingPrompt)} AuditLogEntryTarget
* GuildScheduledEvent|ApplicationCommand|AutoModerationRule|GuildOnboardingPrompt|SoundboardSound)} AuditLogEntryTarget
*/
/**
@@ -86,6 +88,8 @@ const Targets = {
* * ApplicationCommandPermission
* * GuildOnboarding
* * GuildOnboardingPrompt
* * SoundboardSound
* * AutoModeration
* * Unknown
* @typedef {string} AuditLogTargetType
*/
@@ -199,7 +203,6 @@ class GuildAuditLogsEntry {
case AuditLogEvent.MemberMove:
case AuditLogEvent.MessageDelete:
case AuditLogEvent.MessageBulkDelete:
this.extra = {
channel: guild.channels.cache.get(data.options.channel_id) ?? { id: data.options.channel_id },
count: Number(data.options.count),
@@ -214,6 +217,7 @@ class GuildAuditLogsEntry {
};
break;
case AuditLogEvent.MessageBulkDelete:
case AuditLogEvent.MemberDisconnect:
this.extra = {
count: Number(data.options.count),
@@ -365,10 +369,14 @@ class GuildAuditLogsEntry {
data.action_type === AuditLogEvent.OnboardingPromptCreate
? new GuildOnboardingPrompt(guild.client, changesReduce(this.changes, { id: data.target_id }), guild.id)
: changesReduce(this.changes, { id: data.target_id });
} else if (targetType === Targets.GuildOnboarding) {
this.target = changesReduce(this.changes, { id: data.target_id });
} else if (targetType === Targets.Role) {
this.target = guild.roles.cache.get(data.target_id) ?? { id: data.target_id };
} else if (targetType === Targets.Emoji) {
this.target = guild.emojis.cache.get(data.target_id) ?? { id: data.target_id };
} else if (targetType === Targets.SoundboardSound) {
this.target = guild.soundboardSounds.cache.get(data.target_id) ?? { id: data.target_id };
} else if (data.target_id) {
this.target = guild[`${targetType.toLowerCase()}s`]?.cache.get(data.target_id) ?? { id: data.target_id };
this.target = { id: data.target_id };
}
}
@@ -392,7 +400,9 @@ class GuildAuditLogsEntry {
if (target < 110) return Targets.GuildScheduledEvent;
if (target < 120) return Targets.Thread;
if (target < 130) return Targets.ApplicationCommand;
if (target >= 140 && target < 150) return Targets.AutoModeration;
if (target < 140) return Targets.SoundboardSound;
if (target < 143) return Targets.AutoModeration;
if (target < 146) return Targets.User;
if (target >= 163 && target <= 165) return Targets.GuildOnboardingPrompt;
if (target >= 160 && target < 170) return Targets.GuildOnboarding;
return Targets.Unknown;
@@ -420,6 +430,7 @@ class GuildAuditLogsEntry {
AuditLogEvent.StickerCreate,
AuditLogEvent.GuildScheduledEventCreate,
AuditLogEvent.ThreadCreate,
AuditLogEvent.SoundboardSoundCreate,
AuditLogEvent.AutoModerationRuleCreate,
AuditLogEvent.AutoModerationBlockMessage,
AuditLogEvent.OnboardingPromptCreate,
@@ -449,6 +460,7 @@ class GuildAuditLogsEntry {
AuditLogEvent.StickerDelete,
AuditLogEvent.GuildScheduledEventDelete,
AuditLogEvent.ThreadDelete,
AuditLogEvent.SoundboardSoundDelete,
AuditLogEvent.AutoModerationRuleDelete,
AuditLogEvent.OnboardingPromptDelete,
].includes(action)
@@ -473,8 +485,12 @@ class GuildAuditLogsEntry {
AuditLogEvent.StickerUpdate,
AuditLogEvent.GuildScheduledEventUpdate,
AuditLogEvent.ThreadUpdate,
AuditLogEvent.SoundboardSoundUpdate,
AuditLogEvent.ApplicationCommandPermissionUpdate,
AuditLogEvent.AutoModerationRuleUpdate,
AuditLogEvent.AutoModerationBlockMessage,
AuditLogEvent.AutoModerationFlagToChannel,
AuditLogEvent.AutoModerationUserCommunicationDisabled,
AuditLogEvent.OnboardingPromptUpdate,
AuditLogEvent.OnboardingUpdate,
].includes(action)

View File

@@ -265,8 +265,8 @@ class GuildChannel extends BaseChannel {
* Locks in the permission overwrites from the parent channel.
* @returns {Promise<GuildChannel>}
*/
lockPermissions() {
if (!this.parent) return Promise.reject(new DiscordjsError(ErrorCodes.GuildChannelOrphan));
async lockPermissions() {
if (!this.parent) throw new DiscordjsError(ErrorCodes.GuildChannelOrphan);
const permissionOverwrites = this.parent.permissionOverwrites.cache.map(overwrite => overwrite.toJSON());
return this.edit({ permissionOverwrites });
}

View File

@@ -84,6 +84,17 @@ class GuildMember extends Base {
} else if (typeof this.avatar !== 'string') {
this.avatar = null;
}
if ('banner' in data) {
/**
* The guild member's banner hash.
* @type {?string}
*/
this.banner = data.banner;
} else {
this.banner ??= null;
}
if ('joined_at' in data) this.joinedTimestamp = Date.parse(data.joined_at);
if ('premium_since' in data) {
this.premiumSinceTimestamp = data.premium_since ? Date.parse(data.premium_since) : null;
@@ -111,6 +122,20 @@ class GuildMember extends Base {
} else {
this.flags ??= new GuildMemberFlagsBitField().freeze();
}
if (data.avatar_decoration_data) {
/**
* The member avatar decoration's data
*
* @type {?AvatarDecorationData}
*/
this.avatarDecorationData = {
asset: data.avatar_decoration_data.asset,
skuId: data.avatar_decoration_data.sku_id,
};
} else {
this.avatarDecorationData = null;
}
}
_clone() {
@@ -155,6 +180,24 @@ class GuildMember extends Base {
return this.avatar && this.client.rest.cdn.guildMemberAvatar(this.guild.id, this.id, this.avatar, options);
}
/**
* A link to the member's avatar decoration.
*
* @returns {?string}
*/
avatarDecorationURL() {
return this.avatarDecorationData ? this.client.rest.cdn.avatarDecoration(this.avatarDecorationData.asset) : null;
}
/**
* A link to the member's banner.
* @param {ImageURLOptions} [options={}] Options for the banner URL
* @returns {?string}
*/
bannerURL(options = {}) {
return this.banner && this.client.rest.cdn.guildMemberBanner(this.guild.id, this.id, this.banner, options);
}
/**
* A link to the member's guild avatar if they have one.
* Otherwise, a link to their {@link User#displayAvatarURL} will be returned.
@@ -165,6 +208,26 @@ class GuildMember extends Base {
return this.avatarURL(options) ?? this.user.displayAvatarURL(options);
}
/**
* A link to the member's guild banner if they have one.
* Otherwise, a link to their {@link User#bannerURL} will be returned.
* @param {ImageURLOptions} [options={}] Options for the image URL
* @returns {?string}
*/
displayBannerURL(options) {
return this.bannerURL(options) ?? this.user.bannerURL(options);
}
/**
* A link to the member's guild avatar decoration if they have one.
* Otherwise, a link to their {@link User#avatarDecorationURL} will be returned.
*
* @returns {?string}
*/
displayAvatarDecorationURL() {
return this.avatarDecorationURL() ?? this.user.avatarDecorationURL();
}
/**
* The time this member joined the guild
* @type {?Date}
@@ -198,7 +261,7 @@ class GuildMember extends Base {
* @readonly
*/
get presence() {
return this.guild.presences.resolve(this.id);
return this.guild.presences.cache.get(this.id) ?? null;
}
/**
@@ -464,11 +527,15 @@ class GuildMember extends Base {
this.joinedTimestamp === member.joinedTimestamp &&
this.nickname === member.nickname &&
this.avatar === member.avatar &&
this.banner === member.banner &&
this.pending === member.pending &&
this.communicationDisabledUntilTimestamp === member.communicationDisabledUntilTimestamp &&
this.flags.bitfield === member.flags.bitfield &&
(this._roles === member._roles ||
(this._roles.length === member._roles.length && this._roles.every((role, i) => role === member._roles[i])))
(this._roles.length === member._roles.length &&
this._roles.every((role, index) => role === member._roles[index]))) &&
this.avatarDecorationData?.asset === member.avatarDecorationData?.asset &&
this.avatarDecorationData?.skuId === member.avatarDecorationData?.skuId
);
}
@@ -491,7 +558,10 @@ class GuildMember extends Base {
roles: true,
});
json.avatarURL = this.avatarURL();
json.bannerURL = this.bannerURL();
json.displayAvatarURL = this.displayAvatarURL();
json.displayBannerURL = this.displayBannerURL();
json.avatarDecorationURL = this.avatarDecorationURL();
return json;
}
}

View File

@@ -79,7 +79,7 @@ class GuildOnboardingPromptOption extends Base {
*/
get emoji() {
if (!this._emoji.id && !this._emoji.name) return null;
return this.client.emojis.resolve(this._emoji.id) ?? new Emoji(this.client, this._emoji);
return this.client.emojis.cache.get(this._emoji.id) ?? new Emoji(this.client, this._emoji);
}
}

View File

@@ -189,6 +189,56 @@ class GuildScheduledEvent extends Base {
} else {
this.image ??= null;
}
/**
* Represents the recurrence rule for a {@link GuildScheduledEvent}.
* @typedef {Object} GuildScheduledEventRecurrenceRule
* @property {number} startTimestamp The timestamp the recurrence rule interval starts at
* @property {Date} startAt The time the recurrence rule interval starts at
* @property {?number} endTimestamp The timestamp the recurrence rule interval ends at
* @property {?Date} endAt The time the recurrence rule interval ends at
* @property {GuildScheduledEventRecurrenceRuleFrequency} frequency How often the event occurs
* @property {number} interval The spacing between the events
* @property {?GuildScheduledEventRecurrenceRuleWeekday[]} byWeekday The days within a week to recur on
* @property {?GuildScheduledEventRecurrenceRuleNWeekday[]} byNWeekday The days within a week to recur on
* @property {?GuildScheduledEventRecurrenceRuleMonth[]} byMonth The months to recur on
* @property {?number[]} byMonthDay The days within a month to recur on
* @property {?number[]} byYearDay The days within a year to recur on
* @property {?number} count The total amount of times the event is allowed to recur before stopping
*/
/**
* @typedef {Object} GuildScheduledEventRecurrenceRuleNWeekday
* @property {number} n The week to recur on
* @property {GuildScheduledEventRecurrenceRuleWeekday} day The day within the week to recur on
*/
if ('recurrence_rule' in data) {
/**
* The recurrence rule for this scheduled event
* @type {?GuildScheduledEventRecurrenceRule}
*/
this.recurrenceRule = data.recurrence_rule && {
startTimestamp: Date.parse(data.recurrence_rule.start),
get startAt() {
return new Date(this.startTimestamp);
},
endTimestamp: data.recurrence_rule.end && Date.parse(data.recurrence_rule.end),
get endAt() {
return this.endTimestamp && new Date(this.endTimestamp);
},
frequency: data.recurrence_rule.frequency,
interval: data.recurrence_rule.interval,
byWeekday: data.recurrence_rule.by_weekday,
byNWeekday: data.recurrence_rule.by_n_weekday,
byMonth: data.recurrence_rule.by_month,
byMonthDay: data.recurrence_rule.by_month_day,
byYearDay: data.recurrence_rule.by_year_day,
count: data.recurrence_rule.count,
};
} else {
this.recurrenceRule ??= null;
}
}
/**

View File

@@ -153,8 +153,8 @@ class InteractionCollector extends Collector {
if (this.messageId && interaction.message?.id !== this.messageId) return null;
if (
this.messageInteractionId &&
interaction.message?.interaction?.id &&
interaction.message.interaction.id !== this.messageInteractionId
interaction.message?.interactionMetadata?.id &&
interaction.message.interactionMetadata.id !== this.messageInteractionId
) {
return null;
}
@@ -180,8 +180,8 @@ class InteractionCollector extends Collector {
if (this.messageId && interaction.message?.id !== this.messageId) return null;
if (
this.messageInteractionId &&
interaction.message?.interaction?.id &&
interaction.message.interaction.id !== this.messageInteractionId
interaction.message?.interactionMetadata?.id &&
interaction.message.interactionMetadata.id !== this.messageInteractionId
) {
return null;
}
@@ -224,7 +224,7 @@ class InteractionCollector extends Collector {
this.stop('messageDelete');
}
if (message.interaction?.id === this.messageInteractionId) {
if (message.interactionMetadata?.id === this.messageInteractionId) {
this.stop('messageDelete');
}
}

View File

@@ -40,7 +40,7 @@ class Invite extends Base {
*/
this.guild ??= null;
if (data.guild) {
this.guild = this.client.guilds.resolve(data.guild.id) ?? new InviteGuild(this.client, data.guild);
this.guild = this.client.guilds.cache.get(data.guild.id) ?? new InviteGuild(this.client, data.guild);
}
if ('code' in data) {

View File

@@ -9,6 +9,7 @@ const {
MessageType,
MessageFlags,
PermissionFlagsBits,
MessageReferenceType,
} = require('discord-api-types/v10');
const Attachment = require('./Attachment');
const Base = require('./Base');
@@ -22,7 +23,7 @@ const ReactionCollector = require('./ReactionCollector');
const { Sticker } = require('./Sticker');
const { DiscordjsError, ErrorCodes } = require('../errors');
const ReactionManager = require('../managers/ReactionManager');
const { createComponent } = require('../util/Components');
const { createComponent, findComponentByCustomId } = require('../util/Components');
const { NonSystemMessageTypes, MaxBulkDeletableMessageAge, UndeletableMessageTypes } = require('../util/Constants');
const MessageFlagsBitField = require('../util/MessageFlagsBitField');
const PermissionsBitField = require('../util/PermissionsBitField');
@@ -150,10 +151,10 @@ class Message extends Base {
if ('components' in data) {
/**
* An array of action rows in the message.
* An array of components in the message.
* <info>This property requires the {@link GatewayIntentBits.MessageContent} privileged intent
* in a guild for messages that do not mention the client.</info>
* @type {ActionRow[]}
* @type {Component[]}
*/
this.components = data.components.map(component => createComponent(component));
} else {
@@ -364,6 +365,7 @@ class Message extends Base {
* @property {Snowflake} channelId The channel id that was referenced
* @property {Snowflake|undefined} guildId The guild id that was referenced
* @property {Snowflake|undefined} messageId The message id that was referenced
* @property {MessageReferenceType} type The type of message reference
*/
if ('message_reference' in data) {
@@ -375,6 +377,7 @@ class Message extends Base {
channelId: data.message_reference.channel_id,
guildId: data.message_reference.guild_id,
messageId: data.message_reference.message_id,
type: data.message_reference.type,
};
} else {
this.reference ??= null;
@@ -448,6 +451,29 @@ class Message extends Base {
this.poll ??= null;
}
if (data.message_snapshots) {
/**
* The message snapshots associated with the message reference
* @type {Collection<Snowflake, Message>}
*/
this.messageSnapshots = data.message_snapshots.reduce((coll, snapshot) => {
const channel = this.client.channels.resolve(this.reference.channelId);
const snapshotData = {
...snapshot.message,
id: this.reference.messageId,
channel_id: this.reference.channelId,
guild_id: this.reference.guildId,
};
return coll.set(
this.reference.messageId,
channel ? channel.messages._add(snapshotData) : new this.constructor(this.client, snapshotData),
);
}, new Collection());
} else {
this.messageSnapshots ??= new Collection();
}
/**
* A call associated with a message
* @typedef {Object} MessageCall
@@ -545,7 +571,7 @@ class Message extends Base {
* @readonly
*/
get thread() {
return this.channel?.threads?.resolve(this.id) ?? null;
return this.channel?.threads?.cache.get(this.id) ?? null;
}
/**
@@ -565,7 +591,7 @@ class Message extends Base {
*/
get cleanContent() {
// eslint-disable-next-line eqeqeq
return this.content != null ? cleanContent(this.content, this.channel) : null;
return this.content != null && this.channel ? cleanContent(this.content, this.channel) : null;
}
/**
@@ -679,7 +705,11 @@ 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 &&
(!this.guild || this.channel?.viewable) &&
this.reference?.type !== MessageReferenceType.Forward,
);
// Regardless of permissions thread messages cannot be edited if
// the thread is archived or the thread is locked and the bot does not have permission to manage threads.
@@ -799,8 +829,8 @@ class Message extends Base {
* .then(msg => console.log(`Updated the content of a message to ${msg.content}`))
* .catch(console.error);
*/
edit(options) {
if (!this.channel) return Promise.reject(new DiscordjsError(ErrorCodes.ChannelNotCached));
async edit(options) {
if (!this.channel) throw new DiscordjsError(ErrorCodes.ChannelNotCached);
return this.channel.messages.edit(this, options);
}
@@ -815,8 +845,8 @@ class Message extends Base {
* .catch(console.error);
* }
*/
crosspost() {
if (!this.channel) return Promise.reject(new DiscordjsError(ErrorCodes.ChannelNotCached));
async crosspost() {
if (!this.channel) throw new DiscordjsError(ErrorCodes.ChannelNotCached);
return this.channel.messages.crosspost(this.id);
}
@@ -914,8 +944,8 @@ class Message extends Base {
* .then(() => console.log(`Replied to message "${message.content}"`))
* .catch(console.error);
*/
reply(options) {
if (!this.channel) return Promise.reject(new DiscordjsError(ErrorCodes.ChannelNotCached));
async reply(options) {
if (!this.channel) throw new DiscordjsError(ErrorCodes.ChannelNotCached);
let data;
if (options instanceof MessagePayload) {
@@ -931,6 +961,24 @@ class Message extends Base {
return this.channel.send(data);
}
/**
* Forwards this message
*
* @param {TextBasedChannelResolvable} channel The channel to forward this message to.
* @returns {Promise<Message>}
*/
forward(channel) {
const resolvedChannel = this.client.channels.resolve(channel);
if (!resolvedChannel) throw new DiscordjsError(ErrorCodes.InvalidType, 'channel', 'TextBasedChannelResolvable');
return resolvedChannel.send({
forward: {
message: this.id,
channel: this.channelId,
guild: this.guildId,
},
});
}
/**
* Options for starting a thread on a message.
* @typedef {Object} StartThreadOptions
@@ -947,12 +995,12 @@ class Message extends Base {
* @param {StartThreadOptions} [options] Options for starting a thread on this message
* @returns {Promise<ThreadChannel>}
*/
startThread(options = {}) {
if (!this.channel) return Promise.reject(new DiscordjsError(ErrorCodes.ChannelNotCached));
async startThread(options = {}) {
if (!this.channel) throw new DiscordjsError(ErrorCodes.ChannelNotCached);
if (![ChannelType.GuildText, ChannelType.GuildAnnouncement].includes(this.channel.type)) {
return Promise.reject(new DiscordjsError(ErrorCodes.MessageThreadParent));
throw new DiscordjsError(ErrorCodes.MessageThreadParent);
}
if (this.hasThread) return Promise.reject(new DiscordjsError(ErrorCodes.MessageExistingThread));
if (this.hasThread) throw new DiscordjsError(ErrorCodes.MessageExistingThread);
return this.channel.threads.create({ ...options, startMessage: this });
}
@@ -961,8 +1009,8 @@ class Message extends Base {
* @param {boolean} [force=true] Whether to skip the cache check and request the API
* @returns {Promise<Message>}
*/
fetch(force = true) {
if (!this.channel) return Promise.reject(new DiscordjsError(ErrorCodes.ChannelNotCached));
async fetch(force = true) {
if (!this.channel) throw new DiscordjsError(ErrorCodes.ChannelNotCached);
return this.channel.messages.fetch({ message: this.id, force });
}
@@ -970,9 +1018,9 @@ class Message extends Base {
* Fetches the webhook used to create this message.
* @returns {Promise<?Webhook>}
*/
fetchWebhook() {
if (!this.webhookId) return Promise.reject(new DiscordjsError(ErrorCodes.WebhookMessage));
if (this.webhookId === this.applicationId) return Promise.reject(new DiscordjsError(ErrorCodes.WebhookApplication));
async fetchWebhook() {
if (!this.webhookId) throw new DiscordjsError(ErrorCodes.WebhookMessage);
if (this.webhookId === this.applicationId) throw new DiscordjsError(ErrorCodes.WebhookApplication);
return this.client.fetchWebhook(this.webhookId);
}
@@ -1007,7 +1055,7 @@ class Message extends Base {
* @returns {?MessageActionRowComponent}
*/
resolveComponent(customId) {
return this.components.flatMap(row => row.components).find(component => component.customId === customId) ?? null;
return findComponentByCustomId(this.components, customId);
}
/**

View File

@@ -4,6 +4,7 @@ const { lazy } = require('@discordjs/util');
const BaseInteraction = require('./BaseInteraction');
const InteractionWebhook = require('./InteractionWebhook');
const InteractionResponses = require('./interfaces/InteractionResponses');
const { findComponentByCustomId } = require('../util/Components');
const getMessage = lazy(() => require('./Message').Message);
@@ -79,13 +80,11 @@ class MessageComponentInteraction extends BaseInteraction {
/**
* The component which was interacted with
* @type {MessageActionRowComponent|APIMessageActionRowComponent}
* @type {MessageActionRowComponent|APIComponentInMessageActionRow}
* @readonly
*/
get component() {
return this.message.components
.flatMap(row => row.components)
.find(component => (component.customId ?? component.custom_id) === this.customId);
return findComponentByCustomId(this.message.components, this.customId);
}
// These are here only for documentation purposes - they are implemented by InteractionResponses
@@ -98,6 +97,7 @@ class MessageComponentInteraction extends BaseInteraction {
followUp() {}
deferUpdate() {}
update() {}
launchActivity() {}
showModal() {}
sendPremiumRequired() {}
awaitModalSubmit() {}

View File

@@ -3,8 +3,7 @@
const { Buffer } = require('node:buffer');
const { lazy, isJSONEncodable } = require('@discordjs/util');
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { MessageFlags } = require('discord-api-types/v10');
const ActionRowBuilder = require('./ActionRowBuilder');
const { MessageFlags, MessageReferenceType } = require('discord-api-types/v10');
const { DiscordjsError, DiscordjsRangeError, ErrorCodes } = require('../errors');
const { resolveFile } = require('../util/DataResolver');
const MessageFlagsBitField = require('../util/MessageFlagsBitField');
@@ -92,6 +91,7 @@ class MessagePayload {
* Whether or not the target is an {@link BaseInteraction} or an {@link InteractionWebhook}
* @type {boolean}
* @readonly
* @deprecated This will no longer serve a purpose in the next major version.
*/
get isInteraction() {
const BaseInteraction = getBaseInteraction();
@@ -148,7 +148,7 @@ class MessagePayload {
}
const components = this.options.components?.map(component =>
(isJSONEncodable(component) ? component : new ActionRowBuilder(component)).toJSON(),
isJSONEncodable(component) ? component.toJSON() : this.target.client.options.jsonTransformer(component),
);
let username;
@@ -164,15 +164,10 @@ class MessagePayload {
let flags;
if (
this.options.flags !== undefined ||
(this.isMessage && this.options.reply === undefined) ||
this.isMessageManager
// eslint-disable-next-line eqeqeq
this.options.flags != null
) {
flags =
// eslint-disable-next-line eqeqeq
this.options.flags != null
? new MessageFlagsBitField(this.options.flags).bitfield
: this.target.flags?.bitfield;
flags = new MessageFlagsBitField(this.options.flags).bitfield;
}
if (isInteraction && this.options.ephemeral) {
@@ -201,6 +196,22 @@ class MessagePayload {
}
}
if (typeof this.options.forward === 'object') {
const reference = this.options.forward.message;
const channel_id = reference.channelId ?? this.target.client.channels.resolveId(this.options.forward.channel);
const guild_id = reference.guildId ?? this.target.client.guilds.resolveId(this.options.forward.guild);
const message_id = this.target.messages.resolveId(reference);
if (message_id) {
if (!channel_id) throw new DiscordjsError(ErrorCodes.InvalidType, 'channelId', 'TextBasedChannelResolvable');
message_reference = {
type: MessageReferenceType.Forward,
message_id,
channel_id,
guild_id: guild_id ?? undefined,
};
}
}
const attachments = this.options.files?.map((file, index) => ({
id: index.toString(),
description: file.description,
@@ -237,7 +248,10 @@ class MessagePayload {
components,
username,
avatar_url: avatarURL,
allowed_mentions: content === undefined && message_reference === undefined ? undefined : allowedMentions,
allowed_mentions:
this.isMessage && message_reference === undefined && this.target.author.id !== this.target.client.user.id
? undefined
: allowedMentions,
flags,
message_reference,
attachments: this.options.attachments,

View File

@@ -1,6 +1,7 @@
'use strict';
const { Routes } = require('discord-api-types/v10');
const ApplicationEmoji = require('./ApplicationEmoji');
const GuildEmoji = require('./GuildEmoji');
const ReactionEmoji = require('./ReactionEmoji');
const ReactionUserManager = require('../managers/ReactionUserManager');
@@ -35,7 +36,7 @@ class MessageReaction {
* Whether the client has super-reacted using this emoji
* @type {boolean}
*/
this.meBurst = data.me_burst;
this.meBurst = Boolean(data.me_burst);
/**
* A manager of the users that have given this reaction
@@ -51,7 +52,7 @@ class MessageReaction {
}
_patch(data) {
if ('burst_colors' in data) {
if (data.burst_colors) {
/**
* Hexadecimal colors used for this super reaction
* @type {?string[]}
@@ -108,16 +109,24 @@ class MessageReaction {
}
/**
* The emoji of this reaction. Either a {@link GuildEmoji} object for known custom emojis, or a {@link ReactionEmoji}
* object which has fewer properties. Whatever the prototype of the emoji, it will still have
* The emoji of this reaction. Either a {@link GuildEmoji} object for known custom emojis,
* {@link ApplicationEmoji} for application emojis, or a {@link ReactionEmoji} object
* which has fewer properties. Whatever the prototype of the emoji, it will still have
* `name`, `id`, `identifier` and `toString()`
* @type {GuildEmoji|ReactionEmoji}
* @type {GuildEmoji|ReactionEmoji|ApplicationEmoji}
* @readonly
*/
get emoji() {
if (this._emoji instanceof GuildEmoji) return this._emoji;
if (this._emoji instanceof ApplicationEmoji) return this._emoji;
// Check to see if the emoji has become known to the client
if (this._emoji.id) {
const applicationEmojis = this.message.client.application.emojis.cache;
if (applicationEmojis.has(this._emoji.id)) {
const emoji = applicationEmojis.get(this._emoji.id);
this._emoji = emoji;
return emoji;
}
const emojis = this.message.client.emojis.cache;
if (emojis.has(this._emoji.id)) {
const emoji = emojis.get(this._emoji.id);

View File

@@ -119,6 +119,7 @@ class ModalSubmitInteraction extends BaseInteraction {
deferUpdate() {}
update() {}
sendPremiumRequired() {}
launchActivity() {}
}
InteractionResponses.applyToClass(ModalSubmitInteraction, 'showModal');

View File

@@ -1,12 +1,14 @@
'use strict';
const { BaseChannel } = require('./BaseChannel');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const { DiscordjsError, ErrorCodes } = require('../errors');
const PartialGroupDMMessageManager = require('../managers/PartialGroupDMMessageManager');
/**
* Represents a Partial Group DM Channel on Discord.
* @extends {BaseChannel}
* @implements {TextBasedChannel}
*/
class PartialGroupDMChannel extends BaseChannel {
constructor(client, data) {
@@ -25,7 +27,7 @@ class PartialGroupDMChannel extends BaseChannel {
* The hash of the channel icon
* @type {?string}
*/
this.icon = data.icon;
this.icon = data.icon ?? null;
/**
* Recipient data received in a {@link PartialGroupDMChannel}.
@@ -37,13 +39,43 @@ class PartialGroupDMChannel extends BaseChannel {
* The recipients of this Group DM Channel.
* @type {PartialRecipient[]}
*/
this.recipients = data.recipients;
this.recipients = data.recipients ?? [];
/**
* A manager of the messages belonging to this channel
* @type {PartialGroupDMMessageManager}
*/
this.messages = new PartialGroupDMMessageManager(this);
if ('owner_id' in data) {
/**
* The user id of the owner of this Group DM Channel
* @type {?Snowflake}
*/
this.ownerId = data.owner_id;
} else {
this.ownerId ??= null;
}
if ('last_message_id' in data) {
/**
* The channel's last message id, if one was sent
* @type {?Snowflake}
*/
this.lastMessageId = data.last_message_id;
} else {
this.lastMessageId ??= null;
}
if ('last_pin_timestamp' in data) {
/**
* The timestamp when the last pinned message was pinned, if there was one
* @type {?number}
*/
this.lastPinTimestamp = data.last_pin_timestamp ? Date.parse(data.last_pin_timestamp) : null;
} else {
this.lastPinTimestamp ??= null;
}
}
/**
@@ -55,13 +87,45 @@ class PartialGroupDMChannel extends BaseChannel {
return this.icon && this.client.rest.cdn.channelIcon(this.id, this.icon, options);
}
delete() {
return Promise.reject(new DiscordjsError(ErrorCodes.DeleteGroupDMChannel));
/**
* Fetches the owner of this Group DM Channel.
* @param {BaseFetchOptions} [options] The options for fetching the user
* @returns {Promise<User>}
*/
async fetchOwner(options) {
if (!this.ownerId) {
throw new DiscordjsError(ErrorCodes.FetchOwnerId, 'group DM');
}
return this.client.users.fetch(this.ownerId, options);
}
fetch() {
return Promise.reject(new DiscordjsError(ErrorCodes.FetchGroupDMChannel));
async delete() {
throw new DiscordjsError(ErrorCodes.DeleteGroupDMChannel);
}
async fetch() {
throw new DiscordjsError(ErrorCodes.FetchGroupDMChannel);
}
// These are here only for documentation purposes - they are implemented by TextBasedChannel
/* eslint-disable no-empty-function */
get lastMessage() {}
get lastPinAt() {}
createMessageComponentCollector() {}
awaitMessageComponent() {}
}
TextBasedChannel.applyToClass(PartialGroupDMChannel, true, [
'bulkDelete',
'send',
'sendTyping',
'createMessageCollector',
'awaitMessages',
'fetchWebhooks',
'createWebhook',
'setRateLimitPerUser',
'setNSFW',
]);
module.exports = PartialGroupDMChannel;

View File

@@ -181,7 +181,10 @@ class PermissionOverwrites extends Base {
}
const userOrRole = guild.roles.resolve(overwrite.id) ?? guild.client.users.resolve(overwrite.id);
if (!userOrRole) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'parameter', 'User nor a Role');
if (!userOrRole) {
throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'parameter', 'cached User or Role');
}
const type = userOrRole instanceof Role ? OverwriteType.Role : OverwriteType.Member;
return {

View File

@@ -97,9 +97,9 @@ class Poll extends Base {
* Ends this poll.
* @returns {Promise<Message>}
*/
end() {
async end() {
if (Date.now() > this.expiresTimestamp) {
return Promise.reject(new DiscordjsError(ErrorCodes.PollAlreadyExpired));
throw new DiscordjsError(ErrorCodes.PollAlreadyExpired);
}
return this.message.channel.messages.endPoll(this.message.id);

View File

@@ -61,7 +61,7 @@ class PollAnswer extends Base {
*/
get emoji() {
if (!this._emoji || (!this._emoji.id && !this._emoji.name)) return null;
return this.client.emojis.resolve(this._emoji.id) ?? new Emoji(this.client, this._emoji);
return this.client.emojis.cache.get(this._emoji.id) ?? new Emoji(this.client, this._emoji);
}
/**

View File

@@ -182,8 +182,8 @@ class Sticker extends Base {
* Fetches the pack that contains this sticker.
* @returns {Promise<?StickerPack>} The sticker pack or `null` if this sticker does not belong to one.
*/
fetchPack() {
if (!this.packId) return Promise.resolve(null);
async fetchPack() {
if (!this.packId) return null;
return this.client.fetchStickerPacks({ packId: this.packId });
}
@@ -225,7 +225,7 @@ class Sticker extends Base {
* @returns {Promise<Sticker>}
* @param {string} [reason] Reason for deleting this sticker
* @example
* // Delete a message
* // Delete a sticker
* sticker.delete()
* .then(sticker => console.log(`Deleted sticker ${sticker.name}`))
* .catch(console.error);

View File

@@ -6,7 +6,7 @@ const { RESTJSONErrorCodes, ChannelFlags, ChannelType, PermissionFlagsBits, Rout
const { BaseChannel } = require('./BaseChannel');
const getThreadOnlyChannel = lazy(() => require('./ThreadOnlyChannel'));
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const { DiscordjsError, DiscordjsRangeError, ErrorCodes } = require('../errors');
const { DiscordjsRangeError, ErrorCodes } = require('../errors');
const GuildMessageManager = require('../managers/GuildMessageManager');
const ThreadMemberManager = require('../managers/ThreadMemberManager');
const ChannelFlagsBitField = require('../util/ChannelFlagsBitField');
@@ -32,6 +32,12 @@ class ThreadChannel extends BaseChannel {
*/
this.guildId = guild?.id ?? data.guild_id;
/**
* The id of the member who created this thread
* @type {Snowflake}
*/
this.ownerId = data.owner_id;
/**
* A manager of the messages sent to this thread
* @type {GuildMessageManager}
@@ -122,16 +128,6 @@ class ThreadChannel extends BaseChannel {
this._createdTimestamp ??= this.type === ChannelType.PrivateThread ? super.createdTimestamp : null;
if ('owner_id' in data) {
/**
* The id of the member who created this thread
* @type {?Snowflake}
*/
this.ownerId = data.owner_id;
} else {
this.ownerId ??= null;
}
if ('last_message_id' in data) {
/**
* The last message id sent in this thread, if one was sent
@@ -288,17 +284,19 @@ class ThreadChannel extends BaseChannel {
return this.parent?.permissionsFor(memberOrRole, checkAdmin) ?? null;
}
/**
* Options used to fetch a thread owner.
* @typedef {BaseFetchOptions} FetchThreadOwnerOptions
* @property {boolean} [withMember] Whether to also return the guild member associated with this thread member
*/
/**
* Fetches the owner of this thread. If the thread member object isn't needed,
* use {@link ThreadChannel#ownerId} instead.
* @param {BaseFetchOptions} [options] The options for fetching the member
* @param {FetchThreadOwnerOptions} [options] Options for fetching the owner
* @returns {Promise<?ThreadMember>}
*/
async fetchOwner(options) {
if (!this.ownerId) {
throw new DiscordjsError(ErrorCodes.FetchOwnerId, 'thread');
}
// TODO: Remove that catch in the next major version
const member = await this.members._fetchSingle({ ...options, member: this.ownerId }).catch(error => {
if (error instanceof DiscordAPIError && error.code === RESTJSONErrorCodes.UnknownMember) {
@@ -319,7 +317,6 @@ class ThreadChannel extends BaseChannel {
* @param {BaseFetchOptions} [options] Additional options for this fetch
* @returns {Promise<?Message<true>>}
*/
// eslint-disable-next-line require-await
async fetchStarterMessage(options) {
const channel = this.parent instanceof getThreadOnlyChannel() ? this : this.parent;
return channel?.messages.fetch({ message: this.id, ...options }) ?? null;
@@ -409,9 +406,9 @@ class ThreadChannel extends BaseChannel {
* @param {string} [reason] Reason for changing invite
* @returns {Promise<ThreadChannel>}
*/
setInvitable(invitable = true, reason) {
async setInvitable(invitable = true, reason) {
if (this.type !== ChannelType.PrivateThread) {
return Promise.reject(new DiscordjsRangeError(ErrorCodes.ThreadInvitableType, this.type));
throw new DiscordjsRangeError(ErrorCodes.ThreadInvitableType, this.type);
}
return this.edit({ invitable, reason });
}

View File

@@ -2,6 +2,7 @@
const Base = require('./Base');
const ThreadMemberFlagsBitField = require('../util/ThreadMemberFlagsBitField');
const { emitDeprecationWarningForRemoveThreadMember } = require('../util/Util');
/**
* Represents a Member for a Thread.
@@ -69,7 +70,7 @@ class ThreadMember extends Base {
* @readonly
*/
get guildMember() {
return this.member ?? this.thread.guild.members.resolve(this.id);
return this.member ?? this.thread.guild.members.cache.get(this.id) ?? null;
}
/**
@@ -87,7 +88,7 @@ class ThreadMember extends Base {
* @readonly
*/
get user() {
return this.client.users.resolve(this.id);
return this.client.users.cache.get(this.id) ?? null;
}
/**
@@ -102,9 +103,14 @@ class ThreadMember extends Base {
/**
* Removes this member from the thread.
* @param {string} [reason] Reason for removing the member
* <warn>This parameter is **deprecated**. Reasons cannot be used.</warn>
* @returns {Promise<ThreadMember>}
*/
async remove(reason) {
if (reason !== undefined) {
emitDeprecationWarningForRemoveThreadMember(this.constructor.name);
}
await this.thread.members.remove(this.id, reason);
return this;
}

View File

@@ -36,7 +36,7 @@ const { transformAPIGuildForumTag, transformAPIGuildDefaultReaction } = require(
*/
/**
* Represents symbols utilised by thread-only channels.
* Represents symbols utilized by thread-only channels.
* @extends {GuildChannel}
* @implements {TextBasedChannel}
* @abstract

View File

@@ -6,6 +6,7 @@ const { DiscordSnowflake } = require('@sapphire/snowflake');
const Base = require('./Base');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const UserFlagsBitField = require('../util/UserFlagsBitField');
const { emitDeprecationWarningForUserFetchFlags } = require('../util/Util');
/**
* Represents a user on Discord.
@@ -346,8 +347,11 @@ class User extends Base {
* Fetches this user's flags.
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<UserFlagsBitField>}
* @deprecated <warn>This method is deprecated and will be removed in the next major version.
* Flags may still be retrieved via {@link User#fetch}.</warn>
*/
fetchFlags(force = false) {
emitDeprecationWarningForUserFetchFlags(this.constructor.name);
return this.client.users.fetchFlags(this.id, { force });
}

View File

@@ -1,6 +1,6 @@
'use strict';
const { PermissionFlagsBits } = require('discord-api-types/v10');
const { PermissionFlagsBits, Routes } = require('discord-api-types/v10');
const BaseGuildVoiceChannel = require('./BaseGuildVoiceChannel');
/**
@@ -35,6 +35,26 @@ class VoiceChannel extends BaseGuildVoiceChannel {
permissions.has(PermissionFlagsBits.Speak, false)
);
}
/**
* @typedef {Object} SendSoundboardSoundOptions
* @property {string} soundId The id of the soundboard sound to send
* @property {string} [guildId] The id of the guild the soundboard sound is a part of
*/
/**
* Send a soundboard sound to a voice channel the user is connected to.
* @param {SoundboardSound|SendSoundboardSoundOptions} sound The sound to send
* @returns {Promise<void>}
*/
async sendSoundboardSound(sound) {
await this.client.rest.post(Routes.sendSoundboardSound(this.id), {
body: {
sound_id: sound.soundId,
source_guild_id: sound.guildId ?? undefined,
},
});
}
}
/**

View File

@@ -108,7 +108,7 @@ class Webhook {
* The source guild of the webhook
* @type {?(Guild|APIGuild)}
*/
this.sourceGuild = this.client.guilds?.resolve(data.source_guild.id) ?? data.source_guild;
this.sourceGuild = this.client.guilds?.cache.get(data.source_guild.id) ?? data.source_guild;
} else {
this.sourceGuild ??= null;
}
@@ -118,7 +118,7 @@ class Webhook {
* The source channel of the webhook
* @type {?(NewsChannel|APIChannel)}
*/
this.sourceChannel = this.client.channels?.resolve(data.source_channel?.id) ?? data.source_channel;
this.sourceChannel = this.client.channels?.cache.get(data.source_channel?.id) ?? data.source_channel;
} else {
this.sourceChannel ??= null;
}
@@ -126,7 +126,7 @@ class Webhook {
/**
* Options that can be passed into send.
* @typedef {BaseMessageOptions} WebhookMessageCreateOptions
* @typedef {BaseMessageOptionsWithPoll} WebhookMessageCreateOptions
* @property {boolean} [tts=false] Whether the message should be spoken aloud
* @property {MessageFlags} [flags] Which flags to set for the message.
* <info>Only the {@link MessageFlags.SuppressEmbeds} flag can be set.</info>
@@ -137,12 +137,13 @@ class Webhook {
* @property {string} [threadName] Name of the thread to create (only available if the webhook is in a forum channel)
* @property {Snowflake[]} [appliedTags]
* The tags to apply to the created thread (only available if the webhook is in a forum channel)
* @property {boolean} [withComponents] Whether to allow sending non-interactive components in the message.
* <info>For application-owned webhooks, this property is ignored</info>
*/
/**
* Options that can be passed into editMessage.
* @typedef {BaseMessageOptions} WebhookMessageEditOptions
* @property {Attachment[]} [attachments] Attachments to send with the message
* @typedef {MessageEditOptions} WebhookMessageEditOptions
* @property {Snowflake} [threadId] The id of the thread this message belongs to
* <info>For interaction webhooks, this property is ignored</info>
*/
@@ -215,12 +216,14 @@ class Webhook {
messagePayload = MessagePayload.create(this, options).resolveBody();
}
const { body, files } = await messagePayload.resolveFiles();
const query = makeURLSearchParams({
wait: true,
thread_id: messagePayload.options.threadId,
with_components: messagePayload.options.withComponents,
});
const { body, files } = await messagePayload.resolveFiles();
const d = await this.client.rest.post(Routes.webhook(this.id, this.token), {
body,
files,
@@ -299,6 +302,8 @@ class Webhook {
* @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>
* @property {boolean} [withComponents] Whether to allow sending non-interactive components in the message.
* <info>For application-owned webhooks, this property is ignored</info>
*/
/**
@@ -338,14 +343,17 @@ class Webhook {
const { body, files } = await messagePayload.resolveBody().resolveFiles();
const query = makeURLSearchParams({
thread_id: messagePayload.options.threadId,
with_components: messagePayload.options.withComponents,
});
const d = await this.client.rest.patch(
Routes.webhookMessage(this.id, this.token, typeof message === 'string' ? message : message.id),
{
body,
files,
query: messagePayload.options.threadId
? makeURLSearchParams({ thread_id: messagePayload.options.threadId })
: undefined,
query,
auth: false,
},
);

View File

@@ -53,7 +53,7 @@ class WelcomeChannel extends Base {
* @type {GuildEmoji|Emoji}
*/
get emoji() {
return this.client.emojis.resolve(this._emoji.id) ?? new Emoji(this.client, this._emoji);
return this.client.emojis.cache.get(this._emoji.id) ?? new Emoji(this.client, this._emoji);
}
}

View File

@@ -50,6 +50,56 @@ class Application extends Base {
} else {
this.icon ??= null;
}
if ('terms_of_service_url' in data) {
/**
* The URL of the application's terms of service
* @type {?string}
*/
this.termsOfServiceURL = data.terms_of_service_url;
} else {
this.termsOfServiceURL ??= null;
}
if ('privacy_policy_url' in data) {
/**
* The URL of the application's privacy policy
* @type {?string}
*/
this.privacyPolicyURL = data.privacy_policy_url;
} else {
this.privacyPolicyURL ??= null;
}
if ('rpc_origins' in data) {
/**
* The application's RPC origins, if enabled
* @type {string[]}
*/
this.rpcOrigins = data.rpc_origins;
} else {
this.rpcOrigins ??= [];
}
if ('cover_image' in data) {
/**
* The hash of the application's cover image
* @type {?string}
*/
this.cover = data.cover_image;
} else {
this.cover ??= null;
}
if ('verify_key' in data) {
/**
* The hex-encoded key for verification in interactions and the GameSDK's GetTicket
* @type {?string}
*/
this.verifyKey = data.verify_key;
} else {
this.verifyKey ??= null;
}
}
/**

View File

@@ -1,14 +1,20 @@
'use strict';
const process = require('node:process');
const { deprecate } = require('node:util');
const { makeURLSearchParams } = require('@discordjs/rest');
const { isJSONEncodable } = require('@discordjs/util');
const { InteractionResponseType, MessageFlags, Routes, InteractionType } = require('discord-api-types/v10');
const { DiscordjsError, ErrorCodes } = require('../../errors');
const MessageFlagsBitField = require('../../util/MessageFlagsBitField');
const InteractionCallbackResponse = require('../InteractionCallbackResponse');
const InteractionCollector = require('../InteractionCollector');
const InteractionResponse = require('../InteractionResponse');
const MessagePayload = require('../MessagePayload');
let deprecationEmittedForEphemeralOption = false;
let deprecationEmittedForFetchReplyOption = false;
/**
* @typedef {Object} ModalComponentData
* @property {string} title The title of the modal
@@ -24,23 +30,33 @@ class InteractionResponses {
/**
* Options for deferring the reply to an {@link BaseInteraction}.
* @typedef {Object} InteractionDeferReplyOptions
* @property {boolean} [ephemeral] Whether the reply should be ephemeral
* @property {boolean} [ephemeral] Whether the reply should be ephemeral.
* <warn>This option is deprecated. Use `flags` instead.</warn>
* @property {MessageFlagsResolvable} [flags] Flags for the reply.
* <info>Only `MessageFlags.Ephemeral` can be set.</info>
* @property {boolean} [withResponse] Whether to return an {@link InteractionCallbackResponse} as the response
* @property {boolean} [fetchReply] Whether to fetch the reply
* <warn>This option is deprecated. Use `withResponse` or fetch the response instead.</warn>
*/
/**
* Options for deferring and updating the reply to a {@link MessageComponentInteraction}.
* @typedef {Object} InteractionDeferUpdateOptions
* @property {boolean} [withResponse] Whether to return an {@link InteractionCallbackResponse} as the response
* @property {boolean} [fetchReply] Whether to fetch the reply
* <warn>This option is deprecated. Use `withResponse` or fetch the response instead.</warn>
*/
/**
* Options for a reply to a {@link BaseInteraction}.
* @typedef {BaseMessageOptions} InteractionReplyOptions
* @typedef {BaseMessageOptionsWithPoll} InteractionReplyOptions
* @property {boolean} [ephemeral] Whether the reply should be ephemeral.
* <warn>This option is deprecated. Use `flags` instead.</warn>
* @property {boolean} [tts=false] Whether the message should be spoken aloud
* @property {boolean} [ephemeral] Whether the reply should be ephemeral
* @property {boolean} [withResponse] Whether to return an {@link InteractionCallbackResponse} as the response
* @property {boolean} [fetchReply] Whether to fetch the reply
* @property {MessageFlags} [flags] Which flags to set for the message.
* <warn>This option is deprecated. Use `withResponse` or fetch the response instead.</warn>
* @property {MessageFlagsResolvable} [flags] Which flags to set for the message.
* <info>Only `MessageFlags.Ephemeral`, `MessageFlags.SuppressEmbeds`, and `MessageFlags.SuppressNotifications`
* can be set.</info>
*/
@@ -48,13 +64,27 @@ class InteractionResponses {
/**
* Options for updating the message received from a {@link MessageComponentInteraction}.
* @typedef {MessageEditOptions} InteractionUpdateOptions
* @property {boolean} [withResponse] Whether to return an {@link InteractionCallbackResponse} as the response
* @property {boolean} [fetchReply] Whether to fetch the reply
* <warn>This option is deprecated. Use `withResponse` or fetch the response instead.</warn>
*/
/**
* Options for launching activity in response to a {@link BaseInteraction}
* @typedef {Object} LaunchActivityOptions
* @property {boolean} [withResponse] Whether to return an {@link InteractionCallbackResponse} as the response
*/
/**
* Options for showing a modal in response to a {@link BaseInteraction}
* @typedef {Object} ShowModalOptions
* @property {boolean} [withResponse] Whether to return an {@link InteractionCallbackResponse} as the response
*/
/**
* Defers the reply to this interaction.
* @param {InteractionDeferReplyOptions} [options] Options for deferring the reply to this interaction
* @returns {Promise<Message|InteractionResponse>}
* @returns {Promise<InteractionCallbackResponse|Message|InteractionResponse>}
* @example
* // Defer the reply to this interaction
* interaction.deferReply()
@@ -62,67 +92,129 @@ class InteractionResponses {
* .catch(console.error)
* @example
* // Defer to send an ephemeral reply later
* interaction.deferReply({ ephemeral: true })
* interaction.deferReply({ flags: MessageFlags.Ephemeral })
* .then(console.log)
* .catch(console.error);
*/
async deferReply(options = {}) {
if (this.deferred || this.replied) throw new DiscordjsError(ErrorCodes.InteractionAlreadyReplied);
this.ephemeral = options.ephemeral ?? false;
await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
if ('ephemeral' in options) {
if (!deprecationEmittedForEphemeralOption) {
process.emitWarning(
`Supplying "ephemeral" for interaction response options is deprecated. Utilize flags instead.`,
);
deprecationEmittedForEphemeralOption = true;
}
}
if ('fetchReply' in options) {
if (!deprecationEmittedForFetchReplyOption) {
process.emitWarning(
// eslint-disable-next-line max-len
`Supplying "fetchReply" for interaction response options is deprecated. Utilize "withResponse" instead or fetch the response after using the method.`,
);
deprecationEmittedForFetchReplyOption = true;
}
}
const flags = new MessageFlagsBitField(options.flags);
if (options.ephemeral) {
flags.add(MessageFlags.Ephemeral);
}
const response = await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
body: {
type: InteractionResponseType.DeferredChannelMessageWithSource,
data: {
flags: options.ephemeral ? MessageFlags.Ephemeral : undefined,
flags: flags.bitfield,
},
},
auth: false,
query: makeURLSearchParams({ with_response: options.withResponse ?? false }),
});
this.deferred = true;
return options.fetchReply ? this.fetchReply() : new InteractionResponse(this);
this.deferred = true;
this.ephemeral = flags.has(MessageFlags.Ephemeral);
return options.withResponse
? new InteractionCallbackResponse(this.client, response)
: options.fetchReply
? this.fetchReply()
: new InteractionResponse(this);
}
/**
* Creates a reply to this interaction.
* <info>Use the `fetchReply` option to get the bot's reply message.</info>
* <info>Use the `withResponse` option to get the interaction callback response.</info>
* @param {string|MessagePayload|InteractionReplyOptions} options The options for the reply
* @returns {Promise<Message|InteractionResponse>}
* @returns {Promise<InteractionCallbackResponse|Message|InteractionResponse>}
* @example
* // Reply to the interaction and fetch the response
* interaction.reply({ content: 'Pong!', fetchReply: true })
* .then((message) => console.log(`Reply sent with content ${message.content}`))
* interaction.reply({ content: 'Pong!', withResponse: true })
* .then((response) => console.log(`Reply sent with content ${response.resource.message.content}`))
* .catch(console.error);
* @example
* // Create an ephemeral reply with an embed
* const embed = new EmbedBuilder().setDescription('Pong!');
*
* interaction.reply({ embeds: [embed], ephemeral: true })
* interaction.reply({ embeds: [embed], flags: MessageFlags.Ephemeral })
* .then(() => console.log('Reply sent.'))
* .catch(console.error);
*/
async reply(options) {
if (this.deferred || this.replied) throw new DiscordjsError(ErrorCodes.InteractionAlreadyReplied);
if (typeof options !== 'string') {
if ('ephemeral' in options) {
if (!deprecationEmittedForEphemeralOption) {
process.emitWarning(
`Supplying "ephemeral" for interaction response options is deprecated. Utilize flags instead.`,
);
deprecationEmittedForEphemeralOption = true;
}
}
if ('fetchReply' in options) {
if (!deprecationEmittedForFetchReplyOption) {
process.emitWarning(
// eslint-disable-next-line max-len
`Supplying "fetchReply" for interaction response options is deprecated. Utilize "withResponse" instead or fetch the response after using the method.`,
);
deprecationEmittedForFetchReplyOption = true;
}
}
}
let messagePayload;
if (options instanceof MessagePayload) messagePayload = options;
else messagePayload = MessagePayload.create(this, options);
const { body: data, files } = await messagePayload.resolveBody().resolveFiles();
this.ephemeral = new MessageFlagsBitField(data.flags).has(MessageFlags.Ephemeral);
await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
const response = await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
body: {
type: InteractionResponseType.ChannelMessageWithSource,
data,
},
files,
auth: false,
query: makeURLSearchParams({ with_response: options.withResponse ?? false }),
});
this.ephemeral = Boolean(data.flags & MessageFlags.Ephemeral);
this.replied = true;
return options.fetchReply ? this.fetchReply() : new InteractionResponse(this);
return options.withResponse
? new InteractionCallbackResponse(this.client, response)
: options.fetchReply
? this.fetchReply()
: new InteractionResponse(this);
}
/**
@@ -176,6 +268,8 @@ class InteractionResponses {
* .catch(console.error);
*/
async deleteReply(message = '@original') {
if (!this.deferred && !this.replied) throw new DiscordjsError(ErrorCodes.InteractionNotReplied);
await this.webhook.deleteMessage(message);
}
@@ -184,15 +278,17 @@ class InteractionResponses {
* @param {string|MessagePayload|InteractionReplyOptions} options The options for the reply
* @returns {Promise<Message>}
*/
followUp(options) {
if (!this.deferred && !this.replied) return Promise.reject(new DiscordjsError(ErrorCodes.InteractionNotReplied));
return this.webhook.send(options);
async followUp(options) {
if (!this.deferred && !this.replied) throw new DiscordjsError(ErrorCodes.InteractionNotReplied);
const msg = await this.webhook.send(options);
this.replied = true;
return msg;
}
/**
* Defers an update to the message to which the component was attached.
* @param {InteractionDeferUpdateOptions} [options] Options for deferring the update to this interaction
* @returns {Promise<Message|InteractionResponse>}
* @returns {Promise<InteractionCallbackResponse|Message|InteractionResponse>}
* @example
* // Defer updating and reset the component's loading state
* interaction.deferUpdate()
@@ -201,21 +297,38 @@ class InteractionResponses {
*/
async deferUpdate(options = {}) {
if (this.deferred || this.replied) throw new DiscordjsError(ErrorCodes.InteractionAlreadyReplied);
await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
if ('fetchReply' in options) {
if (!deprecationEmittedForFetchReplyOption) {
process.emitWarning(
// eslint-disable-next-line max-len
`Supplying "fetchReply" for interaction response options is deprecated. Utilize "withResponse" instead or fetch the response after using the method.`,
);
deprecationEmittedForFetchReplyOption = true;
}
}
const response = await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
body: {
type: InteractionResponseType.DeferredMessageUpdate,
},
auth: false,
query: makeURLSearchParams({ with_response: options.withResponse ?? false }),
});
this.deferred = true;
return options.fetchReply ? this.fetchReply() : new InteractionResponse(this, this.message?.interaction?.id);
return options.withResponse
? new InteractionCallbackResponse(this.client, response)
: options.fetchReply
? this.fetchReply()
: new InteractionResponse(this, this.message?.interactionMetadata?.id);
}
/**
* Updates the original message of the component on which the interaction was received on.
* @param {string|MessagePayload|InteractionUpdateOptions} options The options for the updated message
* @returns {Promise<Message|void>}
* @returns {Promise<InteractionCallbackResponse|Message|void>}
* @example
* // Remove the components from the message
* interaction.update({
@@ -228,40 +341,79 @@ class InteractionResponses {
async update(options) {
if (this.deferred || this.replied) throw new DiscordjsError(ErrorCodes.InteractionAlreadyReplied);
if (typeof options !== 'string' && 'fetchReply' in options) {
if (!deprecationEmittedForFetchReplyOption) {
process.emitWarning(
// eslint-disable-next-line max-len
`Supplying "fetchReply" for interaction response options is deprecated. Utilize "withResponse" instead or fetch the response after using the method.`,
);
deprecationEmittedForFetchReplyOption = true;
}
}
let messagePayload;
if (options instanceof MessagePayload) messagePayload = options;
else messagePayload = MessagePayload.create(this, options);
const { body: data, files } = await messagePayload.resolveBody().resolveFiles();
await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
const response = await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
body: {
type: InteractionResponseType.UpdateMessage,
data,
},
files,
auth: false,
query: makeURLSearchParams({ with_response: options.withResponse ?? false }),
});
this.replied = true;
return options.fetchReply ? this.fetchReply() : new InteractionResponse(this, this.message.interaction?.id);
return options.withResponse
? new InteractionCallbackResponse(this.client, response)
: options.fetchReply
? this.fetchReply()
: new InteractionResponse(this, this.message.interactionMetadata?.id);
}
/**
* Launches this application's activity, if enabled
* @param {LaunchActivityOptions} [options={}] Options for launching the activity
* @returns {Promise<InteractionCallbackResponse|undefined>}
*/
async launchActivity({ withResponse } = {}) {
if (this.deferred || this.replied) throw new DiscordjsError(ErrorCodes.InteractionAlreadyReplied);
const response = await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
query: makeURLSearchParams({ with_response: withResponse ?? false }),
body: {
type: InteractionResponseType.LaunchActivity,
},
auth: false,
});
this.replied = true;
return withResponse ? new InteractionCallbackResponse(this.client, response) : undefined;
}
/**
* Shows a modal component
* @param {ModalBuilder|ModalComponentData|APIModalInteractionResponseCallbackData} modal The modal to show
* @returns {Promise<void>}
* @param {ShowModalOptions} [options={}] The options for sending this interaction response
* @returns {Promise<InteractionCallbackResponse|undefined>}
*/
async showModal(modal) {
async showModal(modal, options = {}) {
if (this.deferred || this.replied) throw new DiscordjsError(ErrorCodes.InteractionAlreadyReplied);
await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
const response = await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
body: {
type: InteractionResponseType.Modal,
data: isJSONEncodable(modal) ? modal.toJSON() : this.client.options.jsonTransformer(modal),
},
auth: false,
query: makeURLSearchParams({ with_response: options.withResponse ?? false }),
});
this.replied = true;
return options.withResponse ? new InteractionCallbackResponse(this.client, response) : undefined;
}
/**
@@ -300,7 +452,7 @@ class InteractionResponses {
* .then(interaction => console.log(`${interaction.customId} was submitted!`))
* .catch(console.error);
*/
awaitModalSubmit(options) {
async awaitModalSubmit(options) {
if (typeof options.time !== 'number') throw new DiscordjsError(ErrorCodes.InvalidType, 'time', 'number');
const _options = { ...options, max: 1, interactionType: InteractionType.ModalSubmit };
return new Promise((resolve, reject) => {
@@ -323,6 +475,7 @@ class InteractionResponses {
'followUp',
'deferUpdate',
'update',
'launchActivity',
'showModal',
'sendPremiumRequired',
'awaitModalSubmit',

View File

@@ -75,11 +75,19 @@ class TextBasedChannel {
* @property {?string} [content=''] The content for the message. This can only be `null` when editing a message.
* @property {Array<(EmbedBuilder|Embed|APIEmbed)>} [embeds] The embeds for the message
* @property {MessageMentionOptions} [allowedMentions] Which mentions should be parsed from the message content
* (see [here](https://discord.com/developers/docs/resources/message#allowed-mentions-object) for more details)
* (see {@link https://discord.com/developers/docs/resources/message#allowed-mentions-object here} for more details)
* @property {Array<(AttachmentBuilder|Attachment|AttachmentPayload|BufferResolvable)>} [files]
* The files to send with the message.
* @property {Array<(ActionRowBuilder|ActionRow|APIActionRowComponent)>} [components]
* Action rows containing interactive components for the message (buttons, select menus)
* @property {Array<(ActionRowBuilder|MessageTopLevelComponent|APIMessageTopLevelComponent)>} [components]
* Action rows containing interactive components for the message (buttons, select menus) and other
* top-level components.
* <info>When using components v2, the flag {@link MessageFlags.IsComponentsV2} needs to be set
* and `content`, `embeds`, `stickers`, and `poll` cannot be used.</info>
*/
/**
* The base message options for messages including a poll.
* @typedef {BaseMessageOptions} BaseMessageOptionsWithPoll
* @property {PollData} [poll] The poll to send with the message
*/
@@ -93,7 +101,7 @@ class TextBasedChannel {
/**
* The options for sending a message.
* @typedef {BaseMessageOptions} BaseMessageCreateOptions
* @typedef {BaseMessageOptionsWithPoll} BaseMessageCreateOptions
* @property {boolean} [tts=false] Whether the message should be spoken aloud
* @property {string} [nonce] The nonce for the message
* <info>This property is required if `enforceNonce` set to `true`.</info>
@@ -102,13 +110,23 @@ class TextBasedChannel {
* that message will be returned and no new message will be created
* @property {StickerResolvable[]} [stickers=[]] The stickers to send in the message
* @property {MessageFlags} [flags] Which flags to set for the message.
* <info>Only `MessageFlags.SuppressEmbeds` and `MessageFlags.SuppressNotifications` can be set.</info>
* <info>Only {@link MessageFlags.SuppressEmbeds}, {@link MessageFlags.SuppressNotifications} and
* {@link MessageFlags.IsComponentsV2} can be set.</info>
* <info>{@link MessageFlags.IsComponentsV2} is required if passing components that aren't action rows</info>
*/
/**
* @typedef {Object} ForwardOptions
* @property {MessageResolvable} message The originating message
* @property {TextBasedChannelResolvable} [channel] The channel of the originating message
* @property {GuildResolvable} [guild] The guild of the originating message
*/
/**
* The options for sending a message.
* @typedef {BaseMessageCreateOptions} MessageCreateOptions
* @property {ReplyOptions} [reply] The options for replying to a message
* @property {ForwardOptions} [forward] The options for forwarding a message
*/
/**

View File

@@ -60,6 +60,11 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIChannelSelectComponent}
*/
/**
* @external APIContainerComponent
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIContainerComponent}
*/
/**
* @external APIEmbed
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIEmbed}
@@ -80,6 +85,11 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIEmoji}
*/
/**
* @external APIFileComponent
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIFileComponent}
*/
/**
* @external APIGuild
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIGuild}
@@ -100,6 +110,16 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIGuildMember}
*/
/**
* @external APIGuildScheduledEventRecurrenceRule
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIGuildScheduledEventRecurrenceRule}
*/
/**
* @external APIIncidentsData
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIIncidentsData}
*/
/**
* @external APIInteraction
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIInteraction}
@@ -125,6 +145,16 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIInteractionGuildMember}
*/
/**
* @external APIMediaGalleryComponent
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIMediaGalleryComponent}
*/
/**
* @external APIMediaGalleryItem
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIMediaGalleryItem}
*/
/**
* @external APIMentionableSelectComponent
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIMentionableSelectComponent}
@@ -136,8 +166,8 @@
*/
/**
* @external APIMessageActionRowComponent
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIMessageActionRowComponent}
* @external APIComponentInMessageActionRow
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIComponentInMessageActionRow}
*/
/**
@@ -155,6 +185,11 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIMessageInteractionMetadata}
*/
/**
* @external APIMessageTopLevelComponent
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIMessageTopLevelComponent}
*/
/**
* @external APIModalInteractionResponse
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIModalInteractionResponse}
@@ -195,6 +230,16 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIRoleSelectComponent}
*/
/**
* @external APISectionComponent
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APISectionComponent}
*/
/**
* @external APISeparatorComponent
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APISeparatorComponent}
*/
/**
* @external APISelectMenuOption
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APISelectMenuOption}
@@ -215,6 +260,16 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APITextInputComponent}
*/
/**
* @external APIThumbnailComponent
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIThumbnailComponent}
*/
/**
* @external APIUnfurledMediaItem
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIUnfurledMediaItem}
*/
/**
* @external APIUser
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIUser}
@@ -250,6 +305,16 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/ApplicationRoleConnectionMetadataType}
*/
/**
* @external ApplicationWebhookEventStatus
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/ApplicationWebhookEventStatus}
*/
/**
* @external ApplicationWebhookEventType
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/ApplicationWebhookEventType}
*/
/**
* @external AttachmentFlags
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/AttachmentFlags}
@@ -305,6 +370,11 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/EntitlementType}
*/
/**
* @external EntryPointCommandHandlerType
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/EntryPointCommandHandlerType}
*/
/**
* @external ForumLayoutType
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/ForumLayoutType}
@@ -390,6 +460,21 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/GuildScheduledEventPrivacyLevel}
*/
/**
* @external GuildScheduledEventRecurrenceRuleFrequency
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/GuildScheduledEventRecurrenceRuleFrequency}
*/
/**
* @external GuildScheduledEventRecurrenceRuleMonth
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/GuildScheduledEventRecurrenceRuleMonth}
*/
/**
* @external GuildScheduledEventRecurrenceRuleWeekday
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/GuildScheduledEventRecurrenceRuleWeekday}
*/
/**
* @external GuildScheduledEventStatus
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/GuildScheduledEventStatus}
@@ -442,12 +527,7 @@
/**
* @external Locale
* @see {@link https://discord-api-types.dev/api/discord-api-types-rest/common/enum/Locale}
*/
/**
* @external LocaleString
* @see {@link https://discord-api-types.dev/api/discord-api-types-rest/common#LocaleString}
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/Locale}
*/
/**
@@ -455,6 +535,11 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/MessageActivityType}
*/
/**
* @external MessageReferenceType
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/MessageReferenceType}
*/
/**
* @external MessageType
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/MessageType}
@@ -477,7 +562,7 @@
/**
* @external PermissionFlagsBits
* @see {@link https://discord-api-types.dev/api/discord-api-types-payloads/common#PermissionFlagsBits}
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10#PermissionFlagsBits}
*/
/**
@@ -485,6 +570,11 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/PollLayoutType}
*/
/**
* @external ReactionType
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/ReactionType}
*/
/**
* @external RoleFlags
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/RoleFlags}
@@ -497,7 +587,7 @@
/**
* @external RESTJSONErrorCodes
* @see {@link https://discord-api-types.dev/api/discord-api-types-rest/common/enum/RESTJSONErrorCodes}
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/RESTJSONErrorCodes}
*/
/**
@@ -560,6 +650,11 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/VideoQualityMode}
*/
/**
* @external VoiceChannelEffectSendAnimationType
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/VoiceChannelEffectSendAnimationType}
*/
/**
* @external WebhookType
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/WebhookType}

View File

@@ -1,10 +1,11 @@
'use strict';
// This file contains the typedefs for camel-cased JSON data
const { ComponentBuilder } = require('@discordjs/builders');
const { ComponentType } = require('discord-api-types/v10');
/**
* @typedef {Object} BaseComponentData
* @property {number} [id] the id of this component
* @property {ComponentType} type The type of component
*/
@@ -16,30 +17,30 @@ const { ComponentType } = require('discord-api-types/v10');
/**
* @typedef {BaseComponentData} ButtonComponentData
* @property {ButtonStyle} style The style of the button
* @property {?boolean} disabled Whether this button is disabled
* @property {boolean} [disabled] Whether this button is disabled
* @property {string} label The label of this button
* @property {?APIMessageComponentEmoji} emoji The emoji on this button
* @property {?string} customId The custom id of the button
* @property {?string} url The URL of the button
* @property {APIMessageComponentEmoji} [emoji] The emoji on this button
* @property {string} [customId] The custom id of the button
* @property {string} [url] The URL of the button
*/
/**
* @typedef {object} SelectMenuComponentOptionData
* @property {string} label The label of the option
* @property {string} value The value of the option
* @property {?string} description The description of the option
* @property {?APIMessageComponentEmoji} emoji The emoji on the option
* @property {?boolean} default Whether this option is selected by default
* @property {string} [description] The description of the option
* @property {APIMessageComponentEmoji} [emoji] The emoji on the option
* @property {boolean} [default] Whether this option is selected by default
*/
/**
* @typedef {BaseComponentData} SelectMenuComponentData
* @property {string} customId The custom id of the select menu
* @property {?boolean} disabled Whether the select menu is disabled or not
* @property {?number} maxValues The maximum amount of options that can be selected
* @property {?number} minValues The minimum amount of options that can be selected
* @property {?SelectMenuComponentOptionData[]} options The options in this select menu
* @property {?string} placeholder The placeholder of the select menu
* @property {boolean} [disabled] Whether the select menu is disabled or not
* @property {number} [maxValues] The maximum amount of options that can be selected
* @property {number} [minValues] The minimum amount of options that can be selected
* @property {SelectMenuComponentOptionData[]} [options] The options in this select menu
* @property {string} [placeholder] The placeholder of the select menu
*/
/**
@@ -51,15 +52,76 @@ const { ComponentType } = require('discord-api-types/v10');
* @property {string} customId The custom id of the text input
* @property {TextInputStyle} style The style of the text input
* @property {string} label The text that appears on top of the text input field
* @property {?number} minLength The minimum number of characters that can be entered in the text input
* @property {?number} maxLength The maximum number of characters that can be entered in the text input
* @property {?boolean} required Whether or not the text input is required or not
* @property {?string} value The pre-filled text in the text input
* @property {?string} placeholder Placeholder for the text input
* @property {number} [minLength] The minimum number of characters that can be entered in the text input
* @property {number} [maxLength] The maximum number of characters that can be entered in the text input
* @property {boolean} [required] Whether or not the text input is required or not
* @property {string} [value] The pre-filled text in the text input
* @property {string} [placeholder] Placeholder for the text input
*/
/**
* @typedef {ActionRowData|ButtonComponentData|SelectMenuComponentData|TextInputComponentData} ComponentData
* @typedef {Object} UnfurledMediaItemData
* @property {string} url The url of this media item. Accepts either http:, https: or attachment: protocol
*/
/**
* @typedef {BaseComponentData} ThumbnailComponentData
* @property {UnfurledMediaItemData} media The media for the thumbnail
* @property {string} [description] The description of the thumbnail
* @property {boolean} [spoiler] Whether the thumbnail should be spoilered
*/
/**
* @typedef {BaseComponentData} FileComponentData
* @property {UnfurledMediaItemData} file The file media in this component
* @property {boolean} [spoiler] Whether the file should be spoilered
*/
/**
* @typedef {Object} MediaGalleryItemData
* @property {UnfurledMediaItemData} media The media for the media gallery item
* @property {string} [description] The description of the media gallery item
* @property {boolean} [spoiler] Whether the media gallery item should be spoilered
*/
/**
* @typedef {BaseComponentData} MediaGalleryComponentData
* @property {MediaGalleryItemData[]} items The media gallery items in this media gallery component
*/
/**
* @typedef {BaseComponentData} SeparatorComponentData
* @property {SeparatorSpacingSize} [spacing] The spacing size of this component
* @property {boolean} [divider] Whether the separator shows as a divider
*/
/**
* @typedef {BaseComponentData} SectionComponentData
* @property {Components[]} components The components in this section
* @property {ButtonComponentData|ThumbnailComponentData} accessory The accessory shown next to this section
*/
/**
* @typedef {BaseComponentData} TextDisplayComponentData
* @property {string} content The content displayed in this component
*/
/**
* @typedef {ActionRowData|FileComponentData|MediaGalleryComponentData|SectionComponentData|
* SeparatorComponentData|TextDisplayComponentData} ComponentInContainerData
*/
/**
* @typedef {BaseComponentData} ContainerComponentData
* @property {ComponentInContainerData} components The components in this container
* @property {?number} [accentColor] The accent color of this container
* @property {boolean} [spoiler] Whether the container should be spoilered
*/
/**
* @typedef {ActionRowData|ButtonComponentData|SelectMenuComponentData|TextInputComponentData|
* ThumbnailComponentData|FileComponentData|MediaGalleryComponentData|SeparatorComponentData|
* SectionComponentData|TextDisplayComponentData|ContainerComponentData} ComponentData
*/
/**
@@ -67,71 +129,66 @@ const { ComponentType } = require('discord-api-types/v10');
* @typedef {APIMessageComponentEmoji|string} ComponentEmojiResolvable
*/
/**
* @typedef {ActionRow|ContainerComponent|FileComponent|MediaGalleryComponent|
* SectionComponent|SeparatorComponent|TextDisplayComponent} MessageTopLevelComponent
*/
/**
* Transforms API data into a component
* @param {APIMessageComponent|Component} data The data to create the component from
* @returns {Component}
* @ignore
*/
function createComponent(data) {
if (data instanceof Component) {
return data;
}
switch (data.type) {
case ComponentType.ActionRow:
return new ActionRow(data);
case ComponentType.Button:
return new ButtonComponent(data);
case ComponentType.StringSelect:
return new StringSelectMenuComponent(data);
case ComponentType.TextInput:
return new TextInputComponent(data);
case ComponentType.UserSelect:
return new UserSelectMenuComponent(data);
case ComponentType.RoleSelect:
return new RoleSelectMenuComponent(data);
case ComponentType.MentionableSelect:
return new MentionableSelectMenuComponent(data);
case ComponentType.ChannelSelect:
return new ChannelSelectMenuComponent(data);
default:
return new Component(data);
}
return data instanceof Component ? data : new (ComponentTypeToComponent[data.type] ?? Component)(data);
}
/**
* Transforms API data into a component builder
* @param {APIMessageComponent|ComponentBuilder} data The data to create the component from
* @returns {ComponentBuilder}
* @ignore
*/
function createComponentBuilder(data) {
if (data instanceof ComponentBuilder) {
return data;
}
return data instanceof ComponentBuilder ? data : new (ComponentTypeToBuilder[data.type] ?? ComponentBuilder)(data);
}
switch (data.type) {
/**
* Extracts all interactive components from the component tree
* @param {Component|APIMessageComponent} component The component to find all interactive components in
* @returns {Array<Component|APIMessageComponent>}
* @ignore
*/
function extractInteractiveComponents(component) {
switch (component.type) {
case ComponentType.ActionRow:
return new ActionRowBuilder(data);
case ComponentType.Button:
return new ButtonBuilder(data);
case ComponentType.StringSelect:
return new StringSelectMenuBuilder(data);
case ComponentType.TextInput:
return new TextInputBuilder(data);
case ComponentType.UserSelect:
return new UserSelectMenuBuilder(data);
case ComponentType.RoleSelect:
return new RoleSelectMenuBuilder(data);
case ComponentType.MentionableSelect:
return new MentionableSelectMenuBuilder(data);
case ComponentType.ChannelSelect:
return new ChannelSelectMenuBuilder(data);
return component.components;
case ComponentType.Section:
return [...component.components, component.accessory];
case ComponentType.Container:
return component.components.flatMap(extractInteractiveComponents);
default:
return new ComponentBuilder(data);
return [component];
}
}
module.exports = { createComponent, createComponentBuilder };
/**
* Finds a component by customId in nested components
* @param {Array<Component|APIMessageComponent>} components The components to search in
* @param {string} customId The customId to search for
* @returns {Component|APIMessageComponent}
* @ignore
*/
function findComponentByCustomId(components, customId) {
return (
components
.flatMap(extractInteractiveComponents)
.find(component => (component.customId ?? component.custom_id) === customId) ?? null
);
}
module.exports = { createComponent, createComponentBuilder, findComponentByCustomId };
const ActionRow = require('../structures/ActionRow');
const ActionRowBuilder = require('../structures/ActionRowBuilder');
@@ -140,13 +197,49 @@ const ButtonComponent = require('../structures/ButtonComponent');
const ChannelSelectMenuBuilder = require('../structures/ChannelSelectMenuBuilder');
const ChannelSelectMenuComponent = require('../structures/ChannelSelectMenuComponent');
const Component = require('../structures/Component');
const ContainerComponent = require('../structures/ContainerComponent');
const FileComponent = require('../structures/FileComponent');
const MediaGalleryComponent = require('../structures/MediaGalleryComponent');
const MentionableSelectMenuBuilder = require('../structures/MentionableSelectMenuBuilder');
const MentionableSelectMenuComponent = require('../structures/MentionableSelectMenuComponent');
const RoleSelectMenuBuilder = require('../structures/RoleSelectMenuBuilder');
const RoleSelectMenuComponent = require('../structures/RoleSelectMenuComponent');
const SectionComponent = require('../structures/SectionComponent');
const SeparatorComponent = require('../structures/SeparatorComponent');
const StringSelectMenuBuilder = require('../structures/StringSelectMenuBuilder');
const StringSelectMenuComponent = require('../structures/StringSelectMenuComponent');
const TextDisplayComponent = require('../structures/TextDisplayComponent');
const TextInputBuilder = require('../structures/TextInputBuilder');
const TextInputComponent = require('../structures/TextInputComponent');
const ThumbnailComponent = require('../structures/ThumbnailComponent');
const UserSelectMenuBuilder = require('../structures/UserSelectMenuBuilder');
const UserSelectMenuComponent = require('../structures/UserSelectMenuComponent');
const ComponentTypeToComponent = {
[ComponentType.ActionRow]: ActionRow,
[ComponentType.Button]: ButtonComponent,
[ComponentType.StringSelect]: StringSelectMenuComponent,
[ComponentType.TextInput]: TextInputComponent,
[ComponentType.UserSelect]: UserSelectMenuComponent,
[ComponentType.RoleSelect]: RoleSelectMenuComponent,
[ComponentType.MentionableSelect]: MentionableSelectMenuComponent,
[ComponentType.ChannelSelect]: ChannelSelectMenuComponent,
[ComponentType.Container]: ContainerComponent,
[ComponentType.TextDisplay]: TextDisplayComponent,
[ComponentType.File]: FileComponent,
[ComponentType.MediaGallery]: MediaGalleryComponent,
[ComponentType.Section]: SectionComponent,
[ComponentType.Separator]: SeparatorComponent,
[ComponentType.Thumbnail]: ThumbnailComponent,
};
const ComponentTypeToBuilder = {
[ComponentType.ActionRow]: ActionRowBuilder,
[ComponentType.Button]: ButtonBuilder,
[ComponentType.StringSelect]: StringSelectMenuBuilder,
[ComponentType.TextInput]: TextInputBuilder,
[ComponentType.UserSelect]: UserSelectMenuBuilder,
[ComponentType.RoleSelect]: RoleSelectMenuBuilder,
[ComponentType.MentionableSelect]: MentionableSelectMenuBuilder,
[ComponentType.ChannelSelect]: ChannelSelectMenuBuilder,
};

View File

@@ -113,13 +113,14 @@ async function resolveFile(resource) {
*/
/**
* Resolves a Base64Resolvable to a Base 64 image.
* Resolves a Base64Resolvable to a Base 64 string.
* @param {Base64Resolvable} data The base 64 resolvable you want to resolve
* @param {string} [contentType='image/jpg'] The content type of the data
* @returns {?string}
* @private
*/
function resolveBase64(data) {
if (Buffer.isBuffer(data)) return `data:image/jpg;base64,${data.toString('base64')}`;
function resolveBase64(data, contentType = 'image/jpg') {
if (Buffer.isBuffer(data)) return `data:${contentType};base64,${data.toString('base64')}`;
return data;
}

View File

@@ -41,6 +41,10 @@
* @property {string} GuildScheduledEventUpdate guildScheduledEventUpdate
* @property {string} GuildScheduledEventUserAdd guildScheduledEventUserAdd
* @property {string} GuildScheduledEventUserRemove guildScheduledEventUserRemove
* @property {string} GuildSoundboardSoundCreate guildSoundboardSoundCreate
* @property {string} GuildSoundboardSoundDelete guildSoundboardSoundDelete
* @property {string} GuildSoundboardSoundsUpdate guildSoundboardSoundsUpdate
* @property {string} GuildSoundboardSoundUpdate guildSoundboardSoundUpdate
* @property {string} GuildStickerCreate stickerCreate
* @property {string} GuildStickerDelete stickerDelete
* @property {string} GuildStickerUpdate stickerUpdate
@@ -61,6 +65,7 @@
* @property {string} MessageReactionRemoveEmoji messageReactionRemoveEmoji
* @property {string} MessageUpdate messageUpdate
* @property {string} PresenceUpdate presenceUpdate
* @property {string} SoundboardSounds soundboardSounds
* @property {string} ShardDisconnect shardDisconnect
* @property {string} ShardError shardError
* @property {string} ShardReady shardReady
@@ -69,6 +74,9 @@
* @property {string} StageInstanceCreate stageInstanceCreate
* @property {string} StageInstanceDelete stageInstanceDelete
* @property {string} StageInstanceUpdate stageInstanceUpdate
* @property {string} SubscriptionCreate subscriptionCreate
* @property {string} SubscriptionUpdate subscriptionUpdate
* @property {string} SubscriptionDelete subscriptionDelete
* @property {string} ThreadCreate threadCreate
* @property {string} ThreadDelete threadDelete
* @property {string} ThreadListSync threadListSync
@@ -77,6 +85,7 @@
* @property {string} ThreadUpdate threadUpdate
* @property {string} TypingStart typingStart
* @property {string} UserUpdate userUpdate
* @property {string} VoiceChannelEffectSend voiceChannelEffectSend
* @property {string} VoiceServerUpdate voiceServerUpdate
* @property {string} VoiceStateUpdate voiceStateUpdate
* @property {string} Warn warn
@@ -128,6 +137,10 @@ module.exports = {
GuildScheduledEventUpdate: 'guildScheduledEventUpdate',
GuildScheduledEventUserAdd: 'guildScheduledEventUserAdd',
GuildScheduledEventUserRemove: 'guildScheduledEventUserRemove',
GuildSoundboardSoundCreate: 'guildSoundboardSoundCreate',
GuildSoundboardSoundDelete: 'guildSoundboardSoundDelete',
GuildSoundboardSoundsUpdate: 'guildSoundboardSoundsUpdate',
GuildSoundboardSoundUpdate: 'guildSoundboardSoundUpdate',
GuildStickerCreate: 'stickerCreate',
GuildStickerDelete: 'stickerDelete',
GuildStickerUpdate: 'stickerUpdate',
@@ -148,6 +161,7 @@ module.exports = {
MessageReactionRemoveEmoji: 'messageReactionRemoveEmoji',
MessageUpdate: 'messageUpdate',
PresenceUpdate: 'presenceUpdate',
SoundboardSounds: 'soundboardSounds',
Raw: 'raw',
ShardDisconnect: 'shardDisconnect',
ShardError: 'shardError',
@@ -157,6 +171,9 @@ module.exports = {
StageInstanceCreate: 'stageInstanceCreate',
StageInstanceDelete: 'stageInstanceDelete',
StageInstanceUpdate: 'stageInstanceUpdate',
SubscriptionCreate: 'subscriptionCreate',
SubscriptionUpdate: 'subscriptionUpdate',
SubscriptionDelete: 'subscriptionDelete',
ThreadCreate: 'threadCreate',
ThreadDelete: 'threadDelete',
ThreadListSync: 'threadListSync',
@@ -165,6 +182,7 @@ module.exports = {
ThreadUpdate: 'threadUpdate',
TypingStart: 'typingStart',
UserUpdate: 'userUpdate',
VoiceChannelEffectSend: 'voiceChannelEffectSend',
VoiceServerUpdate: 'voiceServerUpdate',
VoiceStateUpdate: 'voiceStateUpdate',
Warn: 'warn',

View File

@@ -23,6 +23,15 @@ class MessageFlagsBitField extends BitField {
* @param {BitFieldResolvable} [bits=0] Bit(s) to read from
*/
/**
* Data that can be resolved to give a message flags bit field. This can be:
* * A string (see {@link MessageFlagsBitField.Flags})
* * A message flag
* * An instance of {@link MessageFlagsBitField}
* * An array of `MessageFlagsResolvable`
* @typedef {string|number|MessageFlagsBitField|MessageFlagsResolvable[]} MessageFlagsResolvable
*/
/**
* Bitfield of the packed bits
* @type {number}

View File

@@ -30,7 +30,7 @@ const { version } = require('../../package.json');
* @property {MessageMentionOptions} [allowedMentions] The default value for {@link BaseMessageOptions#allowedMentions}
* @property {Partials[]} [partials] Structures allowed to be partial. This means events can be emitted even when
* they're missing all the data for a particular structure. See the "Partial Structures" topic on the
* [guide](https://discordjs.guide/popular-topics/partials.html) for some
* {@link https://discordjs.guide/popular-topics/partials.html guide} for some
* important usage information, as partials require you to put checks in place when handling data.
* @property {boolean} [failIfNotExists=true] The default value for {@link MessageReplyOptions#failIfNotExists}
* @property {PresenceData} [presence={}] Presence data to use upon login
@@ -41,7 +41,7 @@ const { version } = require('../../package.json');
* @property {WebsocketOptions} [ws] Options for the WebSocket
* @property {RESTOptions} [rest] Options for the REST manager
* @property {Function} [jsonTransformer] A function used to transform outgoing json data
* @property {boolean} [enforceNonce=false] The default value for {@link MessageReplyOptions#enforceNonce}
* @property {boolean} [enforceNonce=false] The default value for {@link MessageCreateOptions#enforceNonce}
*/
/**

View File

@@ -26,6 +26,7 @@ const { createEnum } = require('./Enums');
* @property {number} Reaction The partial to receive uncached reactions.
* @property {number} GuildScheduledEvent The partial to receive uncached guild scheduled events.
* @property {number} ThreadMember The partial to receive uncached thread members.
* @property {number} SoundboardSound The partial to receive uncached soundboard sounds.
*/
// JSDoc for IntelliSense purposes
@@ -41,4 +42,5 @@ module.exports = createEnum([
'Reaction',
'GuildScheduledEvent',
'ThreadMember',
'SoundboardSound',
]);

View File

@@ -2,6 +2,7 @@
const { isJSONEncodable } = require('@discordjs/util');
const snakeCase = require('lodash.snakecase');
const { resolvePartialEmoji } = require('./Util');
/**
* Transforms camel-cased keys into snake cased keys
@@ -13,7 +14,14 @@ function toSnakeCase(obj) {
if (obj instanceof Date) return obj;
if (isJSONEncodable(obj)) return toSnakeCase(obj.toJSON());
if (Array.isArray(obj)) return obj.map(toSnakeCase);
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [snakeCase(key), toSnakeCase(value)]));
return Object.fromEntries(
Object.entries(obj).map(([key, value]) => [
snakeCase(key),
// TODO: The special handling of 'emoji' is just a temporary fix for v14, will be dropped in v15.
// See https://github.com/discordjs/discord.js/issues/10909
key === 'emoji' && typeof value === 'string' ? resolvePartialEmoji(value) : toSnakeCase(value),
]),
);
}
/**
@@ -54,4 +62,43 @@ function _transformAPIMessageInteractionMetadata(client, messageInteractionMetad
};
}
module.exports = { toSnakeCase, _transformAPIAutoModerationAction, _transformAPIMessageInteractionMetadata };
/**
* Transforms a guild scheduled event recurrence rule object to a snake-cased variant.
* @param {GuildScheduledEventRecurrenceRuleOptions} recurrenceRule The recurrence rule to transform
* @returns {APIGuildScheduledEventRecurrenceRule}
* @ignore
*/
function _transformGuildScheduledEventRecurrenceRule(recurrenceRule) {
return {
start: new Date(recurrenceRule.startAt).toISOString(),
frequency: recurrenceRule.frequency,
interval: recurrenceRule.interval,
by_weekday: recurrenceRule.byWeekday,
by_n_weekday: recurrenceRule.byNWeekday,
by_month: recurrenceRule.byMonth,
by_month_day: recurrenceRule.byMonthDay,
};
}
/**
* Transforms API incidents data to a camel-cased variant.
* @param {APIIncidentsData} data The incidents data to transform
* @returns {IncidentActions}
* @ignore
*/
function _transformAPIIncidentsData(data) {
return {
invitesDisabledUntil: data.invites_disabled_until ? new Date(data.invites_disabled_until) : null,
dmsDisabledUntil: data.dms_disabled_until ? new Date(data.dms_disabled_until) : null,
dmSpamDetectedAt: data.dm_spam_detected_at ? new Date(data.dm_spam_detected_at) : null,
raidDetectedAt: data.raid_detected_at ? new Date(data.raid_detected_at) : null,
};
}
module.exports = {
toSnakeCase,
_transformAPIAutoModerationAction,
_transformAPIMessageInteractionMetadata,
_transformGuildScheduledEventRecurrenceRule,
_transformAPIIncidentsData,
};

View File

@@ -1,6 +1,7 @@
'use strict';
const { parse } = require('node:path');
const process = require('node:process');
const { Collection } = require('@discordjs/collection');
const { ChannelType, RouteBases, Routes } = require('discord-api-types/v10');
const { fetch } = require('undici');
@@ -8,6 +9,9 @@ const Colors = require('./Colors');
const { DiscordjsError, DiscordjsRangeError, DiscordjsTypeError, ErrorCodes } = require('../errors');
const isObject = d => typeof d === 'object' && d !== null;
let deprecationEmittedForUserFetchFlags = false;
let deprecationEmittedForRemoveThreadMember = false;
/**
* Flatten an object. Any properties that are collections will get converted to an array of keys.
* @param {Object} obj The object to flatten.
@@ -499,6 +503,32 @@ function resolveSKUId(resolvable) {
return null;
}
/**
* Deprecation function for fetching user flags.
* @param {string} name Name of the class
* @private
*/
function emitDeprecationWarningForUserFetchFlags(name) {
if (deprecationEmittedForUserFetchFlags) return;
process.emitWarning(`${name}#fetchFlags() is deprecated. Use ${name}#fetch() instead.`);
deprecationEmittedForUserFetchFlags = true;
}
/**
* Deprecation function for the reason parameter of removing thread members.
* @param {string} name Name of the class
* @private
*/
function emitDeprecationWarningForRemoveThreadMember(name) {
if (deprecationEmittedForRemoveThreadMember) return;
process.emitWarning(
`The reason parameter of ${name}#remove() is deprecated as Discord does not parse them. It will be removed in the next major version.`,
);
deprecationEmittedForRemoveThreadMember = true;
}
module.exports = {
flatten,
fetchRecommendedShardCount,
@@ -518,6 +548,8 @@ module.exports = {
parseWebhookURL,
transformResolved,
resolveSKUId,
emitDeprecationWarningForUserFetchFlags,
emitDeprecationWarningForRemoveThreadMember,
};
// Fixes Circular