"use strict";
const util = require("util");
const Base = require("../structures/Base");
const Bucket = require("../util/Bucket");
const Channel = require("../structures/Channel");
const ForumChannel = require("../structures/ForumChannel");
const GuildChannel = require("../structures/GuildChannel");
const Message = require("../structures/Message");
const DMChannel = require("../structures/DMChannel");
const ExtendedUser = require("../structures/ExtendedUser");
const User = require("../structures/User");
const Invite = require("../structures/Invite");
const Interaction = require("../structures/Interaction");
const Constants = require("../Constants");
const ThreadChannel = require("../structures/ThreadChannel");
const StageInstance = require("../structures/StageInstance");
const GuildScheduledEvent = require("../structures/GuildScheduledEvent");
const GuildAuditLogEntry = require("../structures/GuildAuditLogEntry");
const AutoModerationRule = require("../structures/AutoModerationRule");
const SoundboardSound = require("../structures/SoundboardSound");
const WebSocket = typeof window !== "undefined" ? require("../util/BrowserWebSocket") : require("ws");
let EventEmitter;
try {
EventEmitter = require("eventemitter3");
} catch {
EventEmitter = require("events").EventEmitter;
}
let Erlpack;
try {
Erlpack = require("erlpack");
} catch {} // eslint-disable-line no-empty
let ZlibSync;
try {
ZlibSync = require("zlib-sync");
} catch {
try {
ZlibSync = require("pako");
} catch {} // eslint-disable-line no-empty
}
/**
* Represents a shard
* @extends EventEmitter
* @prop {Number} id The ID of the shard
* @prop {Boolean} connecting Whether the shard is connecting
* @prop {Array<String>?} discordServerTrace Debug trace of Discord servers
* @prop {Number} lastHeartbeatReceived Last time Discord acknowledged a heartbeat, null if shard has not sent heartbeat yet
* @prop {Number} lastHeartbeatSent Last time shard sent a heartbeat, null if shard has not sent heartbeat yet
* @prop {Number} latency The current latency between the shard and Discord, in milliseconds
* @prop {Boolean} ready Whether the shard is ready
* @prop {String} status The status of the shard. "disconnected"/"connecting"/"handshaking"/"ready"/"identifying"/"resuming"
*/
class Shard extends EventEmitter {
constructor(id, client) {
super();
this.id = id;
this.client = client;
this.onPacket = this.onPacket.bind(this);
this._onWSOpen = this._onWSOpen.bind(this);
this._onWSMessage = this._onWSMessage.bind(this);
this._onWSError = this._onWSError.bind(this);
this._onWSClose = this._onWSClose.bind(this);
this.hardReset();
}
checkReady() {
if (!this.ready) {
if (this.getAllUsersQueue.length > 0) {
this.requestGuildMembers(this.getAllUsersQueue);
this.getAllUsersQueue = [];
this.getAllUsersLength = 1;
return;
}
if (Object.keys(this.getAllUsersCount).length === 0) {
this.ready = true;
/**
* Fired when the shard turns ready
* @event Shard#ready
*/
super.emit("ready");
}
}
}
/**
* Tells the shard to connect
*/
connect() {
if (this.ws && this.ws.readyState != WebSocket.CLOSED) {
this.emit("error", new Error("Existing connection detected"), this.id);
return;
}
++this.connectAttempts;
this.connecting = true;
return this.initializeWS();
}
createGuild(_guild) {
this.client.guildShardMap[_guild.id] = this.id;
const guild = this.client.guilds.add(_guild, this.client, true);
if (this.client.options.getAllUsers && guild.members.size < guild.memberCount) {
this.getGuildMembers(guild.id, {
presences: this.client.options.intents && this.client.options.intents & Constants.Intents.guildPresences,
});
}
return guild;
}
/**
* Disconnects the shard
* @arg {Object?} [options] Shard disconnect options
* @arg {String | Boolean} [options.reconnect] false means destroy everything, true means you want to reconnect in the future, "auto" will autoreconnect
* @arg {Error} [error] The error that causes the disconnect
*/
disconnect(options = {}, error) {
if (!this.ws) {
return;
}
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
if (this.ws.readyState !== WebSocket.CLOSED) {
this.ws.removeListener("message", this._onWSMessage);
this.ws.removeListener("close", this._onWSClose);
try {
if (options.reconnect && this.sessionID) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.close(4901, "Eris: reconnect");
} else {
this.emit("debug", `Terminating websocket (state: ${this.ws.readyState})`, this.id);
this.ws.terminate();
}
} else {
this.ws.close(1000, "Eris: normal");
}
} catch (err) {
this.emit("error", err, this.id);
}
}
this.ws = null;
this.reset();
if (error) {
this.emit("error", error, this.id);
}
/**
* Fired when the shard disconnects
* @event Shard#disconnect
* @prop {Error?} err The error, if any
*/
super.emit("disconnect", error);
if (this.sessionID && this.connectAttempts >= this.client.options.maxResumeAttempts) {
this.emit("debug", `Automatically invalidating session due to excessive resume attempts | Attempt ${this.connectAttempts}`, this.id);
this.sessionID = null;
this.resumeURL = null;
}
if (options.reconnect === "auto" && this.client.options.autoreconnect) {
/**
* Fired when stuff happens and gives more info
* @event Client#debug
* @prop {String} message The debug message
* @prop {Number} id The ID of the shard
*/
if (this.sessionID) {
this.emit("debug", `Immediately reconnecting for potential resume | Attempt ${this.connectAttempts}`, this.id);
this.client.shards.connect(this);
} else {
this.emit("debug", `Queueing reconnect in ${this.reconnectInterval}ms | Attempt ${this.connectAttempts}`, this.id);
setTimeout(() => {
this.client.shards.connect(this);
}, this.reconnectInterval);
this.reconnectInterval = Math.min(Math.round(this.reconnectInterval * (Math.random() * 2 + 1)), 30000);
}
} else if (!options.reconnect) {
this.hardReset();
}
}
/**
* Update the bot's AFK status.
* @arg {Boolean} afk Whether the bot user is AFK or not
*/
editAFK(afk) {
this.presence.afk = !!afk;
this.sendStatusUpdate();
}
/**
* Updates the bot's status on all guilds the shard is in
* @arg {String} [status] Sets the bot's status, either "online", "idle", "dnd", or "invisible"
* @arg {Array | Object} [activities] Sets the bot's activities. A single activity object is also accepted for backwards compatibility
* @arg {String} [activities[].name] The name of the activity. Note: When setting a custom status, use `state` instead
* @arg {Number} activities[].type The type of the activity. 0 is playing, 1 is streaming (Twitch only), 2 is listening, 3 is watching, 4 is custom status, 5 is competing in
* @arg {String} [activities[].url] The URL of the activity
* @arg {String} [activities[].state] The state of the activity. This is the text to be displayed as the bots custom status
*/
editStatus(status, activities) {
if (activities === undefined && typeof status === "object") {
activities = status;
status = undefined;
}
if (status) {
this.presence.status = status;
}
if (activities === null) {
activities = [];
} else if (activities && !Array.isArray(activities)) {
activities = [activities];
}
if (activities !== undefined) {
this.presence.activities = activities;
}
this.sendStatusUpdate();
}
emit(event, ...args) {
this.client.emit.call(this.client, event, ...args);
if (event !== "error" || this.listeners("error").length > 0) {
super.emit.call(this, event, ...args);
}
}
getGuildMembers(guildID, timeout) {
if (this.getAllUsersCount.hasOwnProperty(guildID)) {
throw new Error("Cannot request all members while an existing request is processing");
}
this.getAllUsersCount[guildID] = true;
// Using intents, request one guild at a time
if (this.client.options.intents) {
if (!(this.client.options.intents & Constants.Intents.guildMembers)) {
throw new Error("Cannot request all members without guildMembers intent");
}
this.requestGuildMembers([guildID], timeout);
} else {
if (this.getAllUsersLength + 3 + guildID.length > 4048) { // 4096 - "{\"op\":8,\"d\":{\"guild_id\":[],\"query\":\"\",\"limit\":0}}".length + 1 for lazy comma offset
this.requestGuildMembers(this.getAllUsersQueue);
this.getAllUsersQueue = [guildID];
this.getAllUsersLength = 1 + guildID.length + 3;
} else {
this.getAllUsersQueue.push(guildID);
this.getAllUsersLength += guildID.length + 3;
}
}
}
hardReset() {
this.reset();
this.seq = 0;
this.sessionID = null;
this.resumeURL = null;
this.reconnectInterval = 1000;
this.connectAttempts = 0;
this.ws = null;
this.heartbeatInterval = null;
this.guildCreateTimeout = null;
this.globalBucket = new Bucket(120, 60000, { reservedTokens: 5 });
this.presenceUpdateBucket = new Bucket(5, 20000);
this.presence = JSON.parse(JSON.stringify(this.client.presence)); // Fast copy
Object.defineProperty(this, "_token", {
configurable: true,
enumerable: false,
writable: true,
value: this.client._token,
});
}
heartbeat(normal) {
// Can only heartbeat after identify/resume succeeds, session will be killed otherwise, discord/discord-api-docs#1619
if (this.status === "resuming" || this.status === "identifying") {
return;
}
if (normal) {
if (!this.lastHeartbeatAck) {
this.emit("debug", "Heartbeat timeout; " + JSON.stringify({
lastReceived: this.lastHeartbeatReceived,
lastSent: this.lastHeartbeatSent,
interval: this.heartbeatInterval,
status: this.status,
timestamp: Date.now(),
}));
return this.disconnect({
reconnect: "auto",
}, new Error("Server didn't acknowledge previous heartbeat, possible lost connection"));
}
this.lastHeartbeatAck = false;
}
this.lastHeartbeatSent = Date.now();
this.sendWS(Constants.GatewayOPCodes.HEARTBEAT, this.seq, true);
}
identify() {
if (this.client.options.compress && !ZlibSync) {
/**
* Fired when the shard encounters an error
* @event Client#error
* @prop {Error} err The error
* @prop {Number} id The ID of the shard
*/
this.emit("error", new Error("pako/zlib-sync not found, cannot decompress data"));
return;
}
this.status = "identifying";
const identify = {
token: this._token,
v: Constants.GATEWAY_VERSION,
compress: !!this.client.options.compress,
large_threshold: this.client.options.largeThreshold,
intents: this.client.options.intents,
properties: {
os: process.platform,
browser: "Eris",
device: "Eris",
},
};
if (this.client.options.maxShards > 1) {
identify.shard = [this.id, this.client.options.maxShards];
}
if (this.presence.status) {
identify.presence = this.presence;
}
this.sendWS(Constants.GatewayOPCodes.IDENTIFY, identify);
}
initializeWS() {
if (!this._token) {
return this.disconnect(null, new Error("Token not specified"));
}
this.status = "connecting";
if (this.client.options.compress) {
this.emit("debug", "Initializing zlib-sync-based compression");
this._zlibSync = new ZlibSync.Inflate({
chunkSize: 128 * 1024,
});
}
if (this.sessionID) {
if (!this.resumeURL) {
this.emit("warn", "Resume url is not currently present. Discord may disconnect you quicker.");
}
this.ws = new WebSocket(this.resumeURL || this.client.gatewayURL, this.client.options.ws);
} else {
this.ws = new WebSocket(this.client.gatewayURL, this.client.options.ws);
}
this.ws.on("open", this._onWSOpen);
this.ws.on("message", this._onWSMessage);
this.ws.on("error", this._onWSError);
this.ws.on("close", this._onWSClose);
this.connectTimeout = setTimeout(() => {
if (this.connecting) {
this.disconnect({
reconnect: "auto",
}, new Error("Connection timeout"));
}
}, this.client.options.connectionTimeout);
}
onPacket(packet) {
if (this.listeners("rawWS").length > 0 || this.client.listeners("rawWS").length) {
/**
* Fired when the shard receives a websocket packet
* @event Client#rawWS
* @prop {Object} packet The packet
* @prop {Number} id The ID of the shard
*/
this.emit("rawWS", packet, this.id);
}
if (packet.s) {
if (packet.s > this.seq + 1 && this.ws && this.status !== "resuming") {
/**
* Fired to warn of something weird but non-breaking happening
* @event Client#warn
* @prop {String} message The warning message
* @prop {Number} id The ID of the shard
*/
this.emit("warn", `Non-consecutive sequence (${this.seq} -> ${packet.s})`, this.id);
}
this.seq = packet.s;
}
switch (packet.op) {
case Constants.GatewayOPCodes.DISPATCH: {
if (!this.client.options.disableEvents[packet.t]) {
this.wsEvent(packet);
}
break;
}
case Constants.GatewayOPCodes.HEARTBEAT: {
this.heartbeat();
break;
}
case Constants.GatewayOPCodes.INVALID_SESSION: {
this.seq = 0;
this.sessionID = null;
this.resumeURL = null;
this.emit("warn", "Invalid session, reidentifying!", this.id);
this.identify();
break;
}
case Constants.GatewayOPCodes.RECONNECT: {
this.emit("debug", "Reconnecting due to server request", this.id);
this.disconnect({
reconnect: "auto",
});
break;
}
case Constants.GatewayOPCodes.HELLO: {
if (packet.d.heartbeat_interval > 0) {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
}
this.heartbeatInterval = setInterval(() => this.heartbeat(true), packet.d.heartbeat_interval);
}
this.discordServerTrace = packet.d._trace;
this.connecting = false;
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
}
this.connectTimeout = null;
if (this.sessionID) {
this.resume();
} else {
this.identify();
// Cannot heartbeat when resuming, discord/discord-api-docs#1619
this.heartbeat();
}
/**
* Fired when a shard receives an OP:10/HELLO packet
* @event Client#hello
* @prop {Array<String>} trace The Discord server trace of the gateway and session servers
* @prop {Number} id The ID of the shard
*/
this.emit("hello", packet.d._trace, this.id);
break;
}
case Constants.GatewayOPCodes.HEARTBEAT_ACK: {
this.lastHeartbeatAck = true;
this.lastHeartbeatReceived = Date.now();
this.latency = this.lastHeartbeatReceived - this.lastHeartbeatSent;
break;
}
default: {
this.emit("unknown", packet, this.id);
break;
}
}
}
requestGuildMembers(guildID, options) {
const opts = {
guild_id: guildID,
limit: (options && options.limit) || 0,
user_ids: options && options.userIDs,
query: options && options.query,
nonce: Date.now().toString() + Math.random().toString(36),
presences: options && options.presences,
};
if (!opts.user_ids && !opts.query) {
opts.query = "";
}
if (!opts.query && !opts.user_ids && (this.client.options.intents && !(this.client.options.intents & Constants.Intents.guildMembers))) {
throw new Error("Cannot request all members without guildMembers intent");
}
if (opts.presences && (this.client.options.intents && !(this.client.options.intents & Constants.Intents.guildPresences))) {
throw new Error("Cannot request members presences without guildPresences intent");
}
if (opts.user_ids && opts.user_ids.length > 100) {
throw new Error("Cannot request more than 100 users by their ID");
}
this.sendWS(Constants.GatewayOPCodes.REQUEST_GUILD_MEMBERS, opts);
return new Promise((res) => this.requestMembersPromise[opts.nonce] = {
res: res,
received: 0,
members: [],
timeout: setTimeout(() => {
res(this.requestMembersPromise[opts.nonce].members);
delete this.requestMembersPromise[opts.nonce];
}, (options && options.timeout) || this.client.options.requestTimeout),
});
}
requestGuildSoundboardSounds(options) {
const opts = {
guild_ids: options.guildIDs,
};
const soundboardSounds = options.guildIDs.reduce((obj, key) => {
obj[key] = undefined;
return obj;
}, {});
const nonce = Date.now().toString() + Math.random().toString(36);
this.sendWS(Constants.GatewayOPCodes.REQUEST_SOUNDBOARD_SOUNDS, opts);
return new Promise((res) => this.requestSoundboardSoundsPromise[nonce] = {
res: res,
soundboardSounds: soundboardSounds,
timeout: setTimeout(() => {
res(this.requestSoundboardSoundsPromise[nonce].soundboardSounds);
delete this.requestSoundboardSoundsPromise[nonce];
}, (options && options.timeout) || this.client.options.requestTimeout),
});
}
reset() {
this.connecting = false;
this.ready = false;
this.preReady = false;
if (this.requestMembersPromise !== undefined) {
for (const guildID in this.requestMembersPromise) {
if (!this.requestMembersPromise.hasOwnProperty(guildID)) {
continue;
}
clearTimeout(this.requestMembersPromise[guildID].timeout);
this.requestMembersPromise[guildID].res(this.requestMembersPromise[guildID].received);
}
}
this.requestMembersPromise = {};
this.requestSoundboardSoundsPromise = {};
this.getAllUsersCount = {};
this.getAllUsersQueue = [];
this.getAllUsersLength = 1;
this.latency = Infinity;
this.lastHeartbeatAck = true;
this.lastHeartbeatReceived = null;
this.lastHeartbeatSent = null;
this.status = "disconnected";
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
}
this.connectTimeout = null;
}
restartGuildCreateTimeout() {
if (this.guildCreateTimeout) {
clearTimeout(this.guildCreateTimeout);
this.guildCreateTimeout = null;
}
if (!this.ready) {
if (this.client.unavailableGuilds.size === 0) {
return this.checkReady();
}
this.guildCreateTimeout = setTimeout(() => {
this.checkReady();
}, this.client.options.guildCreateTimeout);
}
}
resume() {
this.status = "resuming";
this.sendWS(Constants.GatewayOPCodes.RESUME, {
token: this._token,
session_id: this.sessionID,
seq: this.seq,
});
}
sendStatusUpdate() {
this.sendWS(Constants.GatewayOPCodes.PRESENCE_UPDATE, {
activities: this.presence.activities,
afk: !!this.presence.afk,
since: this.presence.status === "idle" ? Date.now() : null,
status: this.presence.status,
});
}
sendWS(op, _data, priority = false) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
let i = 0;
let waitFor = 1;
const func = () => {
if (++i >= waitFor && this.ws && this.ws.readyState === WebSocket.OPEN) {
const data = Erlpack ? Erlpack.pack({ op: op, d: _data }) : JSON.stringify({ op: op, d: _data });
this.ws.send(data);
if (_data.token) {
delete _data.token;
}
this.emit("debug", JSON.stringify({ op: op, d: _data }), this.id);
}
};
if (op === Constants.GatewayOPCodes.PRESENCE_UPDATE) {
++waitFor;
this.presenceUpdateBucket.queue(func, priority);
}
this.globalBucket.queue(func, priority);
}
}
wsEvent(packet) {
switch (packet.t) {
case "AUTO_MODERATION_ACTION_EXECUTION": {
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("debug", `Missing guild ${packet.d.guild_id} in AUTO_MODERATION_ACTION_EXECUTION`);
break;
}
/**
* Fired when an auto moderation action is executed.
* @event Client#autoModerationActionExecution
* @prop {Guild} guild The guild associated with the action
* @prop {Object} action The exection action
*/
this.emit("autoModerationActionExecution", guild, {
action: packet.d.action,
alertSystemMessageID: packet.d.alert_system_message_id,
channelID: packet.d.channel_id,
content: packet.d.content,
guildID: packet.d.guild_id,
matchedContent: packet.d.matched_content,
matchedKeyword: packet.d.matched_keyword,
messageID: packet.d.message_id,
ruleID: packet.d.rule_id,
ruleTriggerType: packet.d.rule_trigger_type,
userID: packet.d.user_id,
});
break;
}
case "AUTO_MODERATION_RULE_CREATE": {
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("debug", `Missing guild ${packet.d.guild_id} in AUTO_MODERATION_RULE_CREATE`);
break;
}
/**
* Fired when an auto moderation rule is created
* @event Client#autoModerationRuleCreate
* @prop {Guild} guild The guild associated with the rule
* @prop {AutoModerationRule} rule The created rule
*/
this.emit("autoModerationRuleCreate", guild, guild.autoModerationRules.add(packet.d, this.client));
break;
}
case "AUTO_MODERATION_RULE_DELETE": {
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("debug", `Missing guild ${packet.d.guild_id} in AUTO_MODERATION_RULE_DELETE`);
break;
}
/**
* Fired when an auto moderation rule is deleted
* @event Client#autoModerationRuleDelete
* @prop {Guild} guild The guild associated with the rule
* @prop {AutoModerationRule} rule The deleted rule
*/
this.emit("autoModerationRuleDelete", guild, guild.autoModerationRules.remove(packet.d) || new AutoModerationRule(packet.d, this.client));
break;
}
case "AUTO_MODERATION_RULE_UPDATE": {
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("debug", `Missing guild ${packet.d.guild_id} in AUTO_MODERATION_RULE_DELETE`);
break;
}
const rule = guild.autoModerationRules.get(packet.d.id);
let oldRule = null;
oldRule = {
actions: rule.actions,
enabled: rule.enabled,
eventType: rule.eventType,
exemptChannels: rule.exemptChannels,
exemptRoles: rule.exemptRoles,
name: rule.name,
triggerMetadata: rule.triggerMetadata,
};
/**
* Fired when an auto moderation rule is updated
* @event Client#autoModerationRuleUpdate
* @prop {Guild} guild The guild associated with the rule
* @prop {AutoModerationRule} rule The updated role
* @prop {Object?} oldRule The old rule. If the rule was uncached, this will be null
*/
this.emit("autoModerationRuleUpdate", guild, guild.autoModerationRules.update(packet.d, this.client), oldRule);
break;
}
case "PRESENCE_UPDATE": {
if (packet.d.user.username !== undefined) {
let user = this.client.users.get(packet.d.user.id);
let oldUser = null;
if (user && (user.username !== packet.d.user.username || user.globalName !== packet.d.user.global_name || user.discriminator !== packet.d.user.discriminator || user.avatar !== packet.d.user.avatar || (packet.d.user.avatar_decoration_data && user.avatarDecorationData && user.avatarDecorationData.asset !== packet.d.user.avatar_decoration_data.asset))) {
oldUser = {
username: user.username,
globalName: user.globalName,
discriminator: user.discriminator,
avatar: user.avatar,
avatarDecorationData: user.avatarDecorationData,
};
}
if (!user || oldUser) {
user = this.client.users.update(packet.d.user, this.client);
/**
* Fired when a user's avatar, avatar decoration, discriminator, display name or username changes
* @event Client#userUpdate
* @prop {User} user The updated user
* @prop {Object?} oldUser The old user data. If the user was uncached, this will be null
* @prop {String?} oldUser.avatar The hash of the user's avatar, or null if no avatar
* @prop {Object?} oldUser.avatarDecorationData The data of the user's avatar decoration, including the asset and sku ID, or null if no avatar decoration
* @prop {String} oldUser.discriminator The discriminator of the user
* @prop {String?} oldUser.globalName The user's display name, if it is set. For bots, this is the application name
* @prop {String} oldUser.username The username of the user
*/
this.emit("userUpdate", user, oldUser);
}
}
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("debug", "Rogue presence update: " + JSON.stringify(packet), this.id);
break;
}
let member = guild.members.get(packet.d.id = packet.d.user.id);
let oldPresence = null;
if (member) {
oldPresence = {
activities: member.activities,
clientStatus: member.clientStatus,
status: member.status,
};
}
if ((!member && packet.d.user.username) || oldPresence) {
member = guild.members.update(packet.d, guild);
/**
* Fired when a guild member's status or game changes
* @event Client#presenceUpdate
* @prop {Member} other The updated member
* @prop {Object?} oldPresence The old presence data. If the user was offline when the bot started and the client option getAllUsers is not true, this will be null
* @prop {Array<Object>?} oldPresence.activities The member's current activities
* @prop {Object?} oldPresence.clientStatus The member's per-client status
* @prop {String} oldPresence.clientStatus.desktop The member's status on desktop. Either "online", "idle", "dnd", or "offline". Will be "offline" for bots
* @prop {String} oldPresence.clientStatus.mobile The member's status on mobile. Either "online", "idle", "dnd", or "offline". Will be "offline" for bots
* @prop {String} oldPresence.clientStatus.web The member's status on web. Either "online", "idle", "dnd", or "offline". Will be "online" for bots
* @prop {String} oldPresence.status The other user's old status. Either "online", "idle", or "offline"
*/
this.emit("presenceUpdate", member, oldPresence);
}
break;
}
case "VOICE_STATE_UPDATE": { // (╯°□°)╯︵ ┻━┻
if (packet.d.guild_id && packet.d.user_id === this.client.user.id) {
const voiceConnection = this.client.voiceConnections.get(packet.d.guild_id);
if (voiceConnection) {
if (packet.d.channel_id === null) {
this.client.voiceConnections.leave(packet.d.guild_id);
} else if (voiceConnection.channelID !== packet.d.channel_id) {
voiceConnection.switchChannel(packet.d.channel_id, true);
}
}
}
if (packet.d.self_stream === undefined) {
packet.d.self_stream = false;
}
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
break;
}
let member = guild.members.get(packet.d.id = packet.d.user_id);
if (!member) {
if (!packet.d.member) {
this.emit("voiceStateUpdate", {
id: packet.d.user_id,
voiceState: {
deaf: packet.d.deaf,
mute: packet.d.mute,
selfDeaf: packet.d.self_deaf,
selfMute: packet.d.self_mute,
selfStream: packet.d.self_stream,
selfVideo: packet.d.self_video,
},
}, null);
break;
}
// Updates the member cache with this member for future events.
packet.d.member.id = packet.d.user_id;
member = guild.members.add(packet.d.member, guild);
const channel = guild.channels.find((channel) => (channel.type === Constants.ChannelTypes.GUILD_VOICE || channel.type === Constants.ChannelTypes.GUILD_STAGE_VOICE) && channel.voiceMembers.get(packet.d.id));
if (channel) {
channel.voiceMembers.remove(packet.d);
this.emit("debug", "VOICE_STATE_UPDATE member null but in channel: " + packet.d.id, this.id);
}
}
const oldState = {
deaf: member.voiceState.deaf,
mute: member.voiceState.mute,
selfDeaf: member.voiceState.selfDeaf,
selfMute: member.voiceState.selfMute,
selfStream: member.voiceState.selfStream,
selfVideo: member.voiceState.selfVideo,
};
const oldChannelID = member.voiceState.channelID;
member.update(packet.d, this.client);
if (oldChannelID != packet.d.channel_id) {
let oldChannel, newChannel;
if (oldChannelID) {
oldChannel = guild.channels.get(oldChannelID);
if (oldChannel && oldChannel.type !== Constants.ChannelTypes.GUILD_VOICE && oldChannel.type !== Constants.ChannelTypes.GUILD_STAGE_VOICE) {
this.emit("warn", "Old channel not a recognized voice channel: " + oldChannelID, this.id);
oldChannel = null;
}
}
if (packet.d.channel_id && (newChannel = guild.channels.get(packet.d.channel_id)) && (newChannel.type === Constants.ChannelTypes.GUILD_VOICE || newChannel.type === Constants.ChannelTypes.GUILD_STAGE_VOICE)) { // Welcome to Discord, where one can "join" text channels
if (oldChannel) {
/**
* Fired when a guild member switches voice channels
* @event Client#voiceChannelSwitch
* @prop {Member} member The member
* @prop {VoiceChannel | StageChannel} newChannel The new voice channel
* @prop {VoiceChannel | StageChannel} oldChannel The old voice channel
*/
oldChannel.voiceMembers.remove(member);
this.emit("voiceChannelSwitch", newChannel.voiceMembers.add(member, guild), newChannel, oldChannel);
} else {
/**
* Fired when a guild member joins a voice channel. This event is not fired when a member switches voice channels, see `voiceChannelSwitch`
* @event Client#voiceChannelJoin
* @prop {Member} member The member
* @prop {VoiceChannel | StageChannel} newChannel The voice channel
*/
this.emit("voiceChannelJoin", newChannel.voiceMembers.add(member, guild), newChannel);
}
} else if (oldChannel) {
oldChannel.voiceMembers.remove(member);
/**
* Fired when a guild member leaves a voice channel. This event is not fired when a member switches voice channels, see `voiceChannelSwitch`
* @event Client#voiceChannelLeave
* @prop {Member?} member The member
* @prop {VoiceChannel | StageChannel} oldChannel The voice channel
*/
this.emit("voiceChannelLeave", member, oldChannel);
}
}
if (oldState.mute !== member.voiceState.mute || oldState.deaf !== member.voiceState.deaf || oldState.selfMute !== member.voiceState.selfMute || oldState.selfDeaf !== member.voiceState.selfDeaf || oldState.selfStream !== member.voiceState.selfStream || oldState.selfVideo !== member.voiceState.selfVideo) {
/**
* Fired when a guild member's voice state changes
* @event Client#voiceStateUpdate
* @prop {Member | Object} member The member. If the member is not cached and Discord doesn't send a member payload, this will be an object with `id` and `voiceState` keys. No other property is guaranteed
* @prop {Object?} oldState The old voice state of the member. If the above caveat applies, this will be null
* @prop {Boolean} oldState.deaf The previous server deaf status
* @prop {Boolean} oldState.mute The previous server mute status
* @prop {Boolean} oldState.selfDeaf The previous self deaf status
* @prop {Boolean} oldState.selfMute The previous self mute status
* @prop {Boolean} oldState.selfStream The previous self stream status
* @prop {Boolean} oldState.selfVideo The previous self video status
*/
this.emit("voiceStateUpdate", member, oldState);
}
break;
}
case "VOICE_CHANNEL_STATUS_UPDATE": {
const channel = this.client.getChannel(packet.d.id);
if (!channel) {
break;
}
const oldChannel = {
status: channel.status,
};
channel.update({
status: packet.d.status,
});
/**
* Fired when a voice channel status is updated
* @event Client#voiceChannelStatusUpdate
* @prop {VoiceChannel} channel The updated voice channel
* @prop {Object} oldChannel The old channel data
* @prop {String?} oldChannel.status The old voice channel status
*/
this.emit("voiceChannelStatusUpdate", channel, oldChannel);
break;
}
case "VOICE_CHANNEL_EFFECT_SEND": {
const channel = this.client.getChannel(packet.d.channel_id) || { id: packet.d.channel_id };
const guild = this.client.guilds.get(packet.d.guild_id) || { id: packet.d.guild_id };
const user = this.client.users.get(packet.d.user_id) || { id: packet.d.user_id };
/**
* Fired when a user sends a voice channel effect
* @event Client#voiceChannelEffectSend
* @prop {Object} effect The effect that was sent
* @prop {Number?} effect.animationID The animation ID
* @prop {Number?} effect.animationType The animation type
* @prop {VoiceChannel | StageChannel | Object} effect.channel The voice channel the effect was sent in. If the channel is not cached, this will be an object with an `id` key. No other property is guaranteed
* @prop {Object?} effect.emoji The emoji sent
* @prop {Guild | Object} effect.guild The guild the effect was sent in. If the guild is not cached, this will be an object with an `id` key. No other property is guaranteed
* @prop {(String | Number)?} effect.soundID The sound ID
* @prop {Number?} effect.soundVolume The volume of the sound (a number between 0 and 1)
* @prop {User | Object} effect.user The user that sent the effect. If the user is not cached, this will be an object with an `id` key. No other property is guaranteed
*/
this.emit("voiceChannelEffectSend", {
channel: channel,
guild: guild,
user: user,
emoji: packet.d.emoji,
animationType: packet.d.animation_type,
animationID: packet.d.animation_id,
soundID: packet.d.sound_id,
soundVolume: packet.d.sound_volume,
});
break;
}
case "TYPING_START": {
let member = null;
const guild = this.client.guilds.get(packet.d.guild_id);
if (guild) {
packet.d.member.id = packet.d.user_id;
member = guild.members.update(packet.d.member, guild);
}
if (this.client.listeners("typingStart").length > 0) {
/**
* Fired when a user begins typing
* @event Client#typingStart
* @prop {DMChannel | TextChannel | NewsChannel | Object} channel The text channel the user is typing in. If the channel is not cached, this will be an object with an `id` key. No other property is guaranteed
* @prop {User | Object} user The user. If the user is not cached, this will be an object with an `id` key. No other property is guaranteed
* @prop {Member?} member The guild member, if typing in a guild channel, or `null`, if typing in a DMChannel
*/
this.emit("typingStart", this.client.getChannel(packet.d.channel_id) || { id: packet.d.channel_id }, this.client.users.get(packet.d.user_id) || { id: packet.d.user_id }, member);
}
break;
}
case "MESSAGE_CREATE": {
const channel = this.client.getChannel(packet.d.channel_id);
if (channel) { // MESSAGE_CREATE just when deleting o.o
channel.lastMessageID = packet.d.id;
if (channel instanceof ThreadChannel) {
channel.messageCount++;
channel.totalMessageSent++;
}
/**
* Fired when a message is created
* @event Client#messageCreate
* @prop {Message} message The message.
*/
this.emit("messageCreate", channel.messages.add(packet.d, this.client));
} else {
this.emit("messageCreate", new Message(packet.d, this.client));
}
break;
}
case "MESSAGE_UPDATE": {
const channel = this.client.getChannel(packet.d.channel_id);
if (!channel) {
packet.d.channel = {
id: packet.d.channel_id,
};
this.emit("messageUpdate", packet.d, null);
break;
}
const message = channel.messages.get(packet.d.id);
let oldMessage = null;
if (message) {
oldMessage = {
attachments: message.attachments,
channelMentions: message.channelMentions,
content: message.content,
editedTimestamp: message.editedTimestamp,
embeds: message.embeds,
flags: message.flags,
mentionedBy: message.mentionedBy,
mentions: message.mentions,
pinned: message.pinned,
poll: message.poll,
roleMentions: message.roleMentions,
tts: message.tts,
};
} else if (!packet.d.timestamp) {
packet.d.channel = channel;
this.emit("messageUpdate", packet.d, null);
break;
}
/**
* Fired when a message is updated
* @event Client#messageUpdate
* @prop {Message} message The updated message. If oldMessage is null, it is recommended to discard this event, since the message data will be very incomplete (only `id` and `channel` are guaranteed). If the channel isn't cached, `channel` will be an object with an `id` key.
* @prop {Object?} oldMessage The old message data. If the message was cached, this will return the full old message. Otherwise, it will be null
* @prop {Array<Object>} oldMessage.attachments Array of attachments
* @prop {Array<String>} oldMessage.channelMentions Array of mentions channels' ids.
* @prop {String} oldMessage.content Message content
* @prop {Number} oldMessage.editedTimestamp Timestamp of latest message edit
* @prop {Array<Object>} oldMessage.embeds Array of embeds
* @prop {Number} oldMessage.flags Old message flags (see constants)
* @prop {Object} oldMessage.mentionedBy Object of if different things mention the bot user
* @prop {Array<User>} oldMessage.mentions Array of mentioned users' ids
* @prop {Boolean} oldMessage.pinned Whether the message was pinned or not
* @prop {Object} oldMessage.poll A poll object. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/poll#poll-object) for object structure
* @prop {Array<String>} oldMessage.roleMentions Array of mentioned roles' ids.
* @prop {Boolean} oldMessage.tts Whether to play the message using TTS or not
*/
this.emit("messageUpdate", channel.messages.update(packet.d, this.client), oldMessage);
break;
}
case "MESSAGE_DELETE": {
const channel = this.client.getChannel(packet.d.channel_id);
if (channel instanceof ThreadChannel) {
channel.messageCount--;
}
/**
* Fired when a cached message is deleted
* @event Client#messageDelete
* @prop {Message | Object} message The message object. If the message is not cached, this will be an object with `id` and `channel` keys. If the channel is not cached, channel will be an object with an `id` key. If the uncached message is from a guild, the message will also contain a `guildID` key, and the channel will contain a `guild` with an `id` key. No other property is guaranteed.
*/
this.emit("messageDelete", (channel && channel.messages.remove(packet.d)) || {
id: packet.d.id,
channel: channel || {
id: packet.d.channel_id,
guild: packet.d.guild_id ? { id: packet.d.guild_id } : undefined,
},
guildID: packet.d.guild_id,
});
break;
}
case "MESSAGE_DELETE_BULK": {
const channel = this.client.getChannel(packet.d.channel_id);
if (channel instanceof ThreadChannel) {
channel.messageCount -= packet.d.ids.length;
}
/**
* Fired when a bulk delete occurs
* @event Client#messageDeleteBulk
* @prop {Array<Message> | Array<Object>} messages An array of (potentially partial) message objects. If a message is not cached, it will be an object with `id` and `channel` keys If the uncached messages are from a guild, the messages will also contain a `guildID` key, and the channel will contain a `guild` with an `id` key. No other property is guaranteed
*/
this.emit("messageDeleteBulk", packet.d.ids.map((id) => ((channel && channel.messages.remove({ id })) || {
id: id,
channel: { id: packet.d.channel_id, guild: packet.d.guild_id ? { id: packet.d.guild_id } : undefined },
guildID: packet.d.guild_id,
})));
break;
}
case "MESSAGE_REACTION_ADD": {
const channel = this.client.getChannel(packet.d.channel_id);
let message;
let member;
if (channel) {
message = channel.messages.get(packet.d.message_id);
if (channel.guild) {
if (packet.d.member) {
// Updates the member cache with this member for future events.
packet.d.member.id = packet.d.user_id;
member = channel.guild.members.update(packet.d.member, channel.guild);
}
}
}
if (message) {
const reaction = packet.d.emoji.id ? `${packet.d.emoji.name}:${packet.d.emoji.id}` : packet.d.emoji.name;
if (message.reactions[reaction]) {
++message.reactions[reaction].count;
++message.reactions[reaction].count_details[packet.d.burst ? "burst" : "normal"];
if (packet.d.user_id === this.client.user.id) {
message.reactions[reaction].me = true;
if (packet.d.burst) {
message.reactions[reaction].me_burst = true;
}
}
message.reactions[reaction].burst_colors = packet.d.burst_colors;
} else {
message.reactions[reaction] = {
burst_colors: packet.d.burst_colors,
count: 1,
count_details: {
burst: +packet.d.burst,
normal: +!packet.d.burst,
},
me: packet.d.user_id === this.client.user.id && !packet.d.burst,
me_burst: packet.d.user_id === this.client.user.id && packet.d.burst,
type: packet.d.type,
};
}
} else {
message = {
id: packet.d.message_id,
channel: channel || { id: packet.d.channel_id },
};
if (packet.d.guild_id) {
message.guildID = packet.d.guild_id;
if (!message.channel.guild) {
message.channel.guild = { id: packet.d.guild_id };
}
}
}
/**
* Fired when someone adds a reaction to a message
* @event Client#messageReactionAdd
* @prop {Message | Object} message The message object. If the message is not cached, this will be an object with `id`, `channel`, and if inside a guild, `guildID` keys. If the channel is not cached, channel key will be an object with only an id. `guildID` will be present if the message was sent in a guild channel. No other property is guaranteed
* @prop {Object} emoji The reaction emoji object
* @prop {Boolean?} emoji.animated Whether the emoji is animated or not
* @prop {String?} emoji.id The emoji ID (null for non-custom emojis)
* @prop {String} emoji.name The emoji name
* @prop {Member | Object} reactor The member, if the reaction is in a guild. If the reaction is not in a guild, this will be an object with an `id` key. No other property is guaranteed
* @prop {Boolean} burst Whether the reaction is a super reaction
*/
this.emit("messageReactionAdd", message, packet.d.emoji, member || { id: packet.d.user_id }, packet.d.burst);
break;
}
case "MESSAGE_REACTION_REMOVE": {
const channel = this.client.getChannel(packet.d.channel_id);
let message;
if (channel) {
message = channel.messages.get(packet.d.message_id);
}
if (message) {
const reaction = packet.d.emoji.id ? `${packet.d.emoji.name}:${packet.d.emoji.id}` : packet.d.emoji.name;
const reactionObj = message.reactions[reaction];
if (reactionObj) {
--reactionObj.count;
--reactionObj.count_details[packet.d.burst ? "burst" : "normal"];
if (reactionObj.count === 0) {
delete message.reactions[reaction];
} else if (packet.d.user_id === this.client.user.id) {
message.reactions[reaction].me = false;
if (packet.d.burst) {
message.reactions[reaction].me_burst = false;
}
}
}
} else {
message = {
id: packet.d.message_id,
channel: channel || { id: packet.d.channel_id },
};
if (packet.d.guild_id) {
message.guildID = packet.d.guild_id;
if (!message.channel.guild) {
message.channel.guild = { id: packet.d.guild_id };
}
}
}
/**
* Fired when someone removes a reaction from a message
* @event Client#messageReactionRemove
* @prop {Message | Object} message The message object. If the message is not cached, this will be an object with `id`, `channel`, and if inside a guild, `guildID` keys. If the channel is not cached, channel key will be an object with only an id. `guildID` will be present if the message was sent in a guild channel. No other property is guaranteed
* @prop {Object} emoji The reaction emoji object
* @prop {Boolean?} emoji.animated Whether the emoji is animated or not
* @prop {String?} emoji.id The ID of the emoji (null for non-custom emojis)
* @prop {String} emoji.name The emoji name
* @prop {String} userID The ID of the user that removed the reaction
* @prop {Boolean} burst Whether the reaction is a super reaction
*/
this.emit("messageReactionRemove", message, packet.d.emoji, packet.d.user_id, packet.d.burst);
break;
}
case "MESSAGE_REACTION_REMOVE_ALL": {
const channel = this.client.getChannel(packet.d.channel_id);
let message;
if (channel) {
message = channel.messages.get(packet.d.message_id);
if (message) {
message.reactions = {};
}
}
if (!message) {
message = {
id: packet.d.message_id,
channel: channel || { id: packet.d.channel_id },
};
if (packet.d.guild_id) {
message.guildID = packet.d.guild_id;
if (!message.channel.guild) {
message.channel.guild = { id: packet.d.guild_id };
}
}
}
/**
* Fired when all reactions are removed from a message
* @event Client#messageReactionRemoveAll
* @prop {Message | Object} message The message object. If the message is not cached, this will be an object with `id`, `channel`, and if inside a guild, `guildID` keys. If the channel is not cached, channel key will be an object with only an id. No other property is guaranteed
*/
this.emit("messageReactionRemoveAll", message);
break;
}
case "MESSAGE_REACTION_REMOVE_EMOJI": {
const channel = this.client.getChannel(packet.d.channel_id);
let message;
if (channel) {
message = channel.messages.get(packet.d.message_id);
if (message) {
const reaction = packet.d.emoji.id ? `${packet.d.emoji.name}:${packet.d.emoji.id}` : packet.d.emoji.name;
delete message.reactions[reaction];
}
}
if (!message) {
message = {
id: packet.d.message_id,
channel: channel || { id: packet.d.channel_id },
};
if (packet.d.guild_id) {
message.guildID = packet.d.guild_id;
if (!message.channel.guild) {
message.channel.guild = { id: packet.d.guild_id };
}
}
}
/**
* Fired when someone removes all reactions from a message for a single emoji
* @event Client#messageReactionRemoveEmoji
* @prop {Message | Object} message The message object. If the message is not cached, this will be an object with `id` and `channel` keys. If the channel is not cached, channel key will be an object with only an id. No other property is guaranteed
* @prop {Object} emoji The reaction emoji object
* @prop {Boolean?} emoji.animated Whether the emoji is animated or not
* @prop {String?} emoji.id The ID of the emoji (null for non-custom emojis)
* @prop {String} emoji.name The emoji name
*/
this.emit("messageReactionRemoveEmoji", message, packet.d.emoji);
break;
}
case "MESSAGE_POLL_VOTE_ADD": {
const user = this.client.users.get(packet.d.user_id);
const channel = this.client.getChannel(packet.d.channel_id);
let message;
if (channel) {
message = channel.messages.get(packet.d.message_id);
}
if (!message) {
message = {
id: packet.d.message_id,
channel: channel || { id: packet.d.channel_id },
};
if (packet.d.guild_id) {
message.guildID = packet.d.guild_id;
if (!message.channel.guild) {
message.channel.guild = { id: packet.d.guild_id };
}
}
} else if (message.poll.results && message.poll.results.answer_counts) {
const answer = message.poll.results.answer_counts.find((answer) => answer.id === packet.d.answer_id);
if (answer) {
answer.count++;
if (packet.d.user_id === this.client.user.id) {
answer.me_voted = true;
}
} else {
message.poll.results.answer_counts.push({
id: packet.d.answer_id,
count: 1,
me_voted: packet.d.user_id === this.client.user.id,
});
}
}
/**
* Fired when a user votes on a poll. If the poll allows multiple selection, one event will be sent per answer
* @event Client#messagePollVoteAdd
* @prop {Message | Object} message The message object. If the message is not cached, this will be an object with `id` and `channel` keys. If the channel is not cached, channel key will be an object with an id and a `guild` key with only an id. No other property is guaranteed
* @prop {User | Object} user The user who voted on the poll. If the user is not cached, this will be an object with only an `id` key
* @prop {Number} answerID The ID of the answer
*/
this.emit("messagePollVoteAdd", message, user || { id: packet.d.user_id }, packet.d.answer_id);
break;
}
case "MESSAGE_POLL_VOTE_REMOVE": {
const user = this.client.users.get(packet.d.user_id);
const channel = this.client.getChannel(packet.d.channel_id);
let message;
if (channel) {
message = channel.messages.get(packet.d.message_id);
}
if (!message) {
message = {
id: packet.d.message_id,
channel: channel || { id: packet.d.channel_id },
};
if (packet.d.guild_id) {
message.guildID = packet.d.guild_id;
if (!message.channel.guild) {
message.channel.guild = { id: packet.d.guild_id };
}
}
} else if (message.poll.results && message.poll.results.answer_counts) {
const answer = message.poll.results.answer_counts.find((answer) => answer.id === packet.d.answer_id);
if (answer) {
answer.count--;
if (packet.d.user_id === this.client.user.id) {
answer.me_voted = false;
}
}
}
/**
* Fired when a user removes their vote on a poll. If the poll allows multiple selection, one event will be sent per answer
* @event Client#messagePollVoteRemove
* @prop {Message | Object} message The message object. If the message is not cached, this will be an object with `id` and `channel` keys. If the channel is not cached, channel key will be an object with an id and a `guild` key with only an id. No other property is guaranteed
* @prop {User | Object} user The user who removed their vote from the poll. If the user is not cached, this will be an object with only an `id` key
* @prop {Number} answerID The ID of the answer
*/
this.emit("messagePollVoteRemove", message, user || { id: packet.d.user_id }, packet.d.answer_id);
break;
}
case "GUILD_AUDIT_LOG_ENTRY_CREATE": {
/**
* Fired when a guild audit log entry is created
* @event Client#guildAuditLogEntryCreate
* @prop {GuildAuditLogEntry} guildAuditLogEntry The created guild audit log entry
*/
this.emit("guildAuditLogEntryCreate", new GuildAuditLogEntry(packet.d, this.client));
break;
}
case "GUILD_MEMBER_ADD": {
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) { // Eventual Consistency™ (╯°□°)╯︵ ┻━┻
this.emit("debug", `Missing guild ${packet.d.guild_id} in GUILD_MEMBER_ADD`);
break;
}
packet.d.id = packet.d.user.id;
++guild.memberCount;
/**
* Fired when a member joins a server
* @event Client#guildMemberAdd
* @prop {Guild} guild The guild
* @prop {Member} member The member
*/
this.emit("guildMemberAdd", guild, guild.members.add(packet.d, guild));
break;
}
case "GUILD_MEMBER_UPDATE": {
// Check for member update if guildPresences intent isn't set, to prevent emitting twice
if (!(this.client.options.intents & Constants.Intents.guildPresences) && packet.d.user.username !== undefined) {
let user = this.client.users.get(packet.d.user.id);
let oldUser = null;
if (user && (user.username !== packet.d.user.username || user.globalName !== packet.d.user.global_name || user.discriminator !== packet.d.user.discriminator || user.avatar !== packet.d.user.avatar || (packet.d.user.avatar_decoration_data && user.avatarDecorationData && user.avatarDecorationData.asset !== packet.d.user.avatar_decoration_data.asset))) {
oldUser = {
username: user.username,
globalName: user.globalName,
discriminator: user.discriminator,
avatar: user.avatar,
avatarDecorationData: user.avatarDecorationData,
};
}
if (!user || oldUser) {
user = this.client.users.update(packet.d.user, this.client);
this.emit("userUpdate", user, oldUser);
}
}
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("debug", `Missing guild ${packet.d.guild_id} in GUILD_MEMBER_UPDATE`);
break;
}
let member = guild.members.get(packet.d.id = packet.d.user.id);
let oldMember = null;
if (member) {
oldMember = {
avatar: member.avatar,
communicationDisabledUntil: member.communicationDisabledUntil,
flags: member.flags,
roles: member.roles,
nick: member.nick,
premiumSince: member.premiumSince,
pending: member.pending,
};
}
member = guild.members.update(packet.d, guild);
/**
* Fired when a member's guild avatar, roles or nickname are updated or they start boosting a server
* @event Client#guildMemberUpdate
* @prop {Guild} guild The guild
* @prop {Member} member The updated member
* @prop {Object?} oldMember The old member data, or null if the member wasn't cached
* @prop {String?} oldMember.avatar The hash of the member's guild avatar, or null if no guild avatar
* @prop {Number?} oldMember.communicationDisabledUntil Timestamp of previous timeout expiry. If `null`, the member was not timed out
* @prop {Number?} oldMember.flags The member's flags (see Constants). Defaults to 0
* @prop {String?} oldMember.nick The server nickname of the member
* @prop {Boolean?} oldMember.pending Whether the member has passed the guild's Membership Screening requirements
* @prop {Number?} oldMember.premiumSince Timestamp of when the member boosted the guild
* @prop {Array<String>} oldMember.roles An array of role IDs this member is a part of
*/
this.emit("guildMemberUpdate", guild, member, oldMember);
break;
}
case "GUILD_MEMBER_REMOVE": {
if (packet.d.user.id === this.client.user.id) { // The bot is probably leaving
break;
}
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
break;
}
--guild.memberCount;
packet.d.id = packet.d.user.id;
/**
* Fired when a member leaves a server
* @event Client#guildMemberRemove
* @prop {Guild} guild The guild
* @prop {Member | Object} member The member. If the member is not cached, this will be an object with `id` and `user` key
*/
this.emit("guildMemberRemove", guild, guild.members.remove(packet.d) || {
id: packet.d.id,
user: new User(packet.d.user, this.client),
});
break;
}
case "GUILD_CREATE": {
if (!packet.d.unavailable) {
const guild = this.createGuild(packet.d);
if (this.ready) {
if (this.client.unavailableGuilds.remove(packet.d)) {
/**
* Fired when a guild becomes available
* @event Client#guildAvailable
* @prop {Guild} guild The guild
*/
this.emit("guildAvailable", guild);
} else {
/**
* Fired when a guild is created. This happens when:
* - the client creates a guild
* - the client joins a guild
* @event Client#guildCreate
* @prop {Guild} guild The guild
*/
this.emit("guildCreate", guild);
}
} else {
this.client.unavailableGuilds.remove(packet.d);
this.restartGuildCreateTimeout();
}
} else {
this.client.guilds.remove(packet.d);
/**
* Fired when an unavailable guild is created
* @event Client#unavailableGuildCreate
* @prop {UnavailableGuild} guild The unavailable guild
*/
this.emit("unavailableGuildCreate", this.client.unavailableGuilds.add(packet.d, this.client));
}
break;
}
case "GUILD_UPDATE": {
const guild = this.client.guilds.get(packet.d.id);
if (!guild) {
this.emit("debug", `Guild ${packet.d.id} undefined in GUILD_UPDATE`);
break;
}
const oldGuild = {
afkChannelID: guild.afkChannelID,
afkTimeout: guild.afkTimeout,
autoRemoved: guild.autoRemoved,
banner: guild.banner,
defaultNotifications: guild.defaultNotifications,
description: guild.description,
discoverySplash: guild.discoverySplash,
emojiCount: guild.emojiCount,
emojis: guild.emojis,
explicitContentFilter: guild.explicitContentFilter,
features: guild.features,
icon: guild.icon,
keywords: guild.keywords,
large: guild.large,
maxMembers: guild.maxMembers,
maxVideoChannelUsers: guild.maxVideoChannelUsers,
mfaLevel: guild.mfaLevel,
name: guild.name,
nsfw: guild.nsfw,
nsfwLevel: guild.nsfwLevel,
ownerID: guild.ownerID,
preferredLocale: guild.preferredLocale,
premiumProgressBarEnabled: guild.premiumProgressBarEnabled,
premiumSubscriptionCount: guild.premiumSubscriptionCount,
premiumTier: guild.premiumTier,
primaryCategory: guild.primaryCategory,
primaryCategoryID: guild.primaryCategoryID,
publicUpdatesChannelID: guild.publicUpdatesChannelID,
rulesChannelID: guild.rulesChannelID,
splash: guild.splash,
stickers: guild.stickers,
systemChannelFlags: guild.systemChannelFlags,
systemChannelID: guild.systemChannelID,
vanityURL: guild.vanityURL,
verificationLevel: guild.verificationLevel,
welcomeScreen: guild.welcomeScreen && {
description: guild.welcomeScreen.description,
welcomeChannels: guild.welcomeScreen.welcomeChannels,
},
};
/**
* Fired when a guild is updated
* @event Client#guildUpdate
* @prop {Guild} guild The guild
* @prop {Object} oldGuild The old guild data
* @prop {String?} oldGuild.afkChannelID The ID of the AFK voice channel
* @prop {Number} oldGuild.afkTimeout The AFK timeout in seconds
* @prop {Boolean?} oldGuild.autoRemoved Whether the guild was automatically removed from Discovery
* @prop {String?} oldGuild.banner The hash of the guild banner image, or null if no splash (VIP only)
* @prop {Number} oldGuild.defaultNotifications The default notification settings for the guild. 0 is "All Messages", 1 is "Only @mentions"
* @prop {String?} oldGuild.description The description for the guild (VIP only)
* @prop {Number?} oldGuild.emojiCount The number of emojis in the guild
* @prop {Array<Object>} oldGuild.emojis An array of guild emojis
* @prop {Number} oldGuild.explicitContentFilter The explicit content filter level for the guild. 0 is off, 1 is on for people without roles, 2 is on for all
* @prop {Array<String>} oldGuild.features An array of guild features
* @prop {String?} oldGuild.icon The hash of the guild icon, or null if no icon
* @prop {Array<String>?} oldGuild.keywords The guild's discovery keywords
* @prop {Boolean} oldGuild.large Whether the guild is "large" by "some Discord standard"
* @prop {Number?} oldGuild.maxMembers The maximum number of members for this guild
* @prop {Number?} oldGuild.maxVideoChannelUsers The max number of users allowed in a video channel
* @prop {Number} oldGuild.mfaLevel The admin 2FA level for the guild. 0 is not required, 1 is required
* @prop {String} oldGuild.name The name of the guild
* @prop {Boolean} oldGuild.nsfw [DEPRECATED] Whether the guild is designated as NSFW by Discord
* @prop {Number} oldGuild.nsfwLevel The guild NSFW level designated by Discord
* @prop {String} oldGuild.ownerID The ID of the user that is the guild owner
* @prop {String} oldGuild.preferredLocale Preferred "COMMUNITY" guild language used in server discovery and notices from Discord
* @prop {Boolean} oldGuild.premiumProgressBarEnabled If the boost progress bar is enabled
* @prop {Number?} oldGuild.premiumSubscriptionCount The total number of users currently boosting this guild
* @prop {Number} oldGuild.premiumTier Nitro boost level of the guild
* @prop {Object?} oldGuild.primaryCategory The guild's primary discovery category
* @prop {Number?} oldGuild.primaryCategoryID The guild's primary discovery category ID
* @prop {String?} oldGuild.publicUpdatesChannelID ID of the guild's updates channel if the guild has "COMMUNITY" features
* @prop {String?} oldGuild.rulesChannelID The channel where "COMMUNITY" guilds display rules and/or guidelines
* @prop {String?} oldGuild.splash The hash of the guild splash image, or null if no splash (VIP only)
* @prop {Array<Object>?} stickers An array of guild sticker objects
* @prop {Number} oldGuild.systemChannelFlags the flags for the system channel
* @prop {String?} oldGuild.systemChannelID The ID of the default channel for system messages (built-in join messages and boost messages)
* @prop {String?} oldGuild.vanityURL The vanity URL of the guild (VIP only)
* @prop {Number} oldGuild.verificationLevel The guild verification level
* @prop {Object?} oldGuild.welcomeScreen The welcome screen of a Community guild, shown to new members
* @prop {Object} oldGuild.welcomeScreen.description The description in the welcome screen
* @prop {Array<Object>} oldGuild.welcomeScreen.welcomeChannels The list of channels in the welcome screens. Each channels have the following properties: `channelID`, `description`, `emojiID`, `emojiName`. `emojiID` and `emojiName` properties can be null.
*/
this.emit("guildUpdate", this.client.guilds.update(packet.d, this.client), oldGuild);
break;
}
case "GUILD_DELETE": {
const voiceConnection = this.client.voiceConnections.get(packet.d.id);
if (voiceConnection) {
if (voiceConnection.channelID) {
this.client.leaveVoiceChannel(voiceConnection.channelID);
} else {
this.client.voiceConnections.leave(packet.d.id);
}
}
delete this.client.guildShardMap[packet.d.id];
const guild = this.client.guilds.remove(packet.d);
if (guild) { // Discord sends GUILD_DELETE for guilds that were previously unavailable in READY
guild.channels.forEach((channel) => {
delete this.client.channelGuildMap[channel.id];
});
}
if (packet.d.unavailable) {
/**
* Fired when a guild becomes unavailable
* @event Client#guildUnavailable
* @prop {Guild} guild The guild
*/
this.emit("guildUnavailable", this.client.unavailableGuilds.add(packet.d, this.client));
} else {
/**
* Fired when a guild is deleted. This happens when:
* - the client left the guild
* - the client was kicked/banned from the guild
* - the guild was literally deleted
* @event Client#guildDelete
* @prop {Guild | Object} guild The guild. If the guild was not cached, it will be an object with an `id` key. No other property is guaranteed
*/
this.emit("guildDelete", guild || {
id: packet.d.id,
});
}
break;
}
case "GUILD_BAN_ADD": {
/**
* Fired when a user is banned from a guild
* @event Client#guildBanAdd
* @prop {Guild} guild The guild
* @prop {User} user The banned user
*/
this.emit("guildBanAdd", this.client.guilds.get(packet.d.guild_id), this.client.users.update(packet.d.user, this.client));
break;
}
case "GUILD_BAN_REMOVE": {
/**
* Fired when a user is unbanned from a guild
* @event Client#guildBanRemove
* @prop {Guild} guild The guild
* @prop {User} user The banned user
*/
this.emit("guildBanRemove", this.client.guilds.get(packet.d.guild_id), this.client.users.update(packet.d.user, this.client));
break;
}
case "GUILD_ROLE_CREATE": {
/**
* Fired when a guild role is created
* @event Client#guildRoleCreate
* @prop {Guild} guild The guild
* @prop {Role} role The role
*/
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("debug", `Missing guild ${packet.d.guild_id} in GUILD_ROLE_CREATE`);
break;
}
this.emit("guildRoleCreate", guild, guild.roles.add(packet.d.role, guild));
break;
}
case "GUILD_ROLE_UPDATE": {
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("debug", `Guild ${packet.d.guild_id} undefined in GUILD_ROLE_UPDATE`);
break;
}
const role = guild.roles.add(packet.d.role, guild);
if (!role) {
this.emit("debug", `Role ${packet.d.role} in guild ${packet.d.guild_id} undefined in GUILD_ROLE_UPDATE`);
break;
}
const oldRole = {
color: role.color,
flags: role.flags,
hoist: role.hoist,
icon: role.icon,
managed: role.managed,
mentionable: role.mentionable,
name: role.name,
permissions: role.permissions,
position: role.position,
tags: role.tags,
unicodeEmoji: role.unicodeEmoji,
};
/**
* Fired when a guild role is updated
* @event Client#guildRoleUpdate
* @prop {Guild} guild The guild
* @prop {Role} role The updated role
* @prop {Object} oldRole The old role data
* @prop {Number} oldRole.color The hex color of the role in base 10
* @prop {Number} oldRole.flags The flags of the role (see constants)
* @prop {Boolean} oldRole.hoist Whether users with the role are hoisted in the user list or not
* @prop {String?} oldRole.icon The hash of the role's icon, or null if no icon
* @prop {Boolean} oldRole.managed Whether a guild integration manages the role or not
* @prop {Boolean} oldRole.mentionable Whether the role is mentionable or not
* @prop {String} oldRole.name The name of the role
* @prop {Permission} oldRole.permissions The permissions number of the role
* @prop {Number} oldRole.position The position of the role
* @prop {Object?} oldRole.tags The tags of the role
* @prop {String?} oldRole.unicodeEmoji Unicode emoji for the role
*/
this.emit("guildRoleUpdate", guild, guild.roles.update(packet.d.role, guild), oldRole);
break;
}
case "GUILD_ROLE_DELETE": {
/**
* Fired when a guild role is deleted
* @event Client#guildRoleDelete
* @prop {Guild} guild The guild
* @prop {Role} role The role
*/
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("debug", `Missing guild ${packet.d.guild_id} in GUILD_ROLE_DELETE`);
break;
}
if (!guild.roles.has(packet.d.role_id)) {
this.emit("debug", `Missing role ${packet.d.role_id} in GUILD_ROLE_DELETE`);
break;
}
this.emit("guildRoleDelete", guild, guild.roles.remove({ id: packet.d.role_id }));
break;
}
case "INVITE_CREATE": {
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("debug", `Missing guild ${packet.d.guild_id} in INVITE_CREATE`);
break;
}
const channel = this.client.getChannel(packet.d.channel_id);
if (!channel) {
this.emit("debug", `Missing channel ${packet.d.channel_id} in INVITE_CREATE`);
break;
}
/**
* Fired when a guild invite is created
* @event Client#inviteCreate
* @prop {Guild} guild The guild this invite was created in.
* @prop {Invite} invite The invite that was created
*/
this.emit("inviteCreate", guild, new Invite({
...packet.d,
guild,
channel,
}, this.client));
break;
}
case "INVITE_DELETE": {
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("debug", `Missing guild ${packet.d.guild_id} in INVITE_DELETE`);
break;
}
const channel = this.client.getChannel(packet.d.channel_id);
if (!channel) {
this.emit("debug", `Missing channel ${packet.d.channel_id} in INVITE_DELETE`);
break;
}
/**
* Fired when a guild invite is deleted
* @event Client#inviteDelete
* @prop {Guild} guild The guild this invite was created in.
* @prop {Invite} invite The invite that was deleted
*/
this.emit("inviteDelete", guild, new Invite({
...packet.d,
guild,
channel,
}, this.client));
break;
}
case "CHANNEL_CREATE": {
const channel = Channel.from(packet.d, this.client);
if (packet.d.guild_id) {
if (!channel.guild) {
channel.guild = this.client.guilds.get(packet.d.guild_id);
if (!channel.guild) {
this.emit("debug", `Received CHANNEL_CREATE for channel in missing guild ${packet.d.guild_id}`);
break;
}
}
channel.guild.channels.add(channel, this.client);
this.client.channelGuildMap[packet.d.id] = packet.d.guild_id;
/**
* Fired when a channel is created
* @event Client#channelCreate
* @prop {CategoryChannel | ForumChannel | GuildChannel | NewsChannel | TextChannel | VoiceChannel} channel The channel
*/
this.emit("channelCreate", channel);
} else {
this.emit("warn", new Error("Unhandled CHANNEL_CREATE type: " + JSON.stringify(packet, null, 2)));
break;
}
break;
}
case "CHANNEL_UPDATE": {
let channel = this.client.getChannel(packet.d.id);
if (!channel) {
break;
}
let oldChannel;
if (channel instanceof GuildChannel) {
oldChannel = {
availableTags: channel.availableTags,
bitrate: channel.bitrate,
defaultAutoArchiveDuration: channel.defaultAutoArchiveDuration,
defaultForumLayout: channel.defaultForumLayout,
defaultReactionEmoji: channel.defaultReactionEmoji,
defaultSortOrder: channel.defaultSortOrder,
defaultThreadRateLimitPerUser: channel.defaultThreadRateLimitPerUser,
flags: channel.flags,
name: channel.name,
nsfw: channel.nsfw,
parentID: channel.parentID,
permissionOverwrites: channel.permissionOverwrites,
position: channel.position,
rateLimitPerUser: channel.rateLimitPerUser,
rtcRegion: channel.rtcRegion,
topic: channel.topic,
type: channel.type,
userLimit: channel.userLimit,
videoQualityMode: channel.videoQualityMode,
};
} else {
this.emit("warn", `Unexpected CHANNEL_UPDATE for channel ${packet.d.id} with type ${oldType}`);
}
const oldType = channel.type;
if (oldType === packet.d.type) {
channel.update(packet.d);
} else {
this.emit("debug", `Channel ${packet.d.id} changed from type ${oldType} to ${packet.d.type}`);
const newChannel = Channel.from(packet.d, this.client);
if (packet.d.guild_id) {
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("debug", `Received CHANNEL_UPDATE for channel in missing guild ${packet.d.guild_id}`);
break;
}
guild.channels.remove(channel);
guild.channels.add(newChannel, this.client);
} else if (channel instanceof DMChannel) {
this.client.dmChannels.remove(channel);
this.client.dmChannels.add(newChannel, this.client);
} else {
this.emit("warn", new Error("Unhandled CHANNEL_UPDATE type: " + JSON.stringify(packet, null, 2)));
break;
}
channel = newChannel;
}
/**
* Fired when a channel is updated
* @event Client#channelUpdate
* @prop {CategoryChannel | DMChannel | ForumChannel | GuildChannel | TextChannel | VoiceChannel | NewsChannel} channel The updated channel
* @prop {Object} oldChannel The old channel data
* @prop {Array<Object>} oldChannel.availableTags The available tags that can be applied to threads in a forum/media channel. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#forum-tag-object) for object structure (forum/media channels only, max 20)
* @prop {Number} oldChannel.bitrate The bitrate of the channel (voice channels only)
* @prop {Number} oldChannel.defaultAutoArchiveDuration The default duration of newly created threads in minutes to automatically archive the thread after inactivity (60, 1440, 4320, 10080) (text/news/forum/media channels only)
* @prop {Number} oldChannel.defaultForumLayout The default forum layout type used to display posts in forum channels (forum channels only)
* @prop {Object} oldChannel.defaultReactionEmoji The emoji to show in the add reaction button on a thread in a forum/media channel (forum/media channels only)
* @prop {Number} oldChannel.defaultSortOrder The default sort order type used to order posts in forum/media channels (forum/media channels only)
* @prop {Number} oldChannel.defaultThreadRateLimitPerUser The initial rateLimitPerUser to set on newly created threads in a channel (text/forum/media channels only)
* @prop {Number?} oldChannel.flags The flags for the channel combined as a bitfield (thread/forum/media channels only)
* @prop {String} oldChannel.name The name of the channel
* @prop {Boolean} oldChannel.nsfw Whether the channel is NSFW or not (text channels only)
* @prop {String?} oldChannel.parentID The ID of the category this channel belongs to (guild channels only)
* @prop {Collection} oldChannel.permissionOverwrites Collection of PermissionOverwrites in this channel (guild channels only)
* @prop {Number} oldChannel.position The position of the channel (guild channels only)
* @prop {Number?} oldChannel.rateLimitPerUser The time in seconds a user has to wait before sending another message (0-21600) (text/voice/stage/forum/media channels only)
* @prop {String?} oldChannel.rtcRegion The RTC region ID of the channel (automatic when `null`) (voice channels only)
* @prop {String?} oldChannel.topic The topic of the channel (text channels only)
* @prop {Number} oldChannel.type The type of the old channel (text/news channels only)
* @prop {Number?} oldChannel.userLimit The max number of users that can join the channel (voice channels only)
* @prop {Number?} oldChannel.videoQualityMode The camera video quality mode of the channel (voice channels only)
*/
this.emit("channelUpdate", channel, oldChannel);
break;
}
case "CHANNEL_DELETE": {
if (packet.d.type === Constants.ChannelTypes.DM || packet.d.type === undefined) {
if (this.id === 0) {
const channel = this.client.dmChannels.remove(packet.d);
if (channel) {
delete this.client.dmChannelMap[channel.recipient.id];
/**
* Fired when a channel is deleted
* @event Client#channelDelete
* @prop {CategoryChannel | DMChannel | ForumChannel | NewsChannel | TextChannel | VoiceChannel} channel The channel
*/
this.emit("channelDelete", channel);
}
}
} else if (packet.d.guild_id) {
delete this.client.channelGuildMap[packet.d.id];
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("debug", `Missing guild ${packet.d.guild_id} in CHANNEL_DELETE`);
break;
}
const channel = guild.channels.remove(packet.d);
if (!channel) {
break;
}
if (channel.type === Constants.ChannelTypes.GUILD_VOICE || channel.type === Constants.ChannelTypes.GUILD_STAGE_VOICE) {
channel.voiceMembers.forEach((member) => {
channel.voiceMembers.remove(member);
this.emit("voiceChannelLeave", member, channel);
});
}
this.emit("channelDelete", channel);
} else {
this.emit("warn", new Error("Unhandled CHANNEL_DELETE type: " + JSON.stringify(packet, null, 2)));
}
break;
}
case "GUILD_MEMBERS_CHUNK": {
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("debug", `Received GUILD_MEMBERS_CHUNK, but guild ${packet.d.guild_id} is ` + (this.client.unavailableGuilds.has(packet.d.guild_id) ? "unavailable" : "missing"), this.id);
break;
}
const members = packet.d.members.map((member) => {
member.id = member.user.id;
return guild.members.add(member, guild);
});
if (packet.d.presences) {
packet.d.presences.forEach((presence) => {
const member = guild.members.get(presence.user.id);
if (member) {
member.update(presence);
}
});
}
if (this.requestMembersPromise.hasOwnProperty(packet.d.nonce)) {
this.requestMembersPromise[packet.d.nonce].members.push(...members);
}
if (packet.d.chunk_index >= packet.d.chunk_count - 1) {
if (this.requestMembersPromise.hasOwnProperty(packet.d.nonce)) {
clearTimeout(this.requestMembersPromise[packet.d.nonce].timeout);
this.requestMembersPromise[packet.d.nonce].res(this.requestMembersPromise[packet.d.nonce].members);
delete this.requestMembersPromise[packet.d.nonce];
}
if (this.getAllUsersCount.hasOwnProperty(guild.id)) {
delete this.getAllUsersCount[guild.id];
this.checkReady();
}
}
/**
* Fired when Discord sends member chunks
* @event Client#guildMemberChunk
* @prop {Guild} guild The guild the chunked members are in
* @prop {Array<Member>} members The members in the chunk
*/
this.emit("guildMemberChunk", guild, members);
this.lastHeartbeatAck = true;
break;
}
case "RESUMED":
case "READY": {
this.connectAttempts = 0;
this.reconnectInterval = 1000;
this.connecting = false;
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
}
this.connectTimeout = null;
this.status = "ready";
this.presence.status = "online";
this.client.shards._readyPacketCB(this.id);
if (packet.t === "RESUMED") {
// Can only heartbeat after resume succeeds, discord/discord-api-docs#1619
this.heartbeat();
this.preReady = true;
this.ready = true;
/**
* Fired when a shard finishes resuming
* @event Shard#resume
*/
super.emit("resume");
break;
} else {
this.resumeURL = `${packet.d.resume_gateway_url}?v=${Constants.GATEWAY_VERSION}&encoding=${Erlpack ? "etf" : "json"}`;
if (this.client.options.compress) {
this.resumeURL += "&compress=zlib-stream";
}
}
this.client.user = this.client.users.update(new ExtendedUser(packet.d.user, this.client), this.client);
if (this.client.user.bot) {
this.client.bot = true;
if (!this.client._token.startsWith("Bot ")) {
this.client._token = "Bot " + this.client._token;
}
} else {
this.emit("warn", new Error("User accounts are against Discord Terms of Service and not supported"));
}
if (packet.d._trace) {
this.discordServerTrace = packet.d._trace;
}
this.sessionID = packet.d.session_id;
packet.d.guilds.forEach((guild) => {
if (guild.unavailable) {
this.client.guilds.remove(guild);
this.client.unavailableGuilds.add(guild, this.client, true);
} else {
this.client.unavailableGuilds.remove(this.createGuild(guild));
}
});
packet.d.private_channels.forEach((channel) => {
if (channel.type === undefined || channel.type === Constants.ChannelTypes.DM) {
this.client.dmChannelMap[channel.recipients[0].id] = channel.id;
this.client.dmChannels.add(channel, this.client, true);
} else {
this.emit("warn", new Error("Unhandled READY private_channel type: " + JSON.stringify(channel, null, 2)));
}
});
this.client.application = packet.d.application;
this.preReady = true;
/**
* Fired when a shard finishes processing the ready packet
* @event Client#shardPreReady
* @prop {Number} id The ID of the shard
*/
this.emit("shardPreReady", this.id);
if (this.client.unavailableGuilds.size > 0 && packet.d.guilds.length > 0) {
this.restartGuildCreateTimeout();
} else {
this.checkReady();
}
break;
}
case "VOICE_SERVER_UPDATE": {
packet.d.session_id = this.sessionID;
packet.d.user_id = this.client.user.id;
packet.d.shard = this;
this.client.voiceConnections.voiceServerUpdate(packet.d);
break;
}
case "USER_UPDATE": {
let user = this.client.users.get(packet.d.id);
let oldUser = null;
if (user) {
oldUser = {
username: user.username,
discriminator: user.discriminator,
avatar: user.avatar,
avatarDecorationData: user.avatarDecorationData,
};
}
user = this.client.users.update(packet.d, this.client);
this.emit("userUpdate", user, oldUser);
break;
}
case "GUILD_EMOJIS_UPDATE": {
const guild = this.client.guilds.get(packet.d.guild_id);
let oldEmojis = null;
let emojis = packet.d.emojis;
if (guild) {
oldEmojis = guild.emojis;
guild.update(packet.d);
emojis = guild.emojis;
}
/**
* Fired when a guild's emojis are updated
* @event Client#guildEmojisUpdate
* @prop {Guild} guild The guild. If the guild is uncached, this is an object with an ID key. No other property is guaranteed
* @prop {Array} emojis The updated emojis of the guild
* @prop {Array?} oldEmojis The old emojis of the guild. If the guild is uncached, this will be null
*/
this.emit("guildEmojisUpdate", guild || { id: packet.d.guild_id }, emojis, oldEmojis);
break;
}
case "GUILD_STICKERS_UPDATE": {
const guild = this.client.guilds.get(packet.d.guild_id);
let oldStickers = null;
let stickers = packet.d.stickers;
if (guild) {
oldStickers = guild.stickers;
guild.update(packet.d);
stickers = guild.stickers;
}
/**
* Fired when a guild's stickers are updated
* @event Client#guildStickersUpdate
* @prop {Guild} guild The guild. If the guild is uncached, this is an object with an ID key. No other property is guaranteed
* @prop {Array} stickers The updated stickers of the guild
* @prop {Array?} oldStickers The old stickers of the guild. If the guild is uncached, this will be null
*/
this.emit("guildStickersUpdate", guild || { id: packet.d.guild_id }, stickers, oldStickers);
break;
}
case "CHANNEL_PINS_UPDATE": {
const channel = this.client.getChannel(packet.d.channel_id);
if (!channel) {
this.emit("debug", `CHANNEL_PINS_UPDATE target channel ${packet.d.channel_id} not found`);
break;
}
const oldTimestamp = channel.lastPinTimestamp;
channel.lastPinTimestamp = Date.parse(packet.d.last_pin_timestamp);
/**
* Fired when a channel pin timestamp is updated
* @event Client#channelPinUpdate
* @prop {DMChannel | TextChannel | NewsChannel} channel The channel
* @prop {Number} timestamp The new timestamp
* @prop {Number} oldTimestamp The old timestamp
*/
this.emit("channelPinUpdate", channel, channel.lastPinTimestamp, oldTimestamp);
break;
}
case "WEBHOOKS_UPDATE": {
/**
* Fired when a channel's webhooks are updated
* @event Client#webhooksUpdate
* @prop {Object} data The update data
* @prop {String} data.channelID The ID of the channel that webhooks were updated in
* @prop {String} data.guildID The ID of the guild that webhooks were updated in
*/
this.emit("webhooksUpdate", {
channelID: packet.d.channel_id,
guildID: packet.d.guild_id,
});
break;
}
case "THREAD_CREATE": {
const channel = Channel.from(packet.d, this.client);
if (!channel.guild) {
channel.guild = this.client.guilds.get(packet.d.guild_id);
if (!channel.guild) {
this.emit("debug", `Received THREAD_CREATE for channel in missing guild ${packet.d.guild_id}`);
break;
}
}
channel.guild.threads.add(channel, this.client);
this.client.threadGuildMap[packet.d.id] = packet.d.guild_id;
const parent = channel.guild.channels.get(channel.parentID);
if (parent instanceof ForumChannel) {
parent.lastMessageID = channel.id;
}
/**
* Fired when a channel is created
* @event Client#threadCreate
* @prop {NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel} channel The channel
*/
this.emit("threadCreate", channel);
break;
}
case "THREAD_UPDATE": {
const channel = this.client.getChannel(packet.d.id);
if (!channel) {
const thread = Channel.from(packet.d, this.client);
this.emit("threadUpdate", this.client.guilds.get(packet.d.guild_id).threads.add(thread, this.client), null);
this.client.threadGuildMap[packet.d.id] = packet.d.guild_id;
break;
}
if (!(channel instanceof ThreadChannel)) {
this.emit("warn", `Unexpected THREAD_UPDATE for channel ${packet.d.id} with type ${channel.type}`);
break;
}
const oldChannel = {
appliedTags: channel.appliedTags,
autoArchiveDuration: channel.autoArchiveDuration,
flags: channel.flags,
name: channel.name,
rateLimitPerUser: channel.rateLimitPerUser,
threadMetadata: channel.threadMetadata,
};
channel.update(packet.d);
/**
* Fired when a thread channel is updated
* @event Client#threadUpdate
* @prop {NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel} channel The updated channel
* @prop {Object?} oldChannel The old thread channel. This will be null if the channel was uncached
* @prop {Array<String>?} oldChannel.appliedTags The IDs of the set of tags that have been applied to a thread in a forum/media channel
* @prop {Number} oldChannel.autoArchiveDuration The duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080
* @prop {Number} oldChannel.flags The flags for the channel combined as a bitfield
* @prop {String} oldChannel.name The name of the channel
* @prop {Number} oldChannel.rateLimitPerUser The time in seconds a user has to wait before sending another message (0-21600)
* @prop {Object} oldChannel.threadMetadata Metadata for the thread
* @prop {Boolean} oldChannel.threadMetadata.archived Whether the thread is archived
* @prop {Number} oldChannel.threadMetadata.archiveTimestamp Timestamp when the thread's archive status was last changed, used for calculating recent activity
* @prop {Number} oldChannel.threadMetadata.autoArchiveDuration Duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080
* @prop {Boolean?} oldChannel.threadMetadata.locked Whether the thread is locked
*/
this.emit("threadUpdate", channel, oldChannel);
break;
}
case "THREAD_DELETE": {
delete this.client.threadGuildMap[packet.d.id];
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("debug", `Missing guild ${packet.d.guild_id} in THREAD_DELETE`);
break;
}
const channel = guild.threads.remove(packet.d);
if (!channel) {
break;
}
/**
* Fired when a thread channel is deleted
* @event Client#threadDelete
* @prop {NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel} channel The channel
*/
this.emit("threadDelete", channel);
break;
}
case "THREAD_LIST_SYNC": {
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("debug", `Missing guild ${packet.d.guild_id} in THREAD_LIST_SYNC`);
break;
}
const deletedThreads = (packet.d.channel_ids || guild.threads.map((c) => c.id)) // REVIEW Is this a good name?
.filter((c) => !packet.d.threads.some((t) => t.id === c)).map((id) => guild.threads.remove({ id }) || { id });
const activeThreads = packet.d.threads.map((t) => guild.threads.update(t, this.client));
const joinedThreadsMember = packet.d.members.map((m) => guild.threads.get(m.id).members.update(m, this.client));
/**
* Fired when the current user gains access to a channel
* @event Client#threadListSync
* @prop {Guild} guild The guild where threads are being synced
* @prop {Array<NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel | Object>} deletedThreads An array of synced threads that the current user no longer has access to. If a thread channel is uncached, it will be an object with an `id` key. No other property is guaranteed
* @prop {Array<NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel>} activeThreads An array of synced active threads that the current user can access
* @prop {Array<ThreadMember>} joinedThreadsMember An array of thread member objects where the current user has been added in a synced thread channel
*/
this.emit("threadListSync", guild, deletedThreads, activeThreads, joinedThreadsMember);
break;
}
case "THREAD_MEMBER_UPDATE": {
const channel = this.client.getChannel(packet.d.id);
if (!channel) {
this.emit("debug", `Missing channel ${packet.d.id} in THREAD_MEMBER_UPDATE`);
break;
}
let oldMember = null;
// Thanks Discord
packet.d.thread_id = packet.d.id;
let member = channel.members.get((packet.d.id = packet.d.user_id));
if (member) {
oldMember = {
flags: member.flags,
};
}
member = channel.members.update(packet.d, this.client);
/**
* Fired when a thread member is updated
* @event Client#threadMemberUpdate
* @prop {NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel} channel The channel
* @prop {ThreadMember} member The updated thread member
* @prop {Object} oldMember The old thread member data
* @prop {Number} oldMember.flags User thread settings
*/
this.emit("threadMemberUpdate", channel, member, oldMember);
break;
}
case "THREAD_MEMBERS_UPDATE": {
const channel = this.client.getChannel(packet.d.id);
if (!channel) {
this.emit("debug", `Missing channel ${packet.d.id} in THREAD_MEMBERS_UPDATE`);
break;
}
channel.update(packet.d);
let addedMembers;
let removedMembers;
if (packet.d.added_members) {
addedMembers = packet.d.added_members.map((m) => {
if (m.presence) {
m.presence.id = m.presence.user.id;
this.client.users.update(m.presence.user, this.client);
}
m.thread_id = m.id;
m.id = m.user_id;
m.member.id = m.member.user.id;
const guild = this.client.guilds.get(packet.d.guild_id);
if (guild) {
if (m.presence) {
guild.members.update(m.presence, guild);
}
guild.members.update(m.member, guild);
}
return channel.members.update(m, this.client);
});
}
if (packet.d.removed_member_ids) {
removedMembers = packet.d.removed_member_ids.map((id) => channel.members.remove({ id }) || { id });
}
/**
* Fired when anyone is added or removed from a thread. If the `guildMembers` intent is not specified, this will only apply for the current user
* @event Client#threadMembersUpdate
* @prop {NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel} channel The thread channel
* @prop {Array<ThreadMember>} addedMembers An array of members that were added to the thread channel
* @prop {Array<ThreadMember | Object>} removedMembers An array of members that were removed from the thread channel. If a member is uncached, it will be an object with an `id` key. No other property is guaranteed
*/
this.emit("threadMembersUpdate", channel, addedMembers || [], removedMembers || []);
break;
}
case "STAGE_INSTANCE_CREATE": {
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("debug", `Missing guild ${packet.d.guild_id} in STAGE_INSTANCE_CREATE`);
break;
}
/**
* Fired when a stage instance is created
* @event Client#stageInstanceCreate
* @prop {StageInstance} stageInstance The stage instance
*/
this.emit("stageInstanceCreate", guild.stageInstances.add(packet.d, this.client));
break;
}
case "STAGE_INSTANCE_UPDATE": {
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("stageInstanceUpdate", packet.d, null);
break;
}
const stageInstance = guild.stageInstances.get(packet.d.id);
let oldStageInstance = null;
if (stageInstance) {
oldStageInstance = {
discoverableDisabled: stageInstance.discoverableDisabled,
privacyLevel: stageInstance.privacyLevel,
topic: stageInstance.topic,
};
}
/**
* Fired when a stage instance is updated
* @event Client#stageInstanceUpdate
* @prop {StageInstance} stageInstance The stage instance
* @prop {Object?} oldStageInstance The old stage instance. If the stage instance was cached, this will be an object with the properties below. Otherwise, it will be null
* @prop {Boolean} oldStageInstance.discoverableDisabled Whether or not stage discovery was disabled
* @prop {Number} oldStageInstance.privacyLevel The privacy level of the stage instance. 1 is public, 2 is guild only
* @prop {String} oldStageInstance.topic The stage instance topic
*/
this.emit("stageInstanceUpdate", guild.stageInstances.update(packet.d, this.client), oldStageInstance);
break;
}
case "STAGE_INSTANCE_DELETE": {
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("stageInstanceDelete", new StageInstance(packet.d, this.client));
break;
}
/**
* Fired when a stage instance is deleted
* @event Client#stageInstanceDelete
* @prop {StageInstance} stageInstance The deleted stage instance
*/
this.emit("stageInstanceDelete", guild.stageInstances.remove(packet.d) || new StageInstance(packet.d, this.client));
break;
}
case "GUILD_SCHEDULED_EVENT_CREATE": {
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("guildScheduledEventCreate", new GuildScheduledEvent(packet.d, this.client));
break;
}
/**
* Fired when a guild scheduled event is created
* @event Client#guildScheduledEventCreate
* @prop {GuildScheduledEvent} event The event
*/
this.emit("guildScheduledEventCreate", guild.events.add(packet.d, this.client));
break;
}
case "GUILD_SCHEDULED_EVENT_UPDATE": {
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("guildScheduledEventUpdate", new GuildScheduledEvent(packet.d, this.client), null);
break;
}
const event = guild.events.get(packet.d.id);
let oldEvent = null;
if (event) {
oldEvent = {
channel: event.channel,
description: event.description,
entityID: event.entityID,
entityMetadata: event.entityMetadata,
entityType: event.entityType,
image: event.image,
name: event.name,
privacyLevel: event.privacyLevel,
scheduledEndTime: event.scheduledEndTime,
scheduledStartTime: event.scheduledStartTime,
status: event.status,
};
}
/**
* Fired when a guild scheduled event is updated
* @event Client#guildScheduledEventUpdate
* @prop {GuildScheduledEvent} event The updated event
* @prop {Object?} oldEvent The old guild event data, or null if the event wasn't cached.
* @prop {(VoiceChannel | StageChannel | Object)?} oldEvent.channel The channel where the event is held
* @prop {String?} oldEvent.description The description of the event
* @prop {String?} oldEvent.entityID The Entity ID associated to the event
* @prop {Object?} oldEvent.entityMetadata Metadata for the event
* @prop {String?} oldEvent.entityMetadata.location Location of the event
* @prop {Number} oldEvent.entityType The event entity type
* @prop {String?} oldEvent.image The hash of the event's image
* @prop {String} oldEvent.name The name of the event
* @prop {Number} oldEvent.privacyLevel The privacy level of the event
* @prop {Number?} oldEvent.scheduledEndTime The time the event will start
* @prop {Number} oldEvent.scheduledStartTime The time the event will start
* @prop {Number} oldEvent.status The status of the guild scheduled event
*/
this.emit("guildScheduledEventUpdate", guild.events.update(packet.d, this.client), oldEvent);
break;
}
case "GUILD_SCHEDULED_EVENT_DELETE": {
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("guildScheduledEventDelete", new GuildScheduledEvent(packet.d, this.client));
break;
}
/**
* Fired when a guild scheduled event is deleted
* @event Client#guildScheduledEventDelete
* @prop {GuildScheduledEvent} event The event that was deleted.
*/
this.emit("guildScheduledEventDelete", guild.events.remove(packet.d) || new GuildScheduledEvent(packet.d, this.client));
break;
}
case "GUILD_SCHEDULED_EVENT_USER_ADD": {
const user = this.client.users.get(packet.d.user_id) || { id: packet.d.user_id };
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("guildScheduledEventUserAdd", { id: packet.d.guild_scheduled_event_id, guild: { id: packet.d.guild_id } }, user);
break;
}
const event = guild.events.get(packet.d.guild_scheduled_event_id);
if (event) {
++event.userCount;
}
/**
* Fired when an user has subscribed to a Guild Event.
* @event Client#guildScheduledEventUserAdd
* @prop {GuildScheduledEvent | Object} event The guild event that the user subscribed to. If the event is uncached, this will be an object with `id` and `guild` keys. No other property is guaranteed
* @prop {User | Object} user The user that subscribed to the Guild Event. If the user is uncached, this will be an object with an `id` key. No other property is guaranteed
*/
this.emit("guildScheduledEventUserAdd", event || { id: packet.d.guild_scheduled_event_id, guild: guild }, user);
break;
}
case "GUILD_SCHEDULED_EVENT_USER_REMOVE": {
const user = this.client.users.get(packet.d.user_id) || { id: packet.d.user_id };
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("guildScheduledEventUserRemove", { id: packet.d.guild_scheduled_event_id, guild: { id: packet.d.guild_id } }, user);
break;
}
const event = guild.events.get(packet.d.guild_scheduled_event_id);
if (event) {
--event.userCount;
}
/**
* Fired when an user has unsubscribed from a Guild Event.
* @event Client#guildScheduledEventUserRemove
* @prop {GuildScheduledEvent | string} event The guild event that the user unsubscribed to. This will be the guild event ID if the guild was uncached
* @prop {User | string} user The user that unsubscribed to the Guild Event. This will be the user ID if the user was uncached
*/
this.emit("guildScheduledEventUserRemove", event || { id: packet.d.guild_scheduled_event_id, guild: guild }, user);
break;
}
case "INTERACTION_CREATE": {
/**
* Fired when an interaction is created
* @event Client#interactionCreate
* @prop {PingInteraction | CommandInteraction | ComponentInteraction | AutocompleteInteraction | ModalSubmitInteraction | UnknownInteraction} interaction The Interaction that was created
*/
this.emit("interactionCreate", Interaction.from(packet.d, this.client));
break;
}
case "APPLICATION_COMMAND_PERMISSIONS_UPDATE": {
/**
* Fired when an application command's permissions are updated
* @event Client#applicationCommandPermissionsUpdate
* @prop {Object} guildApplicationCommandPermissions The updated command permissions
*/
this.emit("applicationCommandPermissionsUpdate", packet.d);
break;
}
case "GUILD_INTEGRATIONS_UPDATE": {
/**
* Fired when a guild integration is updated
* @event Client#guildIntegrationsUpdate
* @prop {Guild} guild The guild where the integration was updated. If the guild isn't cached, this will be an object with an `id` key. No other properties are guaranteed
*/
this.emit("guildIntegrationsUpdate", this.client.guilds.get(packet.d.guild_id) || { id: packet.d.guild_id });
break;
}
case "GUILD_SOUNDBOARD_SOUND_CREATE": {
packet.d.id = packet.d.sound_id;
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("guildSoundboardSoundCreate", new SoundboardSound(packet.d, this.client));
break;
}
/**
* Fired when a guild soundboard sound is created
* @event Client#guildSoundboardSoundCreate
* @prop {SoundboardSound} sound The created soundboard sound
*/
this.emit("guildSoundboardSoundCreate", guild.soundboardSounds.add(packet.d, this.client));
break;
}
case "GUILD_SOUNDBOARD_SOUND_DELETE": {
packet.d.id = packet.d.sound_id;
const guild = this.client.guilds.get(packet.d.guild_id);
/**
* Fired when a guild soundboard sound is deleted
* @event Client#guildSoundboardSoundDelete
* @prop {SoundboardSound} sound The deleted soundboard sound. If the soundboard sound isn't cached, this will be an object with `id` and `guild` keys. If the guild isn't cached, it will be an object with an `id` key. No other properties are guaranteed
*/
this.emit("guildSoundboardSoundDelete", (guild && guild.soundboardSounds.remove(packet.d)) || {
id: packet.d.id,
guild: guild || {
id: packet.d.guild_id,
},
});
break;
}
case "GUILD_SOUNDBOARD_SOUND_UPDATE": {
packet.d.id = packet.d.sound_id;
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("guildSoundboardSoundUpdate", new SoundboardSound(packet.d, this.client), null);
break;
}
const sound = guild.soundboardSounds.get(packet.d.id);
let oldSound = null;
if (sound) {
oldSound = {
name: sound.name,
volume: sound.volume,
emojiID: sound.emojiID,
emojiName: sound.emojiName,
available: sound.available,
};
}
/**
* Fired when a guild soundboard sound is updated
* @event Client#guildSoundboardSoundUpdate
* @prop {SoundboardSound} sound The updated soundboard sound
* @prop {Object} oldSound The old soundboard sound data, or null if not cached
* @prop {Boolean} oldSound.available Whether the soundboard sound was available or not
* @prop {String?} oldSound.emojiID The ID of the relating custom emoji
* @prop {String?} oldSound.emojiName The name of the relating default emoji
* @prop {String} oldSound.name The name of the soundboard sound
* @prop {Number} oldSound.volume The volume of the soundboard sound, between 0 and 1
*/
this.emit("guildSoundboardSoundUpdate", guild.soundboardSounds.update(packet.d, this.client), oldSound);
break;
}
case "GUILD_SOUNDBOARD_SOUNDS_UPDATE": {
packet.d.soundboard_sounds = packet.d.soundboard_sounds.map((sound) => {
sound.id = sound.sound_id;
return sound;
});
const guild = this.client.guilds.get(packet.d.guild_id);
if (!guild) {
this.emit("guildSoundboardSoundsUpdate", { id: packet.d.guild_id }, packet.d.soundboard_sounds.map((sound) => new SoundboardSound(sound, this.client)), null);
break;
}
const oldSounds = packet.d.soundboard_sounds.map((_sound) => {
const sound = guild.soundboardSounds.get(_sound.id);
if (!sound) {
return null;
}
return {
name: sound.name,
volume: sound.volume,
emojiID: sound.emojiID,
emojiName: sound.emojiName,
available: sound.available,
};
});
/**
* Fired when multiple guild soundboard sounds are updated
* @event Client#guildSoundboardSoundsUpdate
* @prop {Guild} guild The guild. If the guild is uncached, this is an object with an ID key. No other property is guaranteed
* @prop {Array<SoundboardSound>} sounds The updated soundboard sounds
* @prop {Array<Object>} oldSounds The old soundboard sounds data, or null if not cached
* @prop {Boolean} oldSounds[].available Whether the soundboard sound was available or not
* @prop {String?} oldSounds[].emojiID The ID of the relating custom emoji
* @prop {String?} oldSounds[].emojiName The name of the relating default emoji
* @prop {String} oldSounds[].name The name of the soundboard sound
* @prop {Number} oldSounds[].volume The volume of the soundboard sound, between 0 and 1
*/
this.emit("guildSoundboardSoundsUpdate", guild, packet.d.soundboard_sounds.map((sound) => guild.soundboardSounds.update(sound, this.client)), oldSounds);
break;
}
case "SOUNDBOARD_SOUNDS": {
const guild = this.client.guilds.get(packet.d.guild_id);
const sounds = packet.d.soundboard_sounds.map((sound) => {
sound.id = sound.sound_id;
const s = guild ? guild.soundboardSounds.update(sound, this.client) : new SoundboardSound(sound, this.client);
return s;
});
/**
* Fired when Discord sends a guild's soundboard sounds. Sent in response to `fetchSoundboardSounds()`
* @prop {Guild} guild The ID of the guild. If the guild is uncached, this will be an object with an `id` key. No other properties are guaranteed
* @prop {Array<SoundboardSound>} sounds The guild's soundboard sounds
*/
this.emit("soundboardSounds", guild || { id: packet.d.guild_id }, sounds);
this.lastHeartbeatAck = true;
for (const nonce in this.requestSoundboardSoundsPromise) {
if (packet.d.guild_id in this.requestSoundboardSoundsPromise[nonce].soundboardSounds) {
this.requestSoundboardSoundsPromise[nonce].soundboardSounds[packet.d.guild_id] = sounds;
if (Object.values(this.requestSoundboardSoundsPromise[nonce].soundboardSounds).every((v) => v !== undefined)) {
clearTimeout(this.requestSoundboardSoundsPromise[nonce].timeout);
this.requestSoundboardSoundsPromise[nonce].res(this.requestSoundboardSoundsPromise[nonce].soundboardSounds);
delete this.requestSoundboardSoundsPromise[nonce];
}
}
}
break;
}
default: {
/**
* Fired when the shard encounters an unknown packet
* @event Client#unknown
* @prop {Object} packet The unknown packet
* @prop {Number} id The ID of the shard
*/
this.emit("unknown", packet, this.id);
break;
}
}
}
_onWSClose(code, reason) {
reason = reason.toString();
this.emit("debug", "WS disconnected: " + JSON.stringify({
code: code,
reason: reason,
status: this.status,
}));
let err = !code || code === 1000 ? null : new Error(code + ": " + reason);
let reconnect = "auto";
if (code) {
this.emit("debug", `${code === 1000 ? "Clean" : "Unclean"} WS close: ${code}: ${reason}`, this.id);
if (code === 4001) {
err = new Error("Gateway received invalid OP code");
} else if (code === 4002) {
err = new Error("Gateway received invalid message");
} else if (code === 4003) {
err = new Error("Not authenticated");
this.sessionID = null;
this.resumeURL = null;
} else if (code === 4004) {
err = new Error("Authentication failed");
this.sessionID = null;
this.resumeURL = null;
reconnect = false;
this.emit("error", new Error(`Invalid token: ${this._token}`));
} else if (code === 4005) {
err = new Error("Already authenticated");
} else if (code === 4006 || code === 4009) {
err = new Error("Invalid session");
this.sessionID = null;
this.resumeURL = null;
} else if (code === 4007) {
err = new Error("Invalid sequence number: " + this.seq);
this.seq = 0;
} else if (code === 4008) {
err = new Error("Gateway connection was ratelimited");
} else if (code === 4010) {
err = new Error("Invalid shard key");
this.sessionID = null;
this.resumeURL = null;
reconnect = false;
} else if (code === 4011) {
err = new Error("Shard has too many guilds (>2500)");
this.sessionID = null;
this.resumeURL = null;
reconnect = false;
} else if (code === 4013) {
err = new Error("Invalid intents specified");
this.sessionID = null;
this.resumeURL = null;
reconnect = false;
} else if (code === 4014) {
err = new Error("Disallowed intents specified");
this.sessionID = null;
this.resumeURL = null;
reconnect = false;
} else if (code === 1006) {
err = new Error("Connection reset by peer");
} else if (code !== 1000 && reason) {
err = new Error(code + ": " + reason);
}
if (err) {
err.code = code;
}
} else {
this.emit("debug", "WS close: unknown code: " + reason, this.id);
}
this.disconnect({
reconnect,
}, err);
}
_onWSError(err) {
this.emit("error", err, this.id);
}
_onWSMessage(data) {
try {
if (data instanceof ArrayBuffer) {
if (this.client.options.compress || Erlpack) {
data = Buffer.from(data);
}
} else if (Array.isArray(data)) { // Fragmented messages
data = Buffer.concat(data); // Copyfull concat is slow, but no alternative
}
if (this.client.options.compress) {
if (data.length >= 4 && data.readUInt32BE(data.length - 4) === 0xFFFF) {
this._zlibSync.push(data, ZlibSync.Z_SYNC_FLUSH);
if (this._zlibSync.err) {
this.emit("error", new Error(`zlib error ${this._zlibSync.err}: ${this._zlibSync.msg}`));
return;
}
data = Buffer.from(this._zlibSync.result);
if (Erlpack) {
return this.onPacket(Erlpack.unpack(data));
} else {
return this.onPacket(JSON.parse(data.toString()));
}
} else {
this._zlibSync.push(data, false);
}
} else if (Erlpack) {
return this.onPacket(Erlpack.unpack(data));
} else {
return this.onPacket(JSON.parse(data.toString()));
}
} catch (err) {
this.emit("error", err, this.id);
}
}
_onWSOpen() {
this.status = "handshaking";
/**
* Fired when the shard establishes a connection
* @event Client#connect
* @prop {Number} id The ID of the shard
*/
this.emit("connect", this.id);
this.lastHeartbeatAck = true;
}
[util.inspect.custom]() {
return Base.prototype[util.inspect.custom].call(this);
}
toString() {
return Base.prototype.toString.call(this);
}
toJSON(props = []) {
return Base.prototype.toJSON.call(this, [
"connectAttempts",
"connecting",
"discordServerTrace",
"getAllUsersCount",
"getAllUsersLength",
"getAllUsersQueue",
"lastHeartbeatAck",
"lastHeartbeatReceived",
"lastHeartbeatSent",
"latency",
"preReady",
"ready",
"reconnectInterval",
"seq",
"sessionID",
"status",
...props,
]);
}
}
module.exports = Shard;