This commit is contained in:
Lukian LEIZOUR 2023-01-29 00:57:47 +01:00
parent a15c733e45
commit 2a5130cbda
2838 changed files with 288613 additions and 0 deletions

39
node_modules/discord.js/src/WebSocket.js generated vendored Normal file
View file

@ -0,0 +1,39 @@
'use strict';
let erlpack;
const { Buffer } = require('node:buffer');
try {
erlpack = require('erlpack');
if (!erlpack.pack) erlpack = null;
} catch {} // eslint-disable-line no-empty
exports.WebSocket = require('ws');
const ab = new TextDecoder();
exports.encoding = erlpack ? 'etf' : 'json';
exports.pack = erlpack ? erlpack.pack : JSON.stringify;
exports.unpack = (data, type) => {
if (exports.encoding === 'json' || type === 'json') {
if (typeof data !== 'string') {
data = ab.decode(data);
}
return JSON.parse(data);
}
if (!Buffer.isBuffer(data)) data = Buffer.from(new Uint8Array(data));
return erlpack.unpack(data);
};
exports.create = (gateway, query = {}, ...args) => {
const [g, q] = gateway.split('?');
query.encoding = exports.encoding;
query = new URLSearchParams(query);
if (q) new URLSearchParams(q).forEach((v, k) => query.set(k, v));
const ws = new exports.WebSocket(`${g}?${query}`, ...args);
return ws;
};
for (const state of ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']) exports[state] = exports.WebSocket[state];

75
node_modules/discord.js/src/client/BaseClient.js generated vendored Normal file
View file

@ -0,0 +1,75 @@
'use strict';
const EventEmitter = require('node:events');
const { REST } = require('@discordjs/rest');
const { DiscordjsTypeError, ErrorCodes } = require('../errors');
const Options = require('../util/Options');
const { mergeDefault, flatten } = require('../util/Util');
/**
* The base class for all clients.
* @extends {EventEmitter}
*/
class BaseClient extends EventEmitter {
constructor(options = {}) {
super({ captureRejections: true });
if (typeof options !== 'object' || options === null) {
throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'options', 'object', true);
}
/**
* The options the client was instantiated with
* @type {ClientOptions}
*/
this.options = mergeDefault(Options.createDefault(), options);
/**
* The REST manager of the client
* @type {REST}
*/
this.rest = new REST(this.options.rest);
}
/**
* Destroys all assets used by the base client.
* @returns {void}
*/
destroy() {
this.rest.requestManager.clearHashSweeper();
this.rest.requestManager.clearHandlerSweeper();
}
/**
* Increments max listeners by one, if they are not zero.
* @private
*/
incrementMaxListeners() {
const maxListeners = this.getMaxListeners();
if (maxListeners !== 0) {
this.setMaxListeners(maxListeners + 1);
}
}
/**
* Decrements max listeners by one, if they are not zero.
* @private
*/
decrementMaxListeners() {
const maxListeners = this.getMaxListeners();
if (maxListeners !== 0) {
this.setMaxListeners(maxListeners - 1);
}
}
toJSON(...props) {
return flatten(this, ...props);
}
}
module.exports = BaseClient;
/**
* @external REST
* @see {@link https://discord.js.org/#/docs/rest/main/class/REST}
*/

558
node_modules/discord.js/src/client/Client.js generated vendored Normal file
View file

@ -0,0 +1,558 @@
'use strict';
const process = require('node:process');
const { Collection } = require('@discordjs/collection');
const { makeURLSearchParams } = require('@discordjs/rest');
const { OAuth2Scopes, Routes } = require('discord-api-types/v10');
const BaseClient = require('./BaseClient');
const ActionsManager = require('./actions/ActionsManager');
const ClientVoiceManager = require('./voice/ClientVoiceManager');
const WebSocketManager = require('./websocket/WebSocketManager');
const { DiscordjsError, DiscordjsTypeError, DiscordjsRangeError, ErrorCodes } = require('../errors');
const BaseGuildEmojiManager = require('../managers/BaseGuildEmojiManager');
const ChannelManager = require('../managers/ChannelManager');
const GuildManager = require('../managers/GuildManager');
const UserManager = require('../managers/UserManager');
const ShardClientUtil = require('../sharding/ShardClientUtil');
const ClientPresence = require('../structures/ClientPresence');
const GuildPreview = require('../structures/GuildPreview');
const GuildTemplate = require('../structures/GuildTemplate');
const Invite = require('../structures/Invite');
const { Sticker } = require('../structures/Sticker');
const StickerPack = require('../structures/StickerPack');
const VoiceRegion = require('../structures/VoiceRegion');
const Webhook = require('../structures/Webhook');
const Widget = require('../structures/Widget');
const DataResolver = require('../util/DataResolver');
const Events = require('../util/Events');
const IntentsBitField = require('../util/IntentsBitField');
const Options = require('../util/Options');
const PermissionsBitField = require('../util/PermissionsBitField');
const Status = require('../util/Status');
const Sweepers = require('../util/Sweepers');
/**
* The main hub for interacting with the Discord API, and the starting point for any bot.
* @extends {BaseClient}
*/
class Client extends BaseClient {
/**
* @param {ClientOptions} options Options for the client
*/
constructor(options) {
super(options);
const data = require('node:worker_threads').workerData ?? process.env;
const defaults = Options.createDefault();
if (this.options.shards === defaults.shards) {
if ('SHARDS' in data) {
this.options.shards = JSON.parse(data.SHARDS);
}
}
if (this.options.shardCount === defaults.shardCount) {
if ('SHARD_COUNT' in data) {
this.options.shardCount = Number(data.SHARD_COUNT);
} else if (Array.isArray(this.options.shards)) {
this.options.shardCount = this.options.shards.length;
}
}
const typeofShards = typeof this.options.shards;
if (typeofShards === 'undefined' && typeof this.options.shardCount === 'number') {
this.options.shards = Array.from({ length: this.options.shardCount }, (_, i) => i);
}
if (typeofShards === 'number') this.options.shards = [this.options.shards];
if (Array.isArray(this.options.shards)) {
this.options.shards = [
...new Set(
this.options.shards.filter(item => !isNaN(item) && item >= 0 && item < Infinity && item === (item | 0)),
),
];
}
this._validateOptions();
/**
* The WebSocket manager of the client
* @type {WebSocketManager}
*/
this.ws = new WebSocketManager(this);
/**
* The action manager of the client
* @type {ActionsManager}
* @private
*/
this.actions = new ActionsManager(this);
/**
* The voice manager of the client
* @type {ClientVoiceManager}
*/
this.voice = new ClientVoiceManager(this);
/**
* Shard helpers for the client (only if the process was spawned from a {@link ShardingManager})
* @type {?ShardClientUtil}
*/
this.shard = process.env.SHARDING_MANAGER
? ShardClientUtil.singleton(this, process.env.SHARDING_MANAGER_MODE)
: null;
/**
* All of the {@link User} objects that have been cached at any point, mapped by their ids
* @type {UserManager}
*/
this.users = new UserManager(this);
/**
* All of the guilds the client is currently handling, mapped by their ids -
* 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 -
* 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.
* @type {ChannelManager}
*/
this.channels = new ChannelManager(this);
/**
* The sweeping functions and their intervals used to periodically sweep caches
* @type {Sweepers}
*/
this.sweepers = new Sweepers(this, this.options.sweepers);
/**
* The presence of the Client
* @private
* @type {ClientPresence}
*/
this.presence = new ClientPresence(this, this.options.presence);
Object.defineProperty(this, 'token', { writable: true });
if (!this.token && 'DISCORD_TOKEN' in process.env) {
/**
* Authorization token for the logged in bot.
* If present, this defaults to `process.env.DISCORD_TOKEN` when instantiating the client
* <warn>This should be kept private at all times.</warn>
* @type {?string}
*/
this.token = process.env.DISCORD_TOKEN;
} else {
this.token = null;
}
/**
* User that the client is logged in as
* @type {?ClientUser}
*/
this.user = null;
/**
* The application of this bot
* @type {?ClientApplication}
*/
this.application = null;
/**
* Timestamp of the time the client was last {@link Status.Ready} at
* @type {?number}
*/
this.readyTimestamp = null;
}
/**
* All custom emojis that the client has access to, mapped by their ids
* @type {BaseGuildEmojiManager}
* @readonly
*/
get emojis() {
const emojis = new BaseGuildEmojiManager(this);
for (const guild of this.guilds.cache.values()) {
if (guild.available) for (const emoji of guild.emojis.cache.values()) emojis.cache.set(emoji.id, emoji);
}
return emojis;
}
/**
* Time at which the client was last regarded as being in the {@link Status.Ready} state
* (each time the client disconnects and successfully reconnects, this will be overwritten)
* @type {?Date}
* @readonly
*/
get readyAt() {
return this.readyTimestamp && new Date(this.readyTimestamp);
}
/**
* How long it has been since the client last entered the {@link Status.Ready} state in milliseconds
* @type {?number}
* @readonly
*/
get uptime() {
return this.readyTimestamp && Date.now() - this.readyTimestamp;
}
/**
* Logs the client in, establishing a WebSocket connection to Discord.
* @param {string} [token=this.token] Token of the account to log in with
* @returns {Promise<string>} Token of the account used
* @example
* client.login('my token');
*/
async login(token = this.token) {
if (!token || typeof token !== 'string') throw new DiscordjsError(ErrorCodes.TokenInvalid);
this.token = token = token.replace(/^(Bot|Bearer)\s*/i, '');
this.rest.setToken(token);
this.emit(Events.Debug, `Provided token: ${this._censoredToken}`);
if (this.options.presence) {
this.options.ws.presence = this.presence._parse(this.options.presence);
}
this.emit(Events.Debug, 'Preparing to connect to the gateway...');
try {
await this.ws.connect();
return this.token;
} catch (error) {
this.destroy();
throw error;
}
}
/**
* Returns whether the client has logged in, indicative of being able to access
* properties such as `user` and `application`.
* @returns {boolean}
*/
isReady() {
return this.ws.status === Status.Ready;
}
/**
* Logs out, terminates the connection to Discord, and destroys the client.
* @returns {void}
*/
destroy() {
super.destroy();
this.sweepers.destroy();
this.ws.destroy();
this.token = null;
this.rest.setToken(null);
}
/**
* Options used when fetching an invite from Discord.
* @typedef {Object} ClientFetchInviteOptions
* @property {Snowflake} [guildScheduledEventId] The id of the guild scheduled event to include with
* the invite
*/
/**
* Obtains an invite from Discord.
* @param {InviteResolvable} invite Invite code or URL
* @param {ClientFetchInviteOptions} [options] Options for fetching the invite
* @returns {Promise<Invite>}
* @example
* client.fetchInvite('https://discord.gg/djs')
* .then(invite => console.log(`Obtained invite with code: ${invite.code}`))
* .catch(console.error);
*/
async fetchInvite(invite, options) {
const code = DataResolver.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 });
return new Invite(this, data);
}
/**
* Obtains a template from Discord.
* @param {GuildTemplateResolvable} template Template code or URL
* @returns {Promise<GuildTemplate>}
* @example
* client.fetchGuildTemplate('https://discord.new/FKvmczH2HyUf')
* .then(template => console.log(`Obtained template with code: ${template.code}`))
* .catch(console.error);
*/
async fetchGuildTemplate(template) {
const code = DataResolver.resolveGuildTemplateCode(template);
const data = await this.rest.get(Routes.template(code));
return new GuildTemplate(this, data);
}
/**
* Obtains a webhook from Discord.
* @param {Snowflake} id The webhook's id
* @param {string} [token] Token for the webhook
* @returns {Promise<Webhook>}
* @example
* client.fetchWebhook('id', 'token')
* .then(webhook => console.log(`Obtained webhook with name: ${webhook.name}`))
* .catch(console.error);
*/
async fetchWebhook(id, token) {
const data = await this.rest.get(Routes.webhook(id, token), { auth: typeof token === 'undefined' });
return new Webhook(this, { token, ...data });
}
/**
* Obtains the available voice regions from Discord.
* @returns {Promise<Collection<string, VoiceRegion>>}
* @example
* client.fetchVoiceRegions()
* .then(regions => console.log(`Available regions are: ${regions.map(region => region.name).join(', ')}`))
* .catch(console.error);
*/
async fetchVoiceRegions() {
const apiRegions = await this.rest.get(Routes.voiceRegions());
const regions = new Collection();
for (const region of apiRegions) regions.set(region.id, new VoiceRegion(region));
return regions;
}
/**
* Obtains a sticker from Discord.
* @param {Snowflake} id The sticker's id
* @returns {Promise<Sticker>}
* @example
* client.fetchSticker('id')
* .then(sticker => console.log(`Obtained sticker with name: ${sticker.name}`))
* .catch(console.error);
*/
async fetchSticker(id) {
const data = await this.rest.get(Routes.sticker(id));
return new Sticker(this, data);
}
/**
* Obtains the list of sticker packs available to Nitro subscribers from Discord.
* @returns {Promise<Collection<Snowflake, StickerPack>>}
* @example
* client.fetchPremiumStickerPacks()
* .then(packs => console.log(`Available sticker packs are: ${packs.map(pack => pack.name).join(', ')}`))
* .catch(console.error);
*/
async fetchPremiumStickerPacks() {
const data = await this.rest.get(Routes.nitroStickerPacks());
return new Collection(data.sticker_packs.map(p => [p.id, new StickerPack(this, p)]));
}
/**
* 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
* @returns {Promise<GuildPreview>}
*/
async fetchGuildPreview(guild) {
const id = this.guilds.resolveId(guild);
if (!id) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'guild', 'GuildResolvable');
const data = await this.rest.get(Routes.guildPreview(id));
return new GuildPreview(this, data);
}
/**
* Obtains the widget data of a guild from Discord, available for guilds with the widget enabled.
* @param {GuildResolvable} guild The guild to fetch the widget data for
* @returns {Promise<Widget>}
*/
async fetchGuildWidget(guild) {
const id = this.guilds.resolveId(guild);
if (!id) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'guild', 'GuildResolvable');
const data = await this.rest.get(Routes.guildWidgetJSON(id));
return new Widget(this, data);
}
/**
* Options for {@link Client#generateInvite}.
* @typedef {Object} InviteGenerationOptions
* @property {OAuth2Scopes[]} scopes Scopes that should be requested
* @property {PermissionResolvable} [permissions] Permissions to request
* @property {GuildResolvable} [guild] Guild to preselect
* @property {boolean} [disableGuildSelect] Whether to disable the guild selection
*/
/**
* Generates a link that can be used to invite the bot to a guild.
* @param {InviteGenerationOptions} [options={}] Options for the invite
* @returns {string}
* @example
* const link = client.generateInvite({
* scopes: [OAuth2Scopes.ApplicationsCommands],
* });
* console.log(`Generated application invite link: ${link}`);
* @example
* const link = client.generateInvite({
* permissions: [
* PermissionFlagsBits.SendMessages,
* PermissionFlagsBits.ManageGuild,
* PermissionFlagsBits.MentionEveryone,
* ],
* scopes: [OAuth2Scopes.Bot],
* });
* console.log(`Generated bot invite link: ${link}`);
*/
generateInvite(options = {}) {
if (typeof options !== 'object') throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'options', 'object', true);
if (!this.application) throw new DiscordjsError(ErrorCodes.ClientNotReady, 'generate an invite link');
const { scopes } = options;
if (typeof scopes === 'undefined') {
throw new DiscordjsTypeError(ErrorCodes.InvalidMissingScopes);
}
if (!Array.isArray(scopes)) {
throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'scopes', 'Array of Invite Scopes', true);
}
if (!scopes.some(scope => [OAuth2Scopes.Bot, OAuth2Scopes.ApplicationsCommands].includes(scope))) {
throw new DiscordjsTypeError(ErrorCodes.InvalidMissingScopes);
}
const validScopes = Object.values(OAuth2Scopes);
const invalidScope = scopes.find(scope => !validScopes.includes(scope));
if (invalidScope) {
throw new DiscordjsTypeError(ErrorCodes.InvalidElement, 'Array', 'scopes', invalidScope);
}
const query = makeURLSearchParams({
client_id: this.application.id,
scope: scopes.join(' '),
disable_guild_select: options.disableGuildSelect,
});
if (options.permissions) {
const permissions = PermissionsBitField.resolve(options.permissions);
if (permissions) query.set('permissions', permissions.toString());
}
if (options.guild) {
const guildId = this.guilds.resolveId(options.guild);
if (!guildId) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'options.guild', 'GuildResolvable');
query.set('guild_id', guildId);
}
return `${this.options.rest.api}${Routes.oauth2Authorization()}?${query}`;
}
toJSON() {
return super.toJSON({
actions: false,
presence: false,
});
}
/**
* Partially censored client token for debug logging purposes.
* @type {?string}
* @readonly
* @private
*/
get _censoredToken() {
if (!this.token) return null;
return this.token
.split('.')
.map((val, i) => (i > 1 ? val.replace(/./g, '*') : val))
.join('.');
}
/**
* Calls {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval} on a script
* with the client as `this`.
* @param {string} script Script to eval
* @returns {*}
* @private
*/
_eval(script) {
return eval(script);
}
/**
* Validates the client options.
* @param {ClientOptions} [options=this.options] Options to validate
* @private
*/
_validateOptions(options = this.options) {
if (typeof options.intents === 'undefined') {
throw new DiscordjsTypeError(ErrorCodes.ClientMissingIntents);
} else {
options.intents = new IntentsBitField(options.intents).freeze();
}
if (typeof options.shardCount !== 'number' || isNaN(options.shardCount) || options.shardCount < 1) {
throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'shardCount', 'a number greater than or equal to 1');
}
if (options.shards && !(options.shards === 'auto' || Array.isArray(options.shards))) {
throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'shards', "'auto', a number or array of numbers");
}
if (options.shards && !options.shards.length) throw new DiscordjsRangeError(ErrorCodes.ClientInvalidProvidedShards);
if (typeof options.makeCache !== 'function') {
throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'makeCache', 'a function');
}
if (typeof options.sweepers !== 'object' || options.sweepers === null) {
throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'sweepers', 'an object');
}
if (!Array.isArray(options.partials)) {
throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'partials', 'an Array');
}
if (typeof options.waitGuildTimeout !== 'number' || isNaN(options.waitGuildTimeout)) {
throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'waitGuildTimeout', 'a number');
}
if (typeof options.failIfNotExists !== 'boolean') {
throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'failIfNotExists', 'a boolean');
}
}
}
module.exports = Client;
/**
* A {@link https://developer.twitter.com/en/docs/twitter-ids Twitter snowflake},
* except the epoch is 2015-01-01T00:00:00.000Z.
*
* If we have a snowflake '266241948824764416' we can represent it as binary:
* ```
* 64 22 17 12 0
* 000000111011000111100001101001000101000000 00001 00000 000000000000
* number of milliseconds since Discord epoch worker pid increment
* ```
* @typedef {string} Snowflake
*/
/**
* Emitted for general debugging information.
* @event Client#debug
* @param {string} info The debug information
*/
/**
* Emitted for general warnings.
* @event Client#warn
* @param {string} info The warning
*/
/**
* @external Collection
* @see {@link https://discord.js.org/#/docs/collection/main/class/Collection}
*/
/**
* @external ImageURLOptions
* @see {@link https://discord.js.org/#/docs/rest/main/typedef/ImageURLOptions}
*/
/**
* @external BaseImageURLOptions
* @see {@link https://discord.js.org/#/docs/rest/main/typedef/BaseImageURLOptions}
*/

103
node_modules/discord.js/src/client/WebhookClient.js generated vendored Normal file
View file

@ -0,0 +1,103 @@
'use strict';
const BaseClient = require('./BaseClient');
const { DiscordjsError, ErrorCodes } = require('../errors');
const Webhook = require('../structures/Webhook');
const { parseWebhookURL } = require('../util/Util');
/**
* The webhook client.
* @implements {Webhook}
* @extends {BaseClient}
*/
class WebhookClient extends BaseClient {
/**
* Represents the credentials used for a webhook in the form of its id and token.
* @typedef {Object} WebhookClientDataIdWithToken
* @property {Snowflake} id The webhook's id
* @property {string} token The webhook's token
*/
/**
* Represents the credentials used for a webhook in the form of a URL.
* @typedef {Object} WebhookClientDataURL
* @property {string} url The full URL for the webhook
*/
/**
* Represents the credentials used for a webhook.
* @typedef {WebhookClientDataIdWithToken|WebhookClientDataURL} WebhookClientData
*/
/**
* Options for a webhook client.
* @typedef {Object} WebhookClientOptions
* @property {MessageMentionOptions} [allowedMentions] Default value for {@link BaseMessageOptions#allowedMentions}
* @property {RESTOptions} [rest] Options for the REST manager
*/
/**
* @param {WebhookClientData} data The data of the webhook
* @param {WebhookClientOptions} [options] Options for the webhook client
*/
constructor(data, options) {
super(options);
Object.defineProperty(this, 'client', { value: this });
let { id, token } = data;
if ('url' in data) {
const parsed = parseWebhookURL(data.url);
if (!parsed) {
throw new DiscordjsError(ErrorCodes.WebhookURLInvalid);
}
({ id, token } = parsed);
}
this.id = id;
Object.defineProperty(this, 'token', { value: token, writable: true, configurable: true });
}
/**
* The options the webhook client was instantiated with.
* @type {WebhookClientOptions}
* @name WebhookClient#options
*/
// These are here only for documentation purposes - they are implemented by Webhook
/* eslint-disable no-empty-function, valid-jsdoc */
/**
* Sends a message with this webhook.
* @param {string|MessagePayload|WebhookCreateMessageOptions} options The content for the reply
* @returns {Promise<APIMessage>}
*/
send() {}
/**
* Gets a message that was sent by this webhook.
* @param {Snowflake} message The id of the message to fetch
* @param {WebhookFetchMessageOptions} [options={}] The options to provide to fetch the message.
* @returns {Promise<APIMessage>} Returns the message sent by this webhook
*/
fetchMessage() {}
/**
* Edits a message that was sent by this webhook.
* @param {MessageResolvable} message The message to edit
* @param {string|MessagePayload|WebhookEditMessageOptions} options The options to provide
* @returns {Promise<APIMessage>} Returns the message edited by this webhook
*/
editMessage() {}
sendSlackMessage() {}
edit() {}
delete() {}
deleteMessage() {}
get createdTimestamp() {}
get createdAt() {}
get url() {}
}
Webhook.applyToClass(WebhookClient);
module.exports = WebhookClient;

119
node_modules/discord.js/src/client/actions/Action.js generated vendored Normal file
View file

@ -0,0 +1,119 @@
'use strict';
const Partials = require('../../util/Partials');
/*
ABOUT ACTIONS
Actions are similar to WebSocket Packet Handlers, but since introducing
the REST API methods, in order to prevent rewriting code to handle data,
"actions" have been introduced. They're basically what Packet Handlers
used to be but they're strictly for manipulating data and making sure
that WebSocket events don't clash with REST methods.
*/
class GenericAction {
constructor(client) {
this.client = client;
}
handle(data) {
return data;
}
getPayload(data, manager, id, partialType, cache) {
const existing = manager.cache.get(id);
if (!existing && this.client.options.partials.includes(partialType)) {
return manager._add(data, cache);
}
return existing;
}
getChannel(data) {
const id = data.channel_id ?? data.id;
return (
data.channel ??
this.getPayload(
{
id,
guild_id: data.guild_id,
recipients: [data.author ?? data.user ?? { id: data.user_id }],
},
this.client.channels,
id,
Partials.Channel,
)
);
}
getMessage(data, channel, cache) {
const id = data.message_id ?? data.id;
return (
data.message ??
this.getPayload(
{
id,
channel_id: channel.id,
guild_id: data.guild_id ?? channel.guild?.id,
},
channel.messages,
id,
Partials.Message,
cache,
)
);
}
getReaction(data, message, user) {
const id = data.emoji.id ?? decodeURIComponent(data.emoji.name);
return this.getPayload(
{
emoji: data.emoji,
count: message.partial ? null : 0,
me: user?.id === this.client.user.id,
},
message.reactions,
id,
Partials.Reaction,
);
}
getMember(data, guild) {
return this.getPayload(data, guild.members, data.user.id, Partials.GuildMember);
}
getUser(data) {
const id = data.user_id;
return data.user ?? this.getPayload({ id }, this.client.users, id, Partials.User);
}
getUserFromMember(data) {
if (data.guild_id && data.member?.user) {
const guild = this.client.guilds.cache.get(data.guild_id);
if (guild) {
return guild.members._add(data.member).user;
} else {
return this.client.users._add(data.member.user);
}
}
return this.getUser(data);
}
getScheduledEvent(data, guild) {
const id = data.guild_scheduled_event_id ?? data.id;
return this.getPayload(
{ id, guild_id: data.guild_id ?? guild.id },
guild.scheduledEvents,
id,
Partials.GuildScheduledEvent,
);
}
getThreadMember(id, manager) {
return this.getPayload({ user_id: id }, manager, id, Partials.ThreadMember, false);
}
}
module.exports = GenericAction;

View file

@ -0,0 +1,71 @@
'use strict';
class ActionsManager {
constructor(client) {
this.client = client;
this.register(require('./ApplicationCommandPermissionsUpdate'));
this.register(require('./AutoModerationActionExecution'));
this.register(require('./AutoModerationRuleCreate'));
this.register(require('./AutoModerationRuleDelete'));
this.register(require('./AutoModerationRuleUpdate'));
this.register(require('./ChannelCreate'));
this.register(require('./ChannelDelete'));
this.register(require('./ChannelUpdate'));
this.register(require('./GuildBanAdd'));
this.register(require('./GuildBanRemove'));
this.register(require('./GuildChannelsPositionUpdate'));
this.register(require('./GuildDelete'));
this.register(require('./GuildEmojiCreate'));
this.register(require('./GuildEmojiDelete'));
this.register(require('./GuildEmojiUpdate'));
this.register(require('./GuildEmojisUpdate'));
this.register(require('./GuildIntegrationsUpdate'));
this.register(require('./GuildMemberRemove'));
this.register(require('./GuildMemberUpdate'));
this.register(require('./GuildRoleCreate'));
this.register(require('./GuildRoleDelete'));
this.register(require('./GuildRoleUpdate'));
this.register(require('./GuildRolesPositionUpdate'));
this.register(require('./GuildScheduledEventCreate'));
this.register(require('./GuildScheduledEventDelete'));
this.register(require('./GuildScheduledEventUpdate'));
this.register(require('./GuildScheduledEventUserAdd'));
this.register(require('./GuildScheduledEventUserRemove'));
this.register(require('./GuildStickerCreate'));
this.register(require('./GuildStickerDelete'));
this.register(require('./GuildStickerUpdate'));
this.register(require('./GuildStickersUpdate'));
this.register(require('./GuildUpdate'));
this.register(require('./InteractionCreate'));
this.register(require('./InviteCreate'));
this.register(require('./InviteDelete'));
this.register(require('./MessageCreate'));
this.register(require('./MessageDelete'));
this.register(require('./MessageDeleteBulk'));
this.register(require('./MessageReactionAdd'));
this.register(require('./MessageReactionRemove'));
this.register(require('./MessageReactionRemoveAll'));
this.register(require('./MessageReactionRemoveEmoji'));
this.register(require('./MessageUpdate'));
this.register(require('./PresenceUpdate'));
this.register(require('./StageInstanceCreate'));
this.register(require('./StageInstanceDelete'));
this.register(require('./StageInstanceUpdate'));
this.register(require('./ThreadCreate'));
this.register(require('./ThreadDelete'));
this.register(require('./ThreadListSync'));
this.register(require('./ThreadMemberUpdate'));
this.register(require('./ThreadMembersUpdate'));
this.register(require('./TypingStart'));
this.register(require('./UserUpdate'));
this.register(require('./VoiceStateUpdate'));
this.register(require('./WebhooksUpdate'));
}
register(Action) {
this[Action.name.replace(/Action$/, '')] = new Action(this.client);
}
}
module.exports = ActionsManager;

View file

@ -0,0 +1,34 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
/**
* The data received in the {@link Client#event:applicationCommandPermissionsUpdate} event
* @typedef {Object} ApplicationCommandPermissionsUpdateData
* @property {Snowflake} id The id of the command or global entity that was updated
* @property {Snowflake} guildId The id of the guild in which permissions were updated
* @property {Snowflake} applicationId The id of the application that owns the command or entity being updated
* @property {ApplicationCommandPermissions[]} permissions The updated permissions
*/
class ApplicationCommandPermissionsUpdateAction extends Action {
handle(data) {
const client = this.client;
/**
* Emitted whenever permissions for an application command in a guild were updated.
* <warn>This includes permission updates for other applications in addition to the logged in client,
* check `data.applicationId` to verify which application the update is for</warn>
* @event Client#applicationCommandPermissionsUpdate
* @param {ApplicationCommandPermissionsUpdateData} data The updated permissions
*/
client.emit(Events.ApplicationCommandPermissionsUpdate, {
permissions: data.permissions,
id: data.id,
guildId: data.guild_id,
applicationId: data.application_id,
});
}
}
module.exports = ApplicationCommandPermissionsUpdateAction;

View file

@ -0,0 +1,26 @@
'use strict';
const Action = require('./Action');
const AutoModerationActionExecution = require('../../structures/AutoModerationActionExecution');
const Events = require('../../util/Events');
class AutoModerationActionExecutionAction extends Action {
handle(data) {
const { client } = this;
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
/**
* Emitted whenever an auto moderation rule is triggered.
* <info>This event requires the {@link PermissionFlagsBits.ManageGuild} permission.</info>
* @event Client#autoModerationActionExecution
* @param {AutoModerationActionExecution} autoModerationActionExecution The data of the execution
*/
client.emit(Events.AutoModerationActionExecution, new AutoModerationActionExecution(data, guild));
}
return {};
}
}
module.exports = AutoModerationActionExecutionAction;

View file

@ -0,0 +1,27 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class AutoModerationRuleCreateAction extends Action {
handle(data) {
const { client } = this;
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
const autoModerationRule = guild.autoModerationRules._add(data);
/**
* Emitted whenever an auto moderation rule is created.
* <info>This event requires the {@link PermissionFlagsBits.ManageGuild} permission.</info>
* @event Client#autoModerationRuleCreate
* @param {AutoModerationRule} autoModerationRule The created auto moderation rule
*/
client.emit(Events.AutoModerationRuleCreate, autoModerationRule);
}
return {};
}
}
module.exports = AutoModerationRuleCreateAction;

View file

@ -0,0 +1,31 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class AutoModerationRuleDeleteAction extends Action {
handle(data) {
const { client } = this;
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
const autoModerationRule = guild.autoModerationRules.cache.get(data.id);
if (autoModerationRule) {
guild.autoModerationRules.cache.delete(autoModerationRule.id);
/**
* Emitted whenever an auto moderation rule is deleted.
* <info>This event requires the {@link PermissionFlagsBits.ManageGuild} permission.</info>
* @event Client#autoModerationRuleDelete
* @param {AutoModerationRule} autoModerationRule The deleted auto moderation rule
*/
client.emit(Events.AutoModerationRuleDelete, autoModerationRule);
}
}
return {};
}
}
module.exports = AutoModerationRuleDeleteAction;

View file

@ -0,0 +1,29 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class AutoModerationRuleUpdateAction extends Action {
handle(data) {
const { client } = this;
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
const oldAutoModerationRule = guild.autoModerationRules.cache.get(data.id)?._clone() ?? null;
const newAutoModerationRule = guild.autoModerationRules._add(data);
/**
* Emitted whenever an auto moderation rule gets updated.
* <info>This event requires the {@link PermissionFlagsBits.ManageGuild} permission.</info>
* @event Client#autoModerationRuleUpdate
* @param {?AutoModerationRule} oldAutoModerationRule The auto moderation rule before the update
* @param {AutoModerationRule} newAutoModerationRule The auto moderation rule after the update
*/
client.emit(Events.AutoModerationRuleUpdate, oldAutoModerationRule, newAutoModerationRule);
}
return {};
}
}
module.exports = AutoModerationRuleUpdateAction;

View file

@ -0,0 +1,23 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class ChannelCreateAction extends Action {
handle(data) {
const client = this.client;
const existing = client.channels.cache.has(data.id);
const channel = client.channels._add(data);
if (!existing && channel) {
/**
* Emitted whenever a guild channel is created.
* @event Client#channelCreate
* @param {GuildChannel} channel The channel that was created
*/
client.emit(Events.ChannelCreate, channel);
}
return { channel };
}
}
module.exports = ChannelCreateAction;

View file

@ -0,0 +1,23 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class ChannelDeleteAction extends Action {
handle(data) {
const client = this.client;
const channel = client.channels.cache.get(data.id);
if (channel) {
client.channels._remove(channel.id);
/**
* Emitted whenever a channel is deleted.
* @event Client#channelDelete
* @param {DMChannel|GuildChannel} channel The channel that was deleted
*/
client.emit(Events.ChannelDelete, channel);
}
}
}
module.exports = ChannelDeleteAction;

View file

@ -0,0 +1,33 @@
'use strict';
const Action = require('./Action');
const { createChannel } = require('../../util/Channels');
class ChannelUpdateAction extends Action {
handle(data) {
const client = this.client;
let channel = client.channels.cache.get(data.id);
if (channel) {
const old = channel._update(data);
if (channel.type !== data.type) {
const newChannel = createChannel(this.client, data, channel.guild);
for (const [id, message] of channel.messages.cache) newChannel.messages.cache.set(id, message);
channel = newChannel;
this.client.channels.cache.set(channel.id, channel);
}
return {
old,
updated: channel,
};
} else {
client.channels._add(data);
}
return {};
}
}
module.exports = ChannelUpdateAction;

View file

@ -0,0 +1,20 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class GuildBanAdd extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.cache.get(data.guild_id);
/**
* Emitted whenever a member is banned from a guild.
* @event Client#guildBanAdd
* @param {GuildBan} ban The ban that occurred
*/
if (guild) client.emit(Events.GuildBanAdd, guild.bans._add(data));
}
}
module.exports = GuildBanAdd;

View file

@ -0,0 +1,25 @@
'use strict';
const Action = require('./Action');
const GuildBan = require('../../structures/GuildBan');
const Events = require('../../util/Events');
class GuildBanRemove extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.cache.get(data.guild_id);
/**
* Emitted whenever a member is unbanned from a guild.
* @event Client#guildBanRemove
* @param {GuildBan} ban The ban that was removed
*/
if (guild) {
const ban = guild.bans.cache.get(data.user.id) ?? new GuildBan(client, data, guild);
guild.bans.cache.delete(ban.user.id);
client.emit(Events.GuildBanRemove, ban);
}
}
}
module.exports = GuildBanRemove;

View file

@ -0,0 +1,21 @@
'use strict';
const Action = require('./Action');
class GuildChannelsPositionUpdate extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
for (const partialChannel of data.channels) {
const channel = guild.channels.cache.get(partialChannel.id);
if (channel) channel.rawPosition = partialChannel.position;
}
}
return { guild };
}
}
module.exports = GuildChannelsPositionUpdate;

View file

@ -0,0 +1,44 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class GuildDeleteAction extends Action {
handle(data) {
const client = this.client;
let guild = client.guilds.cache.get(data.id);
if (guild) {
if (data.unavailable) {
// Guild is unavailable
guild.available = false;
/**
* Emitted whenever a guild becomes unavailable, likely due to a server outage.
* @event Client#guildUnavailable
* @param {Guild} guild The guild that has become unavailable
*/
client.emit(Events.GuildUnavailable, guild);
// Stops the GuildDelete packet thinking a guild was actually deleted,
// handles emitting of event itself
return;
}
for (const channel of guild.channels.cache.values()) this.client.channels._remove(channel.id);
client.voice.adapters.get(data.id)?.destroy();
// Delete guild
client.guilds.cache.delete(guild.id);
/**
* Emitted whenever a guild kicks the client or the guild is deleted/left.
* @event Client#guildDelete
* @param {Guild} guild The guild that was deleted
*/
client.emit(Events.GuildDelete, guild);
}
}
}
module.exports = GuildDeleteAction;

View file

@ -0,0 +1,20 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class GuildEmojiCreateAction extends Action {
handle(guild, createdEmoji) {
const already = guild.emojis.cache.has(createdEmoji.id);
const emoji = guild.emojis._add(createdEmoji);
/**
* Emitted whenever a custom emoji is created in a guild.
* @event Client#emojiCreate
* @param {GuildEmoji} emoji The emoji that was created
*/
if (!already) this.client.emit(Events.GuildEmojiCreate, emoji);
return { emoji };
}
}
module.exports = GuildEmojiCreateAction;

View file

@ -0,0 +1,19 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class GuildEmojiDeleteAction extends Action {
handle(emoji) {
emoji.guild.emojis.cache.delete(emoji.id);
/**
* Emitted whenever a custom emoji is deleted in a guild.
* @event Client#emojiDelete
* @param {GuildEmoji} emoji The emoji that was deleted
*/
this.client.emit(Events.GuildEmojiDelete, emoji);
return { emoji };
}
}
module.exports = GuildEmojiDeleteAction;

View file

@ -0,0 +1,20 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class GuildEmojiUpdateAction extends Action {
handle(current, data) {
const old = current._update(data);
/**
* Emitted whenever a custom emoji is updated in a guild.
* @event Client#emojiUpdate
* @param {GuildEmoji} oldEmoji The old emoji
* @param {GuildEmoji} newEmoji The new emoji
*/
this.client.emit(Events.GuildEmojiUpdate, old, current);
return { emoji: current };
}
}
module.exports = GuildEmojiUpdateAction;

View file

@ -0,0 +1,34 @@
'use strict';
const Action = require('./Action');
class GuildEmojisUpdateAction extends Action {
handle(data) {
const guild = this.client.guilds.cache.get(data.guild_id);
if (!guild?.emojis) return;
const deletions = new Map(guild.emojis.cache);
for (const emoji of data.emojis) {
// Determine type of emoji event
const cachedEmoji = guild.emojis.cache.get(emoji.id);
if (cachedEmoji) {
deletions.delete(emoji.id);
if (!cachedEmoji.equals(emoji)) {
// Emoji updated
this.client.actions.GuildEmojiUpdate.handle(cachedEmoji, emoji);
}
} else {
// Emoji added
this.client.actions.GuildEmojiCreate.handle(guild, emoji);
}
}
for (const emoji of deletions.values()) {
// Emoji deleted
this.client.actions.GuildEmojiDelete.handle(emoji);
}
}
}
module.exports = GuildEmojisUpdateAction;

View file

@ -0,0 +1,19 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class GuildIntegrationsUpdate extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.cache.get(data.guild_id);
/**
* Emitted whenever a guild integration is updated
* @event Client#guildIntegrationsUpdate
* @param {Guild} guild The guild whose integrations were updated
*/
if (guild) client.emit(Events.GuildIntegrationsUpdate, guild);
}
}
module.exports = GuildIntegrationsUpdate;

View file

@ -0,0 +1,31 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
const Status = require('../../util/Status');
class GuildMemberRemoveAction extends Action {
handle(data, shard) {
const client = this.client;
const guild = client.guilds.cache.get(data.guild_id);
let member = null;
if (guild) {
member = this.getMember({ user: data.user }, guild);
guild.memberCount--;
if (member) {
guild.members.cache.delete(member.id);
/**
* Emitted whenever a member leaves a guild, or is kicked.
* @event Client#guildMemberRemove
* @param {GuildMember} member The member that has left/been kicked from the guild
*/
if (shard.status === Status.Ready) client.emit(Events.GuildMemberRemove, member);
}
guild.presences.cache.delete(data.user.id);
guild.voiceStates.cache.delete(data.user.id);
}
return { guild, member };
}
}
module.exports = GuildMemberRemoveAction;

View file

@ -0,0 +1,44 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
const Status = require('../../util/Status');
class GuildMemberUpdateAction extends Action {
handle(data, shard) {
const { client } = this;
if (data.user.username) {
const user = client.users.cache.get(data.user.id);
if (!user) {
client.users._add(data.user);
} else if (!user._equals(data.user)) {
client.actions.UserUpdate.handle(data.user);
}
}
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
const member = this.getMember({ user: data.user }, guild);
if (member) {
const old = member._update(data);
/**
* Emitted whenever a guild member changes - i.e. new role, removed role, nickname.
* @event Client#guildMemberUpdate
* @param {GuildMember} oldMember The member before the update
* @param {GuildMember} newMember The member after the update
*/
if (shard.status === Status.Ready && !member.equals(old)) client.emit(Events.GuildMemberUpdate, old, member);
} else {
const newMember = guild.members._add(data);
/**
* Emitted whenever a member becomes available.
* @event Client#guildMemberAvailable
* @param {GuildMember} member The member that became available
*/
this.client.emit(Events.GuildMemberAvailable, newMember);
}
}
}
}
module.exports = GuildMemberUpdateAction;

View file

@ -0,0 +1,25 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class GuildRoleCreate extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.cache.get(data.guild_id);
let role;
if (guild) {
const already = guild.roles.cache.has(data.role.id);
role = guild.roles._add(data.role);
/**
* Emitted whenever a role is created.
* @event Client#roleCreate
* @param {Role} role The role that was created
*/
if (!already) client.emit(Events.GuildRoleCreate, role);
}
return { role };
}
}
module.exports = GuildRoleCreate;

View file

@ -0,0 +1,29 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class GuildRoleDeleteAction extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.cache.get(data.guild_id);
let role;
if (guild) {
role = guild.roles.cache.get(data.role_id);
if (role) {
guild.roles.cache.delete(data.role_id);
/**
* Emitted whenever a guild role is deleted.
* @event Client#roleDelete
* @param {Role} role The role that was deleted
*/
client.emit(Events.GuildRoleDelete, role);
}
}
return { role };
}
}
module.exports = GuildRoleDeleteAction;

View file

@ -0,0 +1,39 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class GuildRoleUpdateAction extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
let old = null;
const role = guild.roles.cache.get(data.role.id);
if (role) {
old = role._update(data.role);
/**
* Emitted whenever a guild role is updated.
* @event Client#roleUpdate
* @param {Role} oldRole The role before the update
* @param {Role} newRole The role after the update
*/
client.emit(Events.GuildRoleUpdate, old, role);
}
return {
old,
updated: role,
};
}
return {
old: null,
updated: null,
};
}
}
module.exports = GuildRoleUpdateAction;

View file

@ -0,0 +1,21 @@
'use strict';
const Action = require('./Action');
class GuildRolesPositionUpdate extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
for (const partialRole of data.roles) {
const role = guild.roles.cache.get(partialRole.id);
if (role) role.rawPosition = partialRole.position;
}
}
return { guild };
}
}
module.exports = GuildRolesPositionUpdate;

View file

@ -0,0 +1,27 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class GuildScheduledEventCreateAction extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
const guildScheduledEvent = guild.scheduledEvents._add(data);
/**
* Emitted whenever a guild scheduled event is created.
* @event Client#guildScheduledEventCreate
* @param {GuildScheduledEvent} guildScheduledEvent The created guild scheduled event
*/
client.emit(Events.GuildScheduledEventCreate, guildScheduledEvent);
return { guildScheduledEvent };
}
return {};
}
}
module.exports = GuildScheduledEventCreateAction;

View file

@ -0,0 +1,31 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class GuildScheduledEventDeleteAction extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
const guildScheduledEvent = this.getScheduledEvent(data, guild);
if (guildScheduledEvent) {
guild.scheduledEvents.cache.delete(guildScheduledEvent.id);
/**
* Emitted whenever a guild scheduled event is deleted.
* @event Client#guildScheduledEventDelete
* @param {GuildScheduledEvent} guildScheduledEvent The deleted guild scheduled event
*/
client.emit(Events.GuildScheduledEventDelete, guildScheduledEvent);
return { guildScheduledEvent };
}
}
return {};
}
}
module.exports = GuildScheduledEventDeleteAction;

View file

@ -0,0 +1,30 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class GuildScheduledEventUpdateAction extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
const oldGuildScheduledEvent = guild.scheduledEvents.cache.get(data.id)?._clone() ?? null;
const newGuildScheduledEvent = guild.scheduledEvents._add(data);
/**
* Emitted whenever a guild scheduled event gets updated.
* @event Client#guildScheduledEventUpdate
* @param {?GuildScheduledEvent} oldGuildScheduledEvent The guild scheduled event object before the update
* @param {GuildScheduledEvent} newGuildScheduledEvent The guild scheduled event object after the update
*/
client.emit(Events.GuildScheduledEventUpdate, oldGuildScheduledEvent, newGuildScheduledEvent);
return { oldGuildScheduledEvent, newGuildScheduledEvent };
}
return {};
}
}
module.exports = GuildScheduledEventUpdateAction;

View file

@ -0,0 +1,32 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class GuildScheduledEventUserAddAction extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
const guildScheduledEvent = this.getScheduledEvent(data, guild);
const user = this.getUser(data);
if (guildScheduledEvent && user) {
/**
* Emitted whenever a user subscribes to a guild scheduled event
* @event Client#guildScheduledEventUserAdd
* @param {GuildScheduledEvent} guildScheduledEvent The guild scheduled event
* @param {User} user The user who subscribed
*/
client.emit(Events.GuildScheduledEventUserAdd, guildScheduledEvent, user);
return { guildScheduledEvent, user };
}
}
return {};
}
}
module.exports = GuildScheduledEventUserAddAction;

View file

@ -0,0 +1,32 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class GuildScheduledEventUserRemoveAction extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
const guildScheduledEvent = this.getScheduledEvent(data, guild);
const user = this.getUser(data);
if (guildScheduledEvent && user) {
/**
* Emitted whenever a user unsubscribes from a guild scheduled event
* @event Client#guildScheduledEventUserRemove
* @param {GuildScheduledEvent} guildScheduledEvent The guild scheduled event
* @param {User} user The user who unsubscribed
*/
client.emit(Events.GuildScheduledEventUserRemove, guildScheduledEvent, user);
return { guildScheduledEvent, user };
}
}
return {};
}
}
module.exports = GuildScheduledEventUserRemoveAction;

View file

@ -0,0 +1,20 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class GuildStickerCreateAction extends Action {
handle(guild, createdSticker) {
const already = guild.stickers.cache.has(createdSticker.id);
const sticker = guild.stickers._add(createdSticker);
/**
* Emitted whenever a custom sticker is created in a guild.
* @event Client#stickerCreate
* @param {Sticker} sticker The sticker that was created
*/
if (!already) this.client.emit(Events.GuildStickerCreate, sticker);
return { sticker };
}
}
module.exports = GuildStickerCreateAction;

View file

@ -0,0 +1,19 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class GuildStickerDeleteAction extends Action {
handle(sticker) {
sticker.guild.stickers.cache.delete(sticker.id);
/**
* Emitted whenever a custom sticker is deleted in a guild.
* @event Client#stickerDelete
* @param {Sticker} sticker The sticker that was deleted
*/
this.client.emit(Events.GuildStickerDelete, sticker);
return { sticker };
}
}
module.exports = GuildStickerDeleteAction;

View file

@ -0,0 +1,20 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class GuildStickerUpdateAction extends Action {
handle(current, data) {
const old = current._update(data);
/**
* Emitted whenever a custom sticker is updated in a guild.
* @event Client#stickerUpdate
* @param {Sticker} oldSticker The old sticker
* @param {Sticker} newSticker The new sticker
*/
this.client.emit(Events.GuildStickerUpdate, old, current);
return { sticker: current };
}
}
module.exports = GuildStickerUpdateAction;

View file

@ -0,0 +1,34 @@
'use strict';
const Action = require('./Action');
class GuildStickersUpdateAction extends Action {
handle(data) {
const guild = this.client.guilds.cache.get(data.guild_id);
if (!guild?.stickers) return;
const deletions = new Map(guild.stickers.cache);
for (const sticker of data.stickers) {
// Determine type of sticker event
const cachedSticker = guild.stickers.cache.get(sticker.id);
if (cachedSticker) {
deletions.delete(sticker.id);
if (!cachedSticker.equals(sticker)) {
// Sticker updated
this.client.actions.GuildStickerUpdate.handle(cachedSticker, sticker);
}
} else {
// Sticker added
this.client.actions.GuildStickerCreate.handle(guild, sticker);
}
}
for (const sticker of deletions.values()) {
// Sticker deleted
this.client.actions.GuildStickerDelete.handle(sticker);
}
}
}
module.exports = GuildStickersUpdateAction;

View file

@ -0,0 +1,33 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class GuildUpdateAction extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.cache.get(data.id);
if (guild) {
const old = guild._update(data);
/**
* Emitted whenever a guild is updated - e.g. name change.
* @event Client#guildUpdate
* @param {Guild} oldGuild The guild before the update
* @param {Guild} newGuild The guild after the update
*/
client.emit(Events.GuildUpdate, old, guild);
return {
old,
updated: guild,
};
}
return {
old: null,
updated: null,
};
}
}
module.exports = GuildUpdateAction;

View file

@ -0,0 +1,101 @@
'use strict';
const { InteractionType, ComponentType, ApplicationCommandType } = require('discord-api-types/v10');
const Action = require('./Action');
const AutocompleteInteraction = require('../../structures/AutocompleteInteraction');
const ButtonInteraction = require('../../structures/ButtonInteraction');
const ChannelSelectMenuInteraction = require('../../structures/ChannelSelectMenuInteraction');
const ChatInputCommandInteraction = require('../../structures/ChatInputCommandInteraction');
const MentionableSelectMenuInteraction = require('../../structures/MentionableSelectMenuInteraction');
const MessageContextMenuCommandInteraction = require('../../structures/MessageContextMenuCommandInteraction');
const ModalSubmitInteraction = require('../../structures/ModalSubmitInteraction');
const RoleSelectMenuInteraction = require('../../structures/RoleSelectMenuInteraction');
const StringSelectMenuInteraction = require('../../structures/StringSelectMenuInteraction');
const UserContextMenuCommandInteraction = require('../../structures/UserContextMenuCommandInteraction');
const UserSelectMenuInteraction = require('../../structures/UserSelectMenuInteraction');
const Events = require('../../util/Events');
class InteractionCreateAction extends Action {
handle(data) {
const client = this.client;
// Resolve and cache partial channels for Interaction#channel getter
const channel = this.getChannel(data);
// Do not emit this for interactions that cache messages that are non-text-based.
let InteractionClass;
switch (data.type) {
case InteractionType.ApplicationCommand:
switch (data.data.type) {
case ApplicationCommandType.ChatInput:
InteractionClass = ChatInputCommandInteraction;
break;
case ApplicationCommandType.User:
InteractionClass = UserContextMenuCommandInteraction;
break;
case ApplicationCommandType.Message:
if (channel && !channel.isTextBased()) return;
InteractionClass = MessageContextMenuCommandInteraction;
break;
default:
client.emit(
Events.Debug,
`[INTERACTION] Received application command interaction with unknown type: ${data.data.type}`,
);
return;
}
break;
case InteractionType.MessageComponent:
if (channel && !channel.isTextBased()) return;
switch (data.data.component_type) {
case ComponentType.Button:
InteractionClass = ButtonInteraction;
break;
case ComponentType.StringSelect:
InteractionClass = StringSelectMenuInteraction;
break;
case ComponentType.UserSelect:
InteractionClass = UserSelectMenuInteraction;
break;
case ComponentType.RoleSelect:
InteractionClass = RoleSelectMenuInteraction;
break;
case ComponentType.MentionableSelect:
InteractionClass = MentionableSelectMenuInteraction;
break;
case ComponentType.ChannelSelect:
InteractionClass = ChannelSelectMenuInteraction;
break;
default:
client.emit(
Events.Debug,
`[INTERACTION] Received component interaction with unknown type: ${data.data.component_type}`,
);
return;
}
break;
case InteractionType.ApplicationCommandAutocomplete:
InteractionClass = AutocompleteInteraction;
break;
case InteractionType.ModalSubmit:
InteractionClass = ModalSubmitInteraction;
break;
default:
client.emit(Events.Debug, `[INTERACTION] Received interaction with unknown type: ${data.type}`);
return;
}
const interaction = new InteractionClass(client, data);
/**
* Emitted when an interaction is created.
* @event Client#interactionCreate
* @param {BaseInteraction} interaction The interaction which was created
*/
client.emit(Events.InteractionCreate, interaction);
}
}
module.exports = InteractionCreateAction;

View file

@ -0,0 +1,28 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class InviteCreateAction extends Action {
handle(data) {
const client = this.client;
const channel = client.channels.cache.get(data.channel_id);
const guild = client.guilds.cache.get(data.guild_id);
if (!channel) return false;
const inviteData = Object.assign(data, { channel, guild });
const invite = guild.invites._add(inviteData);
/**
* Emitted when an invite is created.
* <info>This event requires either the {@link PermissionFlagsBits.ManageGuild} permission or the
* {@link PermissionFlagsBits.ManageChannels} permission for the channel.</info>
* @event Client#inviteCreate
* @param {Invite} invite The invite that was created
*/
client.emit(Events.InviteCreate, invite);
return { invite };
}
}
module.exports = InviteCreateAction;

View file

@ -0,0 +1,30 @@
'use strict';
const Action = require('./Action');
const Invite = require('../../structures/Invite');
const Events = require('../../util/Events');
class InviteDeleteAction extends Action {
handle(data) {
const client = this.client;
const channel = client.channels.cache.get(data.channel_id);
const guild = client.guilds.cache.get(data.guild_id);
if (!channel) return false;
const inviteData = Object.assign(data, { channel, guild });
const invite = new Invite(client, inviteData);
guild.invites.cache.delete(invite.code);
/**
* Emitted when an invite is deleted.
* <info>This event requires either the {@link PermissionFlagsBits.ManageGuild} permission or the
* {@link PermissionFlagsBits.ManageChannels} permission for the channel.</info>
* @event Client#inviteDelete
* @param {Invite} invite The invite that was deleted
*/
client.emit(Events.InviteDelete, invite);
return { invite };
}
}
module.exports = InviteDeleteAction;

View file

@ -0,0 +1,37 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class MessageCreateAction extends Action {
handle(data) {
const client = this.client;
const channel = this.getChannel(data);
if (channel) {
if (!channel.isTextBased()) return {};
if (channel.isThread()) {
channel.messageCount++;
channel.totalMessageSent++;
}
const existing = channel.messages.cache.get(data.id);
if (existing) return { message: existing };
const message = channel.messages._add(data);
channel.lastMessageId = data.id;
/**
* Emitted whenever a message is created.
* @event Client#messageCreate
* @param {Message} message The created message
*/
client.emit(Events.MessageCreate, message);
return { message };
}
return {};
}
}
module.exports = MessageCreateAction;

View file

@ -0,0 +1,32 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class MessageDeleteAction extends Action {
handle(data) {
const client = this.client;
const channel = this.getChannel(data);
let message;
if (channel) {
if (!channel.isTextBased()) return {};
if (channel.isThread()) channel.messageCount--;
message = this.getMessage(data, channel);
if (message) {
channel.messages.cache.delete(message.id);
/**
* Emitted whenever a message is deleted.
* @event Client#messageDelete
* @param {Message} message The deleted message
*/
client.emit(Events.MessageDelete, message);
}
}
return { message };
}
}
module.exports = MessageDeleteAction;

View file

@ -0,0 +1,47 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const Action = require('./Action');
const Events = require('../../util/Events');
class MessageDeleteBulkAction extends Action {
handle(data) {
const client = this.client;
const channel = client.channels.cache.get(data.channel_id);
if (channel) {
if (!channel.isTextBased()) return {};
if (channel.isThread()) channel.messageCount -= data.ids.length;
const ids = data.ids;
const messages = new Collection();
for (const id of ids) {
const message = this.getMessage(
{
id,
guild_id: data.guild_id,
},
channel,
false,
);
if (message) {
messages.set(message.id, message);
channel.messages.cache.delete(id);
}
}
/**
* Emitted whenever messages are deleted in bulk.
* @event Client#messageDeleteBulk
* @param {Collection<Snowflake, Message>} messages The deleted messages, mapped by their id
* @param {GuildTextBasedChannel} channel The channel that the messages were deleted in
*/
if (messages.size > 0) client.emit(Events.MessageBulkDelete, messages, channel);
return { messages };
}
return {};
}
}
module.exports = MessageDeleteBulkAction;

View file

@ -0,0 +1,55 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
const Partials = require('../../util/Partials');
/*
{ user_id: 'id',
message_id: 'id',
emoji: { name: '<27>', id: null },
channel_id: 'id',
// If originating from a guild
guild_id: 'id',
member: { ..., user: { ... } } }
*/
class MessageReactionAdd extends Action {
handle(data, fromStructure = false) {
if (!data.emoji) return false;
const user = this.getUserFromMember(data);
if (!user) return false;
// Verify channel
const channel = this.getChannel(data);
if (!channel?.isTextBased()) return false;
// Verify message
const message = this.getMessage(data, channel);
if (!message) return false;
// Verify reaction
const includePartial = this.client.options.partials.includes(Partials.Reaction);
if (message.partial && !includePartial) return false;
const reaction = message.reactions._add({
emoji: data.emoji,
count: message.partial ? null : 0,
me: user.id === this.client.user.id,
});
if (!reaction) return false;
reaction._add(user);
if (fromStructure) return { message, reaction, user };
/**
* Emitted whenever a reaction is added to a cached message.
* @event Client#messageReactionAdd
* @param {MessageReaction} messageReaction The reaction object
* @param {User} user The user that applied the guild or reaction emoji
*/
this.client.emit(Events.MessageReactionAdd, reaction, user);
return { message, reaction, user };
}
}
module.exports = MessageReactionAdd;

View file

@ -0,0 +1,45 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
/*
{ user_id: 'id',
message_id: 'id',
emoji: { name: '<27>', id: null },
channel_id: 'id',
guild_id: 'id' }
*/
class MessageReactionRemove extends Action {
handle(data) {
if (!data.emoji) return false;
const user = this.getUser(data);
if (!user) return false;
// Verify channel
const channel = this.getChannel(data);
if (!channel?.isTextBased()) return false;
// Verify message
const message = this.getMessage(data, channel);
if (!message) return false;
// Verify reaction
const reaction = this.getReaction(data, message, user);
if (!reaction) return false;
reaction._remove(user);
/**
* Emitted whenever a reaction is removed from a cached message.
* @event Client#messageReactionRemove
* @param {MessageReaction} messageReaction The reaction object
* @param {User} user The user whose emoji or reaction emoji was removed
*/
this.client.emit(Events.MessageReactionRemove, reaction, user);
return { message, reaction, user };
}
}
module.exports = MessageReactionRemove;

View file

@ -0,0 +1,33 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class MessageReactionRemoveAll extends Action {
handle(data) {
// Verify channel
const channel = this.getChannel(data);
if (!channel?.isTextBased()) return false;
// Verify message
const message = this.getMessage(data, channel);
if (!message) return false;
// Copy removed reactions to emit for the event.
const removed = message.reactions.cache.clone();
message.reactions.cache.clear();
this.client.emit(Events.MessageReactionRemoveAll, message, removed);
return { message };
}
}
/**
* Emitted whenever all reactions are removed from a cached message.
* @event Client#messageReactionRemoveAll
* @param {Message} message The message the reactions were removed from
* @param {Collection<string|Snowflake, MessageReaction>} reactions The cached message reactions that were removed.
*/
module.exports = MessageReactionRemoveAll;

View file

@ -0,0 +1,28 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class MessageReactionRemoveEmoji extends Action {
handle(data) {
const channel = this.getChannel(data);
if (!channel?.isTextBased()) return false;
const message = this.getMessage(data, channel);
if (!message) return false;
const reaction = this.getReaction(data, message);
if (!reaction) return false;
if (!message.partial) message.reactions.cache.delete(reaction.emoji.id ?? reaction.emoji.name);
/**
* Emitted when a bot removes an emoji reaction from a cached message.
* @event Client#messageReactionRemoveEmoji
* @param {MessageReaction} reaction The reaction that was removed
*/
this.client.emit(Events.MessageReactionRemoveEmoji, reaction);
return { reaction };
}
}
module.exports = MessageReactionRemoveEmoji;

View file

@ -0,0 +1,26 @@
'use strict';
const Action = require('./Action');
class MessageUpdateAction extends Action {
handle(data) {
const channel = this.getChannel(data);
if (channel) {
if (!channel.isTextBased()) return {};
const { id, channel_id, guild_id, author, timestamp, type } = data;
const message = this.getMessage({ id, channel_id, guild_id, author, timestamp, type }, channel);
if (message) {
const old = message._update(data);
return {
old,
updated: message,
};
}
}
return {};
}
}
module.exports = MessageUpdateAction;

View file

@ -0,0 +1,42 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
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) return;
if (data.user.username) {
if (!user._equals(data.user)) this.client.actions.UserUpdate.handle(data.user);
}
const guild = this.client.guilds.cache.get(data.guild_id);
if (!guild) return;
const oldPresence = guild.presences.cache.get(user.id)?._clone() ?? null;
let member = guild.members.cache.get(user.id);
if (!member && data.status !== 'offline') {
member = guild.members._add({
user,
deaf: false,
mute: false,
});
this.client.emit(Events.GuildMemberAvailable, member);
}
const newPresence = guild.presences._add(Object.assign(data, { guild }));
if (this.client.listenerCount(Events.PresenceUpdate) && !newPresence.equals(oldPresence)) {
/**
* Emitted whenever a guild member's presence (e.g. status, activity) is changed.
* @event Client#presenceUpdate
* @param {?Presence} oldPresence The presence before the update, if one at all
* @param {Presence} newPresence The presence after the update
*/
this.client.emit(Events.PresenceUpdate, oldPresence, newPresence);
}
}
}
module.exports = PresenceUpdateAction;

View file

@ -0,0 +1,28 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class StageInstanceCreateAction extends Action {
handle(data) {
const client = this.client;
const channel = this.getChannel(data);
if (channel) {
const stageInstance = channel.guild.stageInstances._add(data);
/**
* Emitted whenever a stage instance is created.
* @event Client#stageInstanceCreate
* @param {StageInstance} stageInstance The created stage instance
*/
client.emit(Events.StageInstanceCreate, stageInstance);
return { stageInstance };
}
return {};
}
}
module.exports = StageInstanceCreateAction;

View file

@ -0,0 +1,31 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class StageInstanceDeleteAction extends Action {
handle(data) {
const client = this.client;
const channel = this.getChannel(data);
if (channel) {
const stageInstance = channel.guild.stageInstances._add(data);
if (stageInstance) {
channel.guild.stageInstances.cache.delete(stageInstance.id);
/**
* Emitted whenever a stage instance is deleted.
* @event Client#stageInstanceDelete
* @param {StageInstance} stageInstance The deleted stage instance
*/
client.emit(Events.StageInstanceDelete, stageInstance);
return { stageInstance };
}
}
return {};
}
}
module.exports = StageInstanceDeleteAction;

View file

@ -0,0 +1,30 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class StageInstanceUpdateAction extends Action {
handle(data) {
const client = this.client;
const channel = this.getChannel(data);
if (channel) {
const oldStageInstance = channel.guild.stageInstances.cache.get(data.id)?._clone() ?? null;
const newStageInstance = channel.guild.stageInstances._add(data);
/**
* Emitted whenever a stage instance gets updated - e.g. change in topic or privacy level
* @event Client#stageInstanceUpdate
* @param {?StageInstance} oldStageInstance The stage instance before the update
* @param {StageInstance} newStageInstance The stage instance after the update
*/
client.emit(Events.StageInstanceUpdate, oldStageInstance, newStageInstance);
return { oldStageInstance, newStageInstance };
}
return {};
}
}
module.exports = StageInstanceUpdateAction;

View file

@ -0,0 +1,24 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class ThreadCreateAction extends Action {
handle(data) {
const client = this.client;
const existing = client.channels.cache.has(data.id);
const thread = client.channels._add(data);
if (!existing && thread) {
/**
* Emitted whenever a thread is created or when the client user is added to a thread.
* @event Client#threadCreate
* @param {ThreadChannel} thread The thread that was created
* @param {boolean} newlyCreated Whether the thread was newly created
*/
client.emit(Events.ThreadCreate, thread, data.newly_created ?? false);
}
return { thread };
}
}
module.exports = ThreadCreateAction;

View file

@ -0,0 +1,26 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class ThreadDeleteAction extends Action {
handle(data) {
const client = this.client;
const thread = client.channels.cache.get(data.id);
if (thread) {
client.channels._remove(thread.id);
/**
* Emitted whenever a thread is deleted.
* @event Client#threadDelete
* @param {ThreadChannel} thread The thread that was deleted
*/
client.emit(Events.ThreadDelete, thread);
}
return { thread };
}
}
module.exports = ThreadDeleteAction;

View file

@ -0,0 +1,60 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const Action = require('./Action');
const Events = require('../../util/Events');
class ThreadListSyncAction extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.cache.get(data.guild_id);
if (!guild) return {};
if (data.channel_ids) {
for (const id of data.channel_ids) {
const channel = client.channels.resolve(id);
if (channel) this.removeStale(channel);
}
} else {
for (const channel of guild.channels.cache.values()) {
this.removeStale(channel);
}
}
const syncedThreads = data.threads.reduce((coll, rawThread) => {
const thread = client.channels._add(rawThread);
return coll.set(thread.id, thread);
}, new Collection());
for (const rawMember of Object.values(data.members)) {
// Discord sends the thread id as id in this object
const thread = client.channels.cache.get(rawMember.id);
if (thread) {
thread.members._add(rawMember);
}
}
/**
* Emitted whenever the client user gains access to a text or news channel that contains threads
* @event Client#threadListSync
* @param {Collection<Snowflake, ThreadChannel>} threads The threads that were synced
* @param {Guild} guild The guild that the threads were synced in
*/
client.emit(Events.ThreadListSync, syncedThreads, guild);
return {
syncedThreads,
};
}
removeStale(channel) {
channel.threads?.cache.forEach(thread => {
if (!thread.archived) {
this.client.channels._remove(thread.id);
}
});
}
}
module.exports = ThreadListSyncAction;

View file

@ -0,0 +1,30 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class ThreadMemberUpdateAction extends Action {
handle(data) {
const client = this.client;
// Discord sends the thread id as id in this object
const thread = client.channels.cache.get(data.id);
if (thread) {
const member = thread.members.cache.get(data.user_id);
if (!member) {
const newMember = thread.members._add(data);
return { newMember };
}
const old = member._update(data);
/**
* Emitted whenever the client user's thread member is updated.
* @event Client#threadMemberUpdate
* @param {ThreadMember} oldMember The member before the update
* @param {ThreadMember} newMember The member after the update
*/
client.emit(Events.ThreadMemberUpdate, old, member);
}
return {};
}
}
module.exports = ThreadMemberUpdateAction;

View file

@ -0,0 +1,47 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const Action = require('./Action');
const Events = require('../../util/Events');
class ThreadMembersUpdateAction extends Action {
handle(data) {
const client = this.client;
const thread = client.channels.cache.get(data.id);
if (thread) {
thread.memberCount = data.member_count;
const addedMembers = new Collection();
const removedMembers = new Collection();
data.added_members?.reduce(
(_addedMembers, addedMember) => _addedMembers.set(addedMember.user_id, thread.members._add(addedMember)),
addedMembers,
);
data.removed_member_ids?.reduce((removedMembersIds, removedMembersId) => {
const threadMember = this.getThreadMember(removedMembersId, thread.members);
if (threadMember) removedMembersIds.set(threadMember.id, threadMember);
thread.members.cache.delete(removedMembersId);
return removedMembersIds;
}, removedMembers);
if (addedMembers.size === 0 && removedMembers.size === 0) {
// Uncached thread member(s) left.
return {};
}
/**
* Emitted whenever members are added or removed from a thread.
* <info>This event requires the {@link GatewayIntentBits.GuildMembers} privileged gateway intent.</info>
* @event Client#threadMembersUpdate
* @param {Collection<Snowflake, ThreadMember>} addedMembers The members that were added
* @param {Collection<Snowflake, ThreadMember>} removedMembers The members that were removed
* @param {ThreadChannel} thread The thread where members got updated
*/
client.emit(Events.ThreadMembersUpdate, addedMembers, removedMembers, thread);
}
return {};
}
}
module.exports = ThreadMembersUpdateAction;

View file

@ -0,0 +1,29 @@
'use strict';
const Action = require('./Action');
const Typing = require('../../structures/Typing');
const Events = require('../../util/Events');
class TypingStart extends Action {
handle(data) {
const channel = this.getChannel(data);
if (!channel) return;
if (!channel.isTextBased()) {
this.client.emit(Events.Warn, `Discord sent a typing packet to a ${channel.type} channel ${channel.id}`);
return;
}
const user = this.getUserFromMember(data);
if (user) {
/**
* Emitted whenever a user starts typing in a channel.
* @event Client#typingStart
* @param {Typing} typing The typing state
*/
this.client.emit(Events.TypingStart, new Typing(channel, user, data));
}
}
}
module.exports = TypingStart;

View file

@ -0,0 +1,36 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class UserUpdateAction extends Action {
handle(data) {
const client = this.client;
const newUser = data.id === client.user.id ? client.user : client.users.cache.get(data.id);
const oldUser = newUser._update(data);
if (!oldUser.equals(newUser)) {
/**
* Emitted whenever a user's details (e.g. username) are changed.
* Triggered by the Discord gateway events {@link Events.UserUpdate},
* {@link Events.GuildMemberUpdate}, and {@link Events.PresenceUpdate}.
* @event Client#userUpdate
* @param {User} oldUser The user before the update
* @param {User} newUser The user after the update
*/
client.emit(Events.UserUpdate, oldUser, newUser);
return {
old: oldUser,
updated: newUser,
};
}
return {
old: null,
updated: null,
};
}
}
module.exports = UserUpdateAction;

View file

@ -0,0 +1,43 @@
'use strict';
const Action = require('./Action');
const VoiceState = require('../../structures/VoiceState');
const Events = require('../../util/Events');
class VoiceStateUpdate extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
// Update the state
const oldState =
guild.voiceStates.cache.get(data.user_id)?._clone() ?? new VoiceState(guild, { user_id: data.user_id });
const newState = guild.voiceStates._add(data);
// Get the member
let member = guild.members.cache.get(data.user_id);
if (member && data.member) {
member._patch(data.member);
} else if (data.member?.user && data.member.joined_at) {
member = guild.members._add(data.member);
}
// Emit event
if (member?.user.id === client.user.id) {
client.emit('debug', `[VOICE] received voice state update: ${JSON.stringify(data)}`);
client.voice.onVoiceStateUpdate(data);
}
/**
* Emitted whenever a member changes voice state - e.g. joins/leaves a channel, mutes/unmutes.
* @event Client#voiceStateUpdate
* @param {VoiceState} oldState The voice state before the update
* @param {VoiceState} newState The voice state after the update
*/
client.emit(Events.VoiceStateUpdate, oldState, newState);
}
}
}
module.exports = VoiceStateUpdate;

View file

@ -0,0 +1,19 @@
'use strict';
const Action = require('./Action');
const Events = require('../../util/Events');
class WebhooksUpdate extends Action {
handle(data) {
const client = this.client;
const channel = client.channels.cache.get(data.channel_id);
/**
* Emitted whenever a channel has its webhooks changed.
* @event Client#webhookUpdate
* @param {TextChannel|NewsChannel|VoiceChannel|ForumChannel} channel The channel that had a webhook update
*/
if (channel) client.emit(Events.WebhooksUpdate, channel);
}
}
module.exports = WebhooksUpdate;

View file

@ -0,0 +1,44 @@
'use strict';
const Events = require('../../util/Events');
/**
* Manages voice connections for the client
*/
class ClientVoiceManager {
constructor(client) {
/**
* The client that instantiated this voice manager
* @type {Client}
* @readonly
* @name ClientVoiceManager#client
*/
Object.defineProperty(this, 'client', { value: client });
/**
* Maps guild ids to voice adapters created for use with @discordjs/voice.
* @type {Map<Snowflake, Object>}
*/
this.adapters = new Map();
client.on(Events.ShardDisconnect, (_, shardId) => {
for (const [guildId, adapter] of this.adapters.entries()) {
if (client.guilds.cache.get(guildId)?.shardId === shardId) {
adapter.destroy();
}
}
});
}
onVoiceServer(payload) {
this.adapters.get(payload.guild_id)?.onVoiceServerUpdate(payload);
}
onVoiceStateUpdate(payload) {
if (payload.guild_id && payload.session_id && payload.user_id === this.client.user?.id) {
this.adapters.get(payload.guild_id)?.onVoiceStateUpdate(payload);
}
}
}
module.exports = ClientVoiceManager;

View file

@ -0,0 +1,391 @@
'use strict';
const EventEmitter = require('node:events');
const { setImmediate } = require('node:timers');
const { setTimeout: sleep } = require('node:timers/promises');
const { Collection } = require('@discordjs/collection');
const { GatewayCloseCodes, GatewayDispatchEvents, Routes } = require('discord-api-types/v10');
const WebSocketShard = require('./WebSocketShard');
const PacketHandlers = require('./handlers');
const { DiscordjsError, ErrorCodes } = require('../../errors');
const Events = require('../../util/Events');
const Status = require('../../util/Status');
const WebSocketShardEvents = require('../../util/WebSocketShardEvents');
const BeforeReadyWhitelist = [
GatewayDispatchEvents.Ready,
GatewayDispatchEvents.Resumed,
GatewayDispatchEvents.GuildCreate,
GatewayDispatchEvents.GuildDelete,
GatewayDispatchEvents.GuildMembersChunk,
GatewayDispatchEvents.GuildMemberAdd,
GatewayDispatchEvents.GuildMemberRemove,
];
const unrecoverableErrorCodeMap = {
[GatewayCloseCodes.AuthenticationFailed]: ErrorCodes.TokenInvalid,
[GatewayCloseCodes.InvalidShard]: ErrorCodes.ShardingInvalid,
[GatewayCloseCodes.ShardingRequired]: ErrorCodes.ShardingRequired,
[GatewayCloseCodes.InvalidIntents]: ErrorCodes.InvalidIntents,
[GatewayCloseCodes.DisallowedIntents]: ErrorCodes.DisallowedIntents,
};
const UNRESUMABLE_CLOSE_CODES = [1000, GatewayCloseCodes.AlreadyAuthenticated, GatewayCloseCodes.InvalidSeq];
/**
* The WebSocket manager for this client.
* <info>This class forwards raw dispatch events,
* read more about it here {@link https://discord.com/developers/docs/topics/gateway}</info>
* @extends {EventEmitter}
*/
class WebSocketManager extends EventEmitter {
constructor(client) {
super();
/**
* The client that instantiated this WebSocketManager
* @type {Client}
* @readonly
* @name WebSocketManager#client
*/
Object.defineProperty(this, 'client', { value: client });
/**
* The gateway this manager uses
* @type {?string}
*/
this.gateway = null;
/**
* The amount of shards this manager handles
* @private
* @type {number}
*/
this.totalShards = this.client.options.shards.length;
/**
* A collection of all shards this manager handles
* @type {Collection<number, WebSocketShard>}
*/
this.shards = new Collection();
/**
* An array of shards to be connected or that need to reconnect
* @type {Set<WebSocketShard>}
* @private
* @name WebSocketManager#shardQueue
*/
Object.defineProperty(this, 'shardQueue', { value: new Set(), writable: true });
/**
* An array of queued events before this WebSocketManager became ready
* @type {Object[]}
* @private
* @name WebSocketManager#packetQueue
*/
Object.defineProperty(this, 'packetQueue', { value: [] });
/**
* The current status of this WebSocketManager
* @type {Status}
*/
this.status = Status.Idle;
/**
* If this manager was destroyed. It will prevent shards from reconnecting
* @type {boolean}
* @private
*/
this.destroyed = false;
/**
* If this manager is currently reconnecting one or multiple shards
* @type {boolean}
* @private
*/
this.reconnecting = false;
}
/**
* The average ping of all WebSocketShards
* @type {number}
* @readonly
*/
get ping() {
const sum = this.shards.reduce((a, b) => a + b.ping, 0);
return sum / this.shards.size;
}
/**
* Emits a debug message.
* @param {string} message The debug message
* @param {?WebSocketShard} [shard] The shard that emitted this message, if any
* @private
*/
debug(message, shard) {
this.client.emit(Events.Debug, `[WS => ${shard ? `Shard ${shard.id}` : 'Manager'}] ${message}`);
}
/**
* Connects this manager to the gateway.
* @private
*/
async connect() {
const invalidToken = new DiscordjsError(ErrorCodes.TokenInvalid);
const {
url: gatewayURL,
shards: recommendedShards,
session_start_limit: sessionStartLimit,
} = await this.client.rest.get(Routes.gatewayBot()).catch(error => {
throw error.status === 401 ? invalidToken : error;
});
const { total, remaining } = sessionStartLimit;
this.debug(`Fetched Gateway Information
URL: ${gatewayURL}
Recommended Shards: ${recommendedShards}`);
this.debug(`Session Limit Information
Total: ${total}
Remaining: ${remaining}`);
this.gateway = `${gatewayURL}/`;
let { shards } = this.client.options;
if (shards === 'auto') {
this.debug(`Using the recommended shard count provided by Discord: ${recommendedShards}`);
this.totalShards = this.client.options.shardCount = recommendedShards;
shards = this.client.options.shards = Array.from({ length: recommendedShards }, (_, i) => i);
}
this.totalShards = shards.length;
this.debug(`Spawning shards: ${shards.join(', ')}`);
this.shardQueue = new Set(shards.map(id => new WebSocketShard(this, id)));
return this.createShards();
}
/**
* Handles the creation of a shard.
* @returns {Promise<boolean>}
* @private
*/
async createShards() {
// If we don't have any shards to handle, return
if (!this.shardQueue.size) return false;
const [shard] = this.shardQueue;
this.shardQueue.delete(shard);
if (!shard.eventsAttached) {
shard.on(WebSocketShardEvents.AllReady, unavailableGuilds => {
/**
* Emitted when a shard turns ready.
* @event Client#shardReady
* @param {number} id The shard id that turned ready
* @param {?Set<Snowflake>} unavailableGuilds Set of unavailable guild ids, if any
*/
this.client.emit(Events.ShardReady, shard.id, unavailableGuilds);
if (!this.shardQueue.size) this.reconnecting = false;
this.checkShardsReady();
});
shard.on(WebSocketShardEvents.Close, event => {
if (event.code === 1_000 ? this.destroyed : event.code in unrecoverableErrorCodeMap) {
/**
* Emitted when a shard's WebSocket disconnects and will no longer reconnect.
* @event Client#shardDisconnect
* @param {CloseEvent} event The WebSocket close event
* @param {number} id The shard id that disconnected
*/
this.client.emit(Events.ShardDisconnect, event, shard.id);
this.debug(GatewayCloseCodes[event.code], shard);
return;
}
if (UNRESUMABLE_CLOSE_CODES.includes(event.code)) {
// These event codes cannot be resumed
shard.sessionId = null;
}
/**
* Emitted when a shard is attempting to reconnect or re-identify.
* @event Client#shardReconnecting
* @param {number} id The shard id that is attempting to reconnect
*/
this.client.emit(Events.ShardReconnecting, shard.id);
this.shardQueue.add(shard);
if (shard.sessionId) this.debug(`Session id is present, attempting an immediate reconnect...`, shard);
this.reconnect();
});
shard.on(WebSocketShardEvents.InvalidSession, () => {
this.client.emit(Events.ShardReconnecting, shard.id);
});
shard.on(WebSocketShardEvents.Destroyed, () => {
this.debug('Shard was destroyed but no WebSocket connection was present! Reconnecting...', shard);
this.client.emit(Events.ShardReconnecting, shard.id);
this.shardQueue.add(shard);
this.reconnect();
});
shard.eventsAttached = true;
}
this.shards.set(shard.id, shard);
try {
await shard.connect();
} catch (error) {
if (error?.code && error.code in unrecoverableErrorCodeMap) {
throw new DiscordjsError(unrecoverableErrorCodeMap[error.code]);
// Undefined if session is invalid, error event for regular closes
} else if (!error || error.code) {
this.debug('Failed to connect to the gateway, requeueing...', shard);
this.shardQueue.add(shard);
} else {
throw error;
}
}
// If we have more shards, add a 5s delay
if (this.shardQueue.size) {
this.debug(`Shard Queue Size: ${this.shardQueue.size}; continuing in 5 seconds...`);
await sleep(5_000);
return this.createShards();
}
return true;
}
/**
* Handles reconnects for this manager.
* @private
* @returns {Promise<boolean>}
*/
async reconnect() {
if (this.reconnecting || this.status !== Status.Ready) return false;
this.reconnecting = true;
try {
await this.createShards();
} catch (error) {
this.debug(`Couldn't reconnect or fetch information about the gateway. ${error}`);
if (error.httpStatus !== 401) {
this.debug(`Possible network error occurred. Retrying in 5s...`);
await sleep(5_000);
this.reconnecting = false;
return this.reconnect();
}
// If we get an error at this point, it means we cannot reconnect anymore
if (this.client.listenerCount(Events.Invalidated)) {
/**
* Emitted when the client's session becomes invalidated.
* You are expected to handle closing the process gracefully and preventing a boot loop
* if you are listening to this event.
* @event Client#invalidated
*/
this.client.emit(Events.Invalidated);
// Destroy just the shards. This means you have to handle the cleanup yourself
this.destroy();
} else {
this.client.destroy();
}
} finally {
this.reconnecting = false;
}
return true;
}
/**
* Broadcasts a packet to every shard this manager handles.
* @param {Object} packet The packet to send
* @private
*/
broadcast(packet) {
for (const shard of this.shards.values()) shard.send(packet);
}
/**
* Destroys this manager and all its shards.
* @private
*/
destroy() {
if (this.destroyed) return;
// TODO: Make a util for getting a stack
this.debug(`Manager was destroyed. Called by:\n${new Error().stack}`);
this.destroyed = true;
this.shardQueue.clear();
for (const shard of this.shards.values()) shard.destroy({ closeCode: 1_000, reset: true, emit: false, log: false });
}
/**
* Processes a packet and queues it if this WebSocketManager is not ready.
* @param {Object} [packet] The packet to be handled
* @param {WebSocketShard} [shard] The shard that will handle this packet
* @returns {boolean}
* @private
*/
handlePacket(packet, shard) {
if (packet && this.status !== Status.Ready) {
if (!BeforeReadyWhitelist.includes(packet.t)) {
this.packetQueue.push({ packet, shard });
return false;
}
}
if (this.packetQueue.length) {
const item = this.packetQueue.shift();
setImmediate(() => {
this.handlePacket(item.packet, item.shard);
}).unref();
}
if (packet && PacketHandlers[packet.t]) {
PacketHandlers[packet.t](this.client, packet, shard);
}
return true;
}
/**
* Checks whether the client is ready to be marked as ready.
* @private
*/
checkShardsReady() {
if (this.status === Status.Ready) return;
if (this.shards.size !== this.totalShards || this.shards.some(s => s.status !== Status.Ready)) {
return;
}
this.triggerClientReady();
}
/**
* Causes the client to be marked as ready and emits the ready event.
* @private
*/
triggerClientReady() {
this.status = Status.Ready;
this.client.readyTimestamp = Date.now();
/**
* Emitted when the client becomes ready to start working.
* @event Client#ready
* @param {Client} client The client
*/
this.client.emit(Events.ClientReady, this.client);
this.handlePacket();
}
}
module.exports = WebSocketManager;

View file

@ -0,0 +1,898 @@
'use strict';
const EventEmitter = require('node:events');
const { setTimeout, setInterval, clearTimeout, clearInterval } = require('node:timers');
const { GatewayDispatchEvents, GatewayIntentBits, GatewayOpcodes } = require('discord-api-types/v10');
const WebSocket = require('../../WebSocket');
const Events = require('../../util/Events');
const Status = require('../../util/Status');
const WebSocketShardEvents = require('../../util/WebSocketShardEvents');
const STATUS_KEYS = Object.keys(Status);
const CONNECTION_STATE = Object.keys(WebSocket.WebSocket);
let zlib;
try {
zlib = require('zlib-sync');
} catch {} // eslint-disable-line no-empty
/**
* Represents a Shard's WebSocket connection
* @extends {EventEmitter}
*/
class WebSocketShard extends EventEmitter {
constructor(manager, id) {
super();
/**
* The WebSocketManager of the shard
* @type {WebSocketManager}
*/
this.manager = manager;
/**
* The shard's id
* @type {number}
*/
this.id = id;
/**
* The current status of the shard
* @type {Status}
*/
this.status = Status.Idle;
/**
* The current sequence of the shard
* @type {number}
* @private
*/
this.sequence = -1;
/**
* The sequence of the shard after close
* @type {number}
* @private
*/
this.closeSequence = 0;
/**
* The current session id of the shard
* @type {?string}
* @private
*/
this.sessionId = null;
/**
* The resume url for this shard
* @type {?string}
* @private
*/
this.resumeURL = null;
/**
* The previous heartbeat ping of the shard
* @type {number}
*/
this.ping = -1;
/**
* The last time a ping was sent (a timestamp)
* @type {number}
*/
this.lastPingTimestamp = -1;
/**
* If we received a heartbeat ack back. Used to identify zombie connections
* @type {boolean}
* @private
*/
this.lastHeartbeatAcked = true;
/**
* Used to prevent calling {@link WebSocketShard#event:close} twice while closing or terminating the WebSocket.
* @type {boolean}
* @private
*/
this.closeEmitted = false;
/**
* Contains the rate limit queue and metadata
* @name WebSocketShard#ratelimit
* @type {Object}
* @private
*/
Object.defineProperty(this, 'ratelimit', {
value: {
queue: [],
total: 120,
remaining: 120,
time: 60e3,
timer: null,
},
});
/**
* The WebSocket connection for the current shard
* @name WebSocketShard#connection
* @type {?WebSocket}
* @private
*/
Object.defineProperty(this, 'connection', { value: null, writable: true });
/**
* @external Inflate
* @see {@link https://www.npmjs.com/package/zlib-sync}
*/
/**
* The compression to use
* @name WebSocketShard#inflate
* @type {?Inflate}
* @private
*/
Object.defineProperty(this, 'inflate', { value: null, writable: true });
/**
* The HELLO timeout
* @name WebSocketShard#helloTimeout
* @type {?NodeJS.Timeout}
* @private
*/
Object.defineProperty(this, 'helloTimeout', { value: null, writable: true });
/**
* The WebSocket timeout.
* @name WebSocketShard#wsCloseTimeout
* @type {?NodeJS.Timeout}
* @private
*/
Object.defineProperty(this, 'wsCloseTimeout', { value: null, writable: true });
/**
* If the manager attached its event handlers on the shard
* @name WebSocketShard#eventsAttached
* @type {boolean}
* @private
*/
Object.defineProperty(this, 'eventsAttached', { value: false, writable: true });
/**
* A set of guild ids this shard expects to receive
* @name WebSocketShard#expectedGuilds
* @type {?Set<string>}
* @private
*/
Object.defineProperty(this, 'expectedGuilds', { value: null, writable: true });
/**
* The ready timeout
* @name WebSocketShard#readyTimeout
* @type {?NodeJS.Timeout}
* @private
*/
Object.defineProperty(this, 'readyTimeout', { value: null, writable: true });
/**
* Time when the WebSocket connection was opened
* @name WebSocketShard#connectedAt
* @type {number}
* @private
*/
Object.defineProperty(this, 'connectedAt', { value: 0, writable: true });
}
/**
* Emits a debug event.
* @param {string} message The debug message
* @private
*/
debug(message) {
this.manager.debug(message, this);
}
/**
* Connects the shard to the gateway.
* @private
* @returns {Promise<void>} A promise that will resolve if the shard turns ready successfully,
* or reject if we couldn't connect
*/
connect() {
const { client } = this.manager;
if (this.connection?.readyState === WebSocket.OPEN && this.status === Status.Ready) {
return Promise.resolve();
}
const gateway = this.resumeURL ?? this.manager.gateway;
return new Promise((resolve, reject) => {
const cleanup = () => {
this.removeListener(WebSocketShardEvents.Close, onClose);
this.removeListener(WebSocketShardEvents.Ready, onReady);
this.removeListener(WebSocketShardEvents.Resumed, onResumed);
this.removeListener(WebSocketShardEvents.InvalidSession, onInvalidOrDestroyed);
this.removeListener(WebSocketShardEvents.Destroyed, onInvalidOrDestroyed);
};
const onReady = () => {
cleanup();
resolve();
};
const onResumed = () => {
cleanup();
resolve();
};
const onClose = event => {
cleanup();
reject(event);
};
const onInvalidOrDestroyed = () => {
cleanup();
// eslint-disable-next-line prefer-promise-reject-errors
reject();
};
this.once(WebSocketShardEvents.Ready, onReady);
this.once(WebSocketShardEvents.Resumed, onResumed);
this.once(WebSocketShardEvents.Close, onClose);
this.once(WebSocketShardEvents.InvalidSession, onInvalidOrDestroyed);
this.once(WebSocketShardEvents.Destroyed, onInvalidOrDestroyed);
if (this.connection?.readyState === WebSocket.OPEN) {
this.debug('An open connection was found, attempting an immediate identify.');
this.identify();
return;
}
if (this.connection) {
this.debug(`A connection object was found. Cleaning up before continuing.
State: ${CONNECTION_STATE[this.connection.readyState]}`);
this.destroy({ emit: false });
}
const wsQuery = { v: client.options.ws.version };
if (zlib) {
this.inflate = new zlib.Inflate({
chunkSize: 65535,
flush: zlib.Z_SYNC_FLUSH,
to: WebSocket.encoding === 'json' ? 'string' : '',
});
wsQuery.compress = 'zlib-stream';
}
this.debug(
`[CONNECT]
Gateway : ${gateway}
Version : ${client.options.ws.version}
Encoding : ${WebSocket.encoding}
Compression: ${zlib ? 'zlib-stream' : 'none'}`,
);
this.status = this.status === Status.Disconnected ? Status.Reconnecting : Status.Connecting;
this.setHelloTimeout();
this.connectedAt = Date.now();
// Adding a handshake timeout to just make sure no zombie connection appears.
const ws = (this.connection = WebSocket.create(gateway, wsQuery, { handshakeTimeout: 30_000 }));
ws.onopen = this.onOpen.bind(this);
ws.onmessage = this.onMessage.bind(this);
ws.onerror = this.onError.bind(this);
ws.onclose = this.onClose.bind(this);
});
}
/**
* Called whenever a connection is opened to the gateway.
* @private
*/
onOpen() {
this.debug(`[CONNECTED] Took ${Date.now() - this.connectedAt}ms`);
this.status = Status.Nearly;
}
/**
* Called whenever a message is received.
* @param {MessageEvent} event Event received
* @private
*/
onMessage({ data }) {
let raw;
if (data instanceof ArrayBuffer) data = new Uint8Array(data);
if (zlib) {
const l = data.length;
const flush =
l >= 4 && data[l - 4] === 0x00 && data[l - 3] === 0x00 && data[l - 2] === 0xff && data[l - 1] === 0xff;
this.inflate.push(data, flush && zlib.Z_SYNC_FLUSH);
if (!flush) return;
raw = this.inflate.result;
} else {
raw = data;
}
let packet;
try {
packet = WebSocket.unpack(raw);
} catch (err) {
this.manager.client.emit(Events.ShardError, err, this.id);
return;
}
this.manager.client.emit(Events.Raw, packet, this.id);
if (packet.op === GatewayOpcodes.Dispatch) this.manager.emit(packet.t, packet.d, this.id);
this.onPacket(packet);
}
/**
* Called whenever an error occurs with the WebSocket.
* @param {ErrorEvent} event The error that occurred
* @private
*/
onError(event) {
const error = event?.error ?? event;
if (!error) return;
/**
* Emitted whenever a shard's WebSocket encounters a connection error.
* @event Client#shardError
* @param {Error} error The encountered error
* @param {number} shardId The shard that encountered this error
*/
this.manager.client.emit(Events.ShardError, error, this.id);
}
/**
* @external CloseEvent
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent}
*/
/**
* @external ErrorEvent
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent}
*/
/**
* @external MessageEvent
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent}
*/
/**
* Called whenever a connection to the gateway is closed.
* @param {CloseEvent} event Close event that was received
* @private
*/
onClose(event) {
this.closeEmitted = true;
if (this.sequence !== -1) this.closeSequence = this.sequence;
this.sequence = -1;
this.setHeartbeatTimer(-1);
this.setHelloTimeout(-1);
// Clearing the WebSocket close timeout as close was emitted.
this.setWsCloseTimeout(-1);
// If we still have a connection object, clean up its listeners
if (this.connection) this._cleanupConnection();
this.status = Status.Disconnected;
this.emitClose(event);
}
/**
* This method is responsible to emit close event for this shard.
* This method helps the shard reconnect.
* @param {CloseEvent} [event] Close event that was received
*/
emitClose(
event = {
code: 1011,
reason: 'INTERNAL_ERROR',
wasClean: false,
},
) {
this.debug(`[CLOSE]
Event Code: ${event.code}
Clean : ${event.wasClean}
Reason : ${event.reason ?? 'No reason received'}`);
/**
* Emitted when a shard's WebSocket closes.
* @private
* @event WebSocketShard#close
* @param {CloseEvent} event The received event
*/
this.emit(WebSocketShardEvents.Close, event);
}
/**
* Called whenever a packet is received.
* @param {Object} packet The received packet
* @private
*/
onPacket(packet) {
if (!packet) {
this.debug(`Received broken packet: '${packet}'.`);
return;
}
switch (packet.t) {
case GatewayDispatchEvents.Ready:
/**
* Emitted when the shard receives the READY payload and is now waiting for guilds
* @event WebSocketShard#ready
*/
this.emit(WebSocketShardEvents.Ready);
this.sessionId = packet.d.session_id;
this.resumeURL = packet.d.resume_gateway_url;
this.expectedGuilds = new Set(packet.d.guilds.map(d => d.id));
this.status = Status.WaitingForGuilds;
this.debug(`[READY] Session ${this.sessionId} | Resume url ${this.resumeURL}.`);
this.lastHeartbeatAcked = true;
this.sendHeartbeat('ReadyHeartbeat');
break;
case GatewayDispatchEvents.Resumed: {
/**
* Emitted when the shard resumes successfully
* @event WebSocketShard#resumed
*/
this.emit(WebSocketShardEvents.Resumed);
this.status = Status.Ready;
const replayed = packet.s - this.closeSequence;
this.debug(`[RESUMED] Session ${this.sessionId} | Replayed ${replayed} events.`);
this.lastHeartbeatAcked = true;
this.sendHeartbeat('ResumeHeartbeat');
break;
}
}
if (packet.s > this.sequence) this.sequence = packet.s;
switch (packet.op) {
case GatewayOpcodes.Hello:
this.setHelloTimeout(-1);
this.setHeartbeatTimer(packet.d.heartbeat_interval);
this.identify();
break;
case GatewayOpcodes.Reconnect:
this.debug('[RECONNECT] Discord asked us to reconnect');
this.destroy({ closeCode: 4_000 });
break;
case GatewayOpcodes.InvalidSession:
this.debug(`[INVALID SESSION] Resumable: ${packet.d}.`);
// If we can resume the session, do so immediately
if (packet.d) {
this.identifyResume();
return;
}
// Reset the sequence
this.sequence = -1;
// Reset the session id as it's invalid
this.sessionId = null;
// Set the status to reconnecting
this.status = Status.Reconnecting;
// Finally, emit the INVALID_SESSION event
/**
* Emitted when the session has been invalidated.
* @event WebSocketShard#invalidSession
*/
this.emit(WebSocketShardEvents.InvalidSession);
break;
case GatewayOpcodes.HeartbeatAck:
this.ackHeartbeat();
break;
case GatewayOpcodes.Heartbeat:
this.sendHeartbeat('HeartbeatRequest', true);
break;
default:
this.manager.handlePacket(packet, this);
if (this.status === Status.WaitingForGuilds && packet.t === GatewayDispatchEvents.GuildCreate) {
this.expectedGuilds.delete(packet.d.id);
this.checkReady();
}
}
}
/**
* Checks if the shard can be marked as ready
* @private
*/
checkReady() {
// Step 0. Clear the ready timeout, if it exists
if (this.readyTimeout) {
clearTimeout(this.readyTimeout);
this.readyTimeout = null;
}
// Step 1. If we don't have any other guilds pending, we are ready
if (!this.expectedGuilds.size) {
this.debug('Shard received all its guilds. Marking as fully ready.');
this.status = Status.Ready;
/**
* Emitted when the shard is fully ready.
* This event is emitted if:
* * all guilds were received by this shard
* * the ready timeout expired, and some guilds are unavailable
* @event WebSocketShard#allReady
* @param {?Set<string>} unavailableGuilds Set of unavailable guilds, if any
*/
this.emit(WebSocketShardEvents.AllReady);
return;
}
const hasGuildsIntent = this.manager.client.options.intents.has(GatewayIntentBits.Guilds);
// Step 2. Create a timeout that will mark the shard as ready if there are still unavailable guilds
// * The timeout is 15 seconds by default
// * This can be optionally changed in the client options via the `waitGuildTimeout` option
// * a timeout time of zero will skip this timeout, which potentially could cause the Client to miss guilds.
const { waitGuildTimeout } = this.manager.client.options;
this.readyTimeout = setTimeout(
() => {
this.debug(
`Shard ${hasGuildsIntent ? 'did' : 'will'} not receive any more guild packets` +
`${hasGuildsIntent ? ` in ${waitGuildTimeout} ms` : ''}.\nUnavailable guild count: ${
this.expectedGuilds.size
}`,
);
this.readyTimeout = null;
this.status = Status.Ready;
this.emit(WebSocketShardEvents.AllReady, this.expectedGuilds);
},
hasGuildsIntent ? waitGuildTimeout : 0,
).unref();
}
/**
* Sets the HELLO packet timeout.
* @param {number} [time] If set to -1, it will clear the hello timeout
* @private
*/
setHelloTimeout(time) {
if (time === -1) {
if (this.helloTimeout) {
this.debug('Clearing the HELLO timeout.');
clearTimeout(this.helloTimeout);
this.helloTimeout = null;
}
return;
}
this.debug('Setting a HELLO timeout for 20s.');
this.helloTimeout = setTimeout(() => {
this.debug('Did not receive HELLO in time. Destroying and connecting again.');
this.destroy({ reset: true, closeCode: 4009 });
}, 20_000).unref();
}
/**
* Sets the WebSocket Close timeout.
* This method is responsible for detecting any zombie connections if the WebSocket fails to close properly.
* @param {number} [time] If set to -1, it will clear the timeout
* @private
*/
setWsCloseTimeout(time) {
if (this.wsCloseTimeout) {
this.debug('[WebSocket] Clearing the close timeout.');
clearTimeout(this.wsCloseTimeout);
}
if (time === -1) {
this.wsCloseTimeout = null;
return;
}
this.wsCloseTimeout = setTimeout(() => {
this.setWsCloseTimeout(-1);
this.debug(`[WebSocket] Close Emitted: ${this.closeEmitted}`);
// Check if close event was emitted.
if (this.closeEmitted) {
this.debug(
`[WebSocket] was closed. | WS State: ${
CONNECTION_STATE[this.connection?.readyState ?? WebSocket.CLOSED]
} | Close Emitted: ${this.closeEmitted}`,
);
// Setting the variable false to check for zombie connections.
this.closeEmitted = false;
return;
}
this.debug(
`[WebSocket] did not close properly, assuming a zombie connection.\nEmitting close and reconnecting again.`,
);
this.emitClose();
// Setting the variable false to check for zombie connections.
this.closeEmitted = false;
}, time).unref();
}
/**
* Sets the heartbeat timer for this shard.
* @param {number} time If -1, clears the interval, any other number sets an interval
* @private
*/
setHeartbeatTimer(time) {
if (time === -1) {
if (this.heartbeatInterval) {
this.debug('Clearing the heartbeat interval.');
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
return;
}
this.debug(`Setting a heartbeat interval for ${time}ms.`);
// Sanity checks
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
this.heartbeatInterval = setInterval(() => this.sendHeartbeat(), time).unref();
}
/**
* Sends a heartbeat to the WebSocket.
* If this shard didn't receive a heartbeat last time, it will destroy it and reconnect
* @param {string} [tag='HeartbeatTimer'] What caused this heartbeat to be sent
* @param {boolean} [ignoreHeartbeatAck] If we should send the heartbeat forcefully.
* @private
*/
sendHeartbeat(
tag = 'HeartbeatTimer',
ignoreHeartbeatAck = [Status.WaitingForGuilds, Status.Identifying, Status.Resuming].includes(this.status),
) {
if (ignoreHeartbeatAck && !this.lastHeartbeatAcked) {
this.debug(`[${tag}] Didn't process heartbeat ack yet but we are still connected. Sending one now.`);
} else if (!this.lastHeartbeatAcked) {
this.debug(
`[${tag}] Didn't receive a heartbeat ack last time, assuming zombie connection. Destroying and reconnecting.
Status : ${STATUS_KEYS[this.status]}
Sequence : ${this.sequence}
Connection State: ${this.connection ? CONNECTION_STATE[this.connection.readyState] : 'No Connection??'}`,
);
this.destroy({ reset: true, closeCode: 4009 });
return;
}
this.debug(`[${tag}] Sending a heartbeat.`);
this.lastHeartbeatAcked = false;
this.lastPingTimestamp = Date.now();
this.send({ op: GatewayOpcodes.Heartbeat, d: this.sequence }, true);
}
/**
* Acknowledges a heartbeat.
* @private
*/
ackHeartbeat() {
this.lastHeartbeatAcked = true;
const latency = Date.now() - this.lastPingTimestamp;
this.debug(`Heartbeat acknowledged, latency of ${latency}ms.`);
this.ping = latency;
}
/**
* Identifies the client on the connection.
* @private
* @returns {void}
*/
identify() {
return this.sessionId ? this.identifyResume() : this.identifyNew();
}
/**
* Identifies as a new connection on the gateway.
* @private
*/
identifyNew() {
const { client } = this.manager;
if (!client.token) {
this.debug('[IDENTIFY] No token available to identify a new session.');
return;
}
this.status = Status.Identifying;
// Clone the identify payload and assign the token and shard info
const d = {
...client.options.ws,
intents: client.options.intents.bitfield,
token: client.token,
shard: [this.id, Number(client.options.shardCount)],
};
this.debug(`[IDENTIFY] Shard ${this.id}/${client.options.shardCount} with intents: ${d.intents}`);
this.send({ op: GatewayOpcodes.Identify, d }, true);
}
/**
* Resumes a session on the gateway.
* @private
*/
identifyResume() {
if (!this.sessionId) {
this.debug('[RESUME] No session id was present; identifying as a new session.');
this.identifyNew();
return;
}
this.status = Status.Resuming;
this.debug(`[RESUME] Session ${this.sessionId}, sequence ${this.closeSequence}`);
const d = {
token: this.manager.client.token,
session_id: this.sessionId,
seq: this.closeSequence,
};
this.send({ op: GatewayOpcodes.Resume, d }, true);
}
/**
* Adds a packet to the queue to be sent to the gateway.
* <warn>If you use this method, make sure you understand that you need to provide
* a full [Payload](https://discord.com/developers/docs/topics/gateway#commands-and-events-gateway-commands).
* Do not use this method if you don't know what you're doing.</warn>
* @param {Object} data The full packet to send
* @param {boolean} [important=false] If this packet should be added first in queue
*/
send(data, important = false) {
this.ratelimit.queue[important ? 'unshift' : 'push'](data);
this.processQueue();
}
/**
* Sends data, bypassing the queue.
* @param {Object} data Packet to send
* @returns {void}
* @private
*/
_send(data) {
if (this.connection?.readyState !== WebSocket.OPEN) {
this.debug(
`Tried to send packet '${JSON.stringify(data).replaceAll(
this.manager.client.token,
this.manager.client._censoredToken,
)}' but no WebSocket is available!`,
);
this.destroy({ closeCode: 4_000 });
return;
}
this.connection.send(WebSocket.pack(data), err => {
if (err) this.manager.client.emit(Events.ShardError, err, this.id);
});
}
/**
* Processes the current WebSocket queue.
* @returns {void}
* @private
*/
processQueue() {
if (this.ratelimit.remaining === 0) return;
if (this.ratelimit.queue.length === 0) return;
if (this.ratelimit.remaining === this.ratelimit.total) {
this.ratelimit.timer = setTimeout(() => {
this.ratelimit.remaining = this.ratelimit.total;
this.processQueue();
}, this.ratelimit.time).unref();
}
while (this.ratelimit.remaining > 0) {
const item = this.ratelimit.queue.shift();
if (!item) return;
this._send(item);
this.ratelimit.remaining--;
}
}
/**
* Destroys this shard and closes its WebSocket connection.
* @param {Object} [options={ closeCode: 1000, reset: false, emit: true, log: true }] Options for destroying the shard
* @private
*/
destroy({ closeCode = 1_000, reset = false, emit = true, log = true } = {}) {
if (log) {
this.debug(`[DESTROY]
Close Code : ${closeCode}
Reset : ${reset}
Emit DESTROYED: ${emit}`);
}
// Step 0: Remove all timers
this.setHeartbeatTimer(-1);
this.setHelloTimeout(-1);
this.debug(
`[WebSocket] Destroy: Attempting to close the WebSocket. | WS State: ${
CONNECTION_STATE[this.connection?.readyState ?? WebSocket.CLOSED]
}`,
);
// Step 1: Close the WebSocket connection, if any, otherwise, emit DESTROYED
if (this.connection) {
// If the connection is currently opened, we will (hopefully) receive close
if (this.connection.readyState === WebSocket.OPEN) {
this.connection.close(closeCode);
this.debug(`[WebSocket] Close: Tried closing. | WS State: ${CONNECTION_STATE[this.connection.readyState]}`);
} else {
// Connection is not OPEN
this.debug(`WS State: ${CONNECTION_STATE[this.connection.readyState]}`);
// Remove listeners from the connection
this._cleanupConnection();
// Attempt to close the connection just in case
try {
this.connection.close(closeCode);
} catch (err) {
this.debug(
`[WebSocket] Close: Something went wrong while closing the WebSocket: ${
err.message || err
}. Forcefully terminating the connection | WS State: ${CONNECTION_STATE[this.connection.readyState]}`,
);
this.connection.terminate();
}
// Emit the destroyed event if needed
if (emit) this._emitDestroyed();
}
} else if (emit) {
// We requested a destroy, but we had no connection. Emit destroyed
this._emitDestroyed();
}
if (this.connection?.readyState === WebSocket.CLOSING || this.connection?.readyState === WebSocket.CLOSED) {
this.closeEmitted = false;
this.debug(
`[WebSocket] Adding a WebSocket close timeout to ensure a correct WS reconnect.
Timeout: ${this.manager.client.options.closeTimeout}ms`,
);
this.setWsCloseTimeout(this.manager.client.options.closeTimeout);
}
// Step 2: Null the connection object
this.connection = null;
// Step 3: Set the shard status to disconnected
this.status = Status.Disconnected;
// Step 4: Cache the old sequence (use to attempt a resume)
if (this.sequence !== -1) this.closeSequence = this.sequence;
// Step 5: Reset the sequence, resume url and session id if requested
if (reset) {
this.sequence = -1;
this.sessionId = null;
this.resumeURL = null;
}
// Step 6: reset the rate limit data
this.ratelimit.remaining = this.ratelimit.total;
this.ratelimit.queue.length = 0;
if (this.ratelimit.timer) {
clearTimeout(this.ratelimit.timer);
this.ratelimit.timer = null;
}
}
/**
* Cleans up the WebSocket connection listeners.
* @private
*/
_cleanupConnection() {
this.connection.onopen = this.connection.onclose = this.connection.onmessage = null;
this.connection.onerror = () => null;
}
/**
* Emits the DESTROYED event on the shard
* @private
*/
_emitDestroyed() {
/**
* Emitted when a shard is destroyed, but no WebSocket connection was present.
* @private
* @event WebSocketShard#destroyed
*/
this.emit(WebSocketShardEvents.Destroyed);
}
}
module.exports = WebSocketShard;

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.ApplicationCommandPermissionsUpdate.handle(packet.d);
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.AutoModerationActionExecution.handle(packet.d);
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.AutoModerationRuleCreate.handle(packet.d);
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.AutoModerationRuleDelete.handle(packet.d);
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.AutoModerationRuleUpdate.handle(packet.d);
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.ChannelCreate.handle(packet.d);
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.ChannelDelete.handle(packet.d);
};

View file

@ -0,0 +1,22 @@
'use strict';
const Events = require('../../../util/Events');
module.exports = (client, { d: data }) => {
const channel = client.channels.cache.get(data.channel_id);
const time = data.last_pin_timestamp ? Date.parse(data.last_pin_timestamp) : null;
if (channel) {
// Discord sends null for last_pin_timestamp if the last pinned message was removed
channel.lastPinTimestamp = time;
/**
* Emitted whenever the pins of a channel are updated. Due to the nature of the WebSocket event,
* not much information can be provided easily here - you need to manually check the pins yourself.
* @event Client#channelPinsUpdate
* @param {TextBasedChannels} channel The channel that the pins update occurred in
* @param {Date} time The time of the pins update
*/
client.emit(Events.ChannelPinsUpdate, channel, time);
}
};

View file

@ -0,0 +1,16 @@
'use strict';
const Events = require('../../../util/Events');
module.exports = (client, packet) => {
const { old, updated } = client.actions.ChannelUpdate.handle(packet.d);
if (old && updated) {
/**
* Emitted whenever a channel is updated - e.g. name change, topic change, channel type change.
* @event Client#channelUpdate
* @param {DMChannel|GuildChannel} oldChannel The channel before the update
* @param {DMChannel|GuildChannel} newChannel The channel after the update
*/
client.emit(Events.ChannelUpdate, old, updated);
}
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildBanAdd.handle(packet.d);
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildBanRemove.handle(packet.d);
};

View file

@ -0,0 +1,26 @@
'use strict';
const Events = require('../../../util/Events');
const Status = require('../../../util/Status');
module.exports = (client, { d: data }, shard) => {
let guild = client.guilds.cache.get(data.id);
if (guild) {
if (!guild.available && !data.unavailable) {
// A newly available guild
guild._patch(data);
}
} else {
// A new guild
data.shardId = shard.id;
guild = client.guilds._add(data);
if (client.ws.status === Status.Ready) {
/**
* Emitted whenever the client joins a guild.
* @event Client#guildCreate
* @param {Guild} guild The created guild
*/
client.emit(Events.GuildCreate, guild);
}
}
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildDelete.handle(packet.d);
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildEmojisUpdate.handle(packet.d);
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildIntegrationsUpdate.handle(packet.d);
};

View file

@ -0,0 +1,36 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const Events = require('../../../util/Events');
module.exports = (client, { d: data }) => {
const guild = client.guilds.cache.get(data.guild_id);
if (!guild) return;
const members = new Collection();
for (const member of data.members) members.set(member.user.id, guild.members._add(member));
if (data.presences) {
for (const presence of data.presences) guild.presences._add(Object.assign(presence, { guild }));
}
/**
* Represents the properties of a guild members chunk
* @typedef {Object} GuildMembersChunk
* @property {number} index Index of the received chunk
* @property {number} count Number of chunks the client should receive
* @property {?string} nonce Nonce for this chunk
*/
/**
* Emitted whenever a chunk of guild members is received (all members come from the same guild).
* @event Client#guildMembersChunk
* @param {Collection<Snowflake, GuildMember>} members The members in the chunk
* @param {Guild} guild The guild related to the member chunk
* @param {GuildMembersChunk} chunk Properties of the received chunk
*/
client.emit(Events.GuildMembersChunk, members, guild, {
count: data.chunk_count,
index: data.chunk_index,
nonce: data.nonce,
});
};

View file

@ -0,0 +1,20 @@
'use strict';
const Events = require('../../../util/Events');
const Status = require('../../../util/Status');
module.exports = (client, { d: data }, shard) => {
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
guild.memberCount++;
const member = guild.members._add(data);
if (shard.status === Status.Ready) {
/**
* Emitted whenever a user joins a guild.
* @event Client#guildMemberAdd
* @param {GuildMember} member The member that has joined a guild
*/
client.emit(Events.GuildMemberAdd, member);
}
}
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet, shard) => {
client.actions.GuildMemberRemove.handle(packet.d, shard);
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet, shard) => {
client.actions.GuildMemberUpdate.handle(packet.d, shard);
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildRoleCreate.handle(packet.d);
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildRoleDelete.handle(packet.d);
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildRoleUpdate.handle(packet.d);
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildScheduledEventCreate.handle(packet.d);
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildScheduledEventDelete.handle(packet.d);
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildScheduledEventUpdate.handle(packet.d);
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildScheduledEventUserAdd.handle(packet.d);
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildScheduledEventUserRemove.handle(packet.d);
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildStickersUpdate.handle(packet.d);
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildUpdate.handle(packet.d);
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.InteractionCreate.handle(packet.d);
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.InviteCreate.handle(packet.d);
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.InviteDelete.handle(packet.d);
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.MessageCreate.handle(packet.d);
};

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.MessageDelete.handle(packet.d);
};

Some files were not shown because too many files have changed in this diff Show more