"use strict"; const util = require("util"); const Base = require("../structures/Base"); const Bucket = require("../util/Bucket"); const Call = require("../structures/Call"); const Channel = require("../structures/Channel"); const GroupChannel = require("../structures/GroupChannel"); const GuildChannel = require("../structures/GuildChannel"); const Message = require("../structures/Message"); const PrivateChannel = require("../structures/PrivateChannel"); const {GATEWAY_VERSION, GatewayOPCodes, ChannelTypes} = require("../Constants"); 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 WebSocket = typeof window !== "undefined" ? require("../util/BrowserWebSocket") : require("ws"); let EventEmitter; try { EventEmitter = require("eventemitter3"); } catch(err) { EventEmitter = require("events").EventEmitter; } let Erlpack; try { Erlpack = require("erlpack"); } catch(err) { // eslint-disable no-empty } let ZlibSync; try { ZlibSync = require("zlib-sync"); } catch(err) { try { ZlibSync = require("pako"); } catch(err) { // eslint-disable 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?} 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.guildSyncQueue.length > 0) { this.requestGuildSync(this.guildSyncQueue); this.guildSyncQueue = []; this.guildSyncQueueLength = 1; return; } if(this.unsyncedGuilds > 0) { return; } 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.bot === false) { ++this.unsyncedGuilds; this.syncGuild(guild.id); } 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 * @arg {Number} activities[].type The type of the activity. 0 is playing, 1 is streaming (Twitch only), 2 is listening, 3 is watching, 5 is competing in * @arg {String} [activities[].url] The URL of the activity */ 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) { if(activities.length > 0 && !activities[0].hasOwnProperty("type")) { activities[0].type = activities[0].url ? 1 : 0; } 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(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: 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(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 GatewayOPCodes.DISPATCH: { if(!this.client.options.disableEvents[packet.t]) { this.wsEvent(packet); } break; } case GatewayOPCodes.HEARTBEAT: { this.heartbeat(); break; } case GatewayOPCodes.INVALID_SESSION: { this.seq = 0; this.sessionID = null; this.resumeURL = null; this.emit("warn", "Invalid session, reidentifying!", this.id); this.identify(); break; } case GatewayOPCodes.RECONNECT: { this.emit("debug", "Reconnecting due to server request", this.id); this.disconnect({ reconnect: "auto" }); break; } case 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} 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; /* eslint-enable no-unreachable */ } case 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(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) }); } requestGuildSync(guildID) { this.sendWS(GatewayOPCodes.SYNC_GUILD, guildID); } 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.getAllUsersCount = {}; this.getAllUsersQueue = []; this.getAllUsersLength = 1; this.guildSyncQueue = []; this.guildSyncQueueLength = 1; this.unsyncedGuilds = 0; 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 && this.unsyncedGuilds === 0) { return this.checkReady(); } this.guildCreateTimeout = setTimeout(() => { this.checkReady(); }, this.client.options.guildCreateTimeout); } } resume() { this.status = "resuming"; this.sendWS(GatewayOPCodes.RESUME, { token: this._token, session_id: this.sessionID, seq: this.seq }); } sendStatusUpdate() { this.sendWS(GatewayOPCodes.PRESENCE_UPDATE, { activities: this.presence.activities, afk: !!this.presence.afk, // For push notifications since: this.presence.status === "idle" ? Date.now() : 0, 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 === GatewayOPCodes.PRESENCE_UPDATE) { ++waitFor; this.presenceUpdateBucket.queue(func, priority); } this.globalBucket.queue(func, priority); } } syncGuild(guildID) { if(this.guildSyncQueueLength + 3 + guildID.length > 4081) { // 4096 - "{\"op\":12,\"d\":[]}".length + 1 for lazy comma offset this.requestGuildSync(this.guildSyncQueue); this.guildSyncQueue = [guildID]; this.guildSyncQueueLength = 1 + guildID.length + 3; } else if(this.ready) { this.requestGuildSync([guildID]); } else { this.guildSyncQueue.push(guildID); this.guildSyncQueueLength += guildID.length + 3; } } wsEvent(packet) { switch(packet.t) { /* eslint-disable no-redeclare */ // (╯°□°)╯︵ ┻━┻ 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.discriminator !== packet.d.user.discriminator || user.avatar !== packet.d.user.avatar)) { oldUser = { username: user.username, discriminator: user.discriminator, avatar: user.avatar }; } if(!user || oldUser) { user = this.client.users.update(packet.d.user, this.client); /** * Fired when a user's avatar, discriminator 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.username The username of the user * @prop {String} oldUser.discriminator The discriminator of the user * @prop {String?} oldUser.avatar The hash of the user's avatar, or null if no avatar */ this.emit("userUpdate", user, oldUser); } } if(!packet.d.guild_id) { packet.d.id = packet.d.user.id; const relationship = this.client.relationships.get(packet.d.id); if(!relationship) { // Removing relationships break; } const oldPresence = { activities: relationship.activities, status: relationship.status }; /** * Fired when a guild member or relationship's status or game changes * @event Client#presenceUpdate * @prop {Member | Relationship} other The updated member or relationship * @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?} oldPresence.activities The member's current activities * @prop {Object?} oldPresence.clientStatus The member's per-client status * @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.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.status The other user's old status. Either "online", "idle", or "offline" */ this.emit("presenceUpdate", this.client.relationships.update(packet.d), oldPresence); break; } 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); 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; } if(packet.d.guild_id === undefined) { packet.d.id = packet.d.user_id; if(packet.d.channel_id === null) { let flag = false; for(const groupChannel of this.client.groupChannels) { const call = (groupChannel[1].call || groupChannel[1].lastCall); if(call && call.voiceStates.remove(packet.d)) { flag = true; break; } } if(!flag) { for(const privateChannel of this.client.privateChannels) { const call = (privateChannel[1].call || privateChannel[1].lastCall); if(call && call.voiceStates.remove(packet.d)) { flag = true; break; } } if(!flag) { this.emit("debug", new Error("VOICE_STATE_UPDATE for user leaving call not found")); break; } } } else { const channel = this.client.getChannel(packet.d.channel_id); if(!channel.call && !channel.lastCall) { this.emit("debug", new Error("VOICE_STATE_UPDATE for untracked call")); break; } (channel.call || channel.lastCall).voiceStates.update(packet.d); } break; } const guild = this.client.guilds.get(packet.d.guild_id); if(!guild) { break; } if(guild.pendingVoiceStates) { guild.pendingVoiceStates.push(packet.d); 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 === ChannelTypes.GUILD_VOICE || channel.type === 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 !== ChannelTypes.GUILD_VOICE && oldChannel.type !== 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 === ChannelTypes.GUILD_VOICE || newChannel.type === 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 {TextVoiceChannel | StageChannel} newChannel The new voice channel * @prop {TextVoiceChannel | 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 {TextVoiceChannel | 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 {TextVoiceChannel | 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 "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 {PrivateChannel | 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 PrivateChannel */ 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; /** * 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, 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} oldMessage.attachments Array of attachments * @prop {Array} oldMessage.channelMentions Array of mentions channels' ids. * @prop {String} oldMessage.content Message content * @prop {Number} oldMessage.editedTimestamp Timestamp of latest message edit * @prop {Array} 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} oldMessage.mentions Array of mentioned users' ids * @prop {Boolean} oldMessage.pinned Whether the message was pinned or not * @prop {Array} 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); /** * 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); /** * Fired when a bulk delete occurs * @event Client#messageDeleteBulk * @prop {Array | Array} 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; if(packet.d.user_id === this.client.user.id) { message.reactions[reaction].me = true; } } else { message.reactions[reaction] = { count: 1, me: packet.d.user_id === this.client.user.id }; } } 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 */ this.emit("messageReactionAdd", message, packet.d.emoji, member || {id: packet.d.user_id}); 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; if(reactionObj.count === 0) { delete message.reactions[reaction]; } else if(packet.d.user_id === this.client.user.id) { reactionObj.me = 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 */ this.emit("messageReactionRemove", message, packet.d.emoji, packet.d.user_id); 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 "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.discriminator !== packet.d.user.discriminator || user.avatar !== packet.d.user.avatar)) { oldUser = { username: user.username, discriminator: user.discriminator, avatar: user.avatar }; } 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, 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?} communicationDisabledUntil Timestamp of previous timeout expiry. If `null`, the member was not timed out * @prop {Array} oldMember.roles An array of role IDs this member is a part of * @prop {String?} oldMember.nick The server nickname of the member * @prop {Number?} oldMember.premiumSince Timestamp of when the member boosted the guild * @prop {Boolean?} oldMember.pending Whether the member has passed the guild's Membership Screening requirements */ 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 on the guild * @prop {Array} 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} oldGuild.features An array of guild features * @prop {String?} oldGuild.icon The hash of the guild icon, or null if no icon * @prop {Array?} 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 {Boolean} oldGuild.premiumProgressBarEnabled If the boost progress bar is enabled * @prop {String} oldGuild.preferredLocale Preferred "COMMUNITY" guild language used in server discovery and notices from Discord * @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?} 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} 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, 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 {Boolean} oldRole.hoist Whether users with this 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 this 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 {TextChannel | TextVoiceChannel | CategoryChannel | StoreChannel | NewsChannel | GuildChannel} 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 GroupChannel) { oldChannel = { name: channel.name, ownerID: channel.ownerID, icon: channel.icon }; } else if(channel instanceof GuildChannel) { oldChannel = { bitrate: channel.bitrate, 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 PrivateChannel) { if(channel instanceof GroupChannel) { this.client.groupChannels.remove(channel); this.client.groupChannels.add(newChannel, this.client); } else { this.client.privateChannels.remove(channel); this.client.privateChannels.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 {TextChannel | TextVoiceChannel | CategoryChannel | StoreChannel | NewsChannel | GuildChannel | PrivateChannel} channel The updated channel * @prop {Object} oldChannel The old channel data * @prop {Number} oldChannel.bitrate The bitrate of the channel (voice 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 ratelimit of the channel, in seconds. 0 means no ratelimit is enabled (text 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 === ChannelTypes.DM || packet.d.type === undefined) { if(this.id === 0) { const channel = this.client.privateChannels.remove(packet.d); if(channel) { delete this.client.privateChannelMap[channel.recipient.id]; /** * Fired when a channel is deleted * @event Client#channelDelete * @prop {PrivateChannel | TextChannel | NewsChannel | TextVoiceChannel | CategoryChannel} 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 === ChannelTypes.GUILD_VOICE || channel.type === ChannelTypes.GUILD_STAGE_VOICE) { channel.voiceMembers.forEach((member) => { channel.voiceMembers.remove(member); this.emit("voiceChannelLeave", member, channel); }); } this.emit("channelDelete", channel); } else if(packet.d.type === ChannelTypes.GROUP_DM) { if(this.id === 0) { this.emit("channelDelete", this.client.groupChannels.remove(packet.d)); } } else { this.emit("warn", new Error("Unhandled CHANNEL_DELETE type: " + JSON.stringify(packet, null, 2))); } break; } case "CALL_CREATE": { packet.d.id = packet.d.message_id; const channel = this.client.getChannel(packet.d.channel_id); if(channel.call) { channel.call.update(packet.d); } else { channel.call = new Call(packet.d, channel); let incrementedID = ""; let overflow = true; const chunks = packet.d.id.match(/\d{1,9}/g).map((chunk) => parseInt(chunk)); for(let i = chunks.length - 1; i >= 0; --i) { if(overflow) { ++chunks[i]; overflow = false; } if(chunks[i] > 999999999) { overflow = true; incrementedID = "000000000" + incrementedID; } else { incrementedID = chunks[i] + incrementedID; } } if(overflow) { incrementedID = overflow + incrementedID; } this.client.getMessages(channel.id, { limit: 1, before: incrementedID }).catch((err) => this.emit("error", err)); } /** * Fired when a call is created * @event Client#callCreate * @prop {Call} call The call */ this.emit("callCreate", channel.call); break; } case "CALL_UPDATE": { const channel = this.client.getChannel(packet.d.channel_id); if(!channel.call) { throw new Error("CALL_UPDATE but channel has no call"); } const oldCall = { endedTimestamp: channel.call.endedTimestamp, participants: channel.call.participants, region: channel.call.region, ringing: channel.call.ringing, unavailable: channel.call.unavailable }; /** * Fired when a call is updated * @event Client#callUpdate * @prop {Call} call The updated call * @prop {Object} oldCall The old call data * @prop {Number?} oldCall.endedTimestamp The timestamp of the call end * @prop {Array} oldCall.participants The IDs of the call participants * @prop {String?} oldCall.region The region of the call server * @prop {Array?} oldCall.ringing The IDs of people that were being rung * @prop {Boolean} oldCall.unavailable Whether the call was unavailable or not */ this.emit("callUpdate", channel.call.update(packet.d), oldCall); break; } case "CALL_DELETE": { const channel = this.client.getChannel(packet.d.channel_id); if(!channel.call) { throw new Error("CALL_DELETE but channel has no call"); } channel.lastCall = channel.call; channel.call = null; /** * Fired when a call is deleted * @event Client#callDelete * @prop {Call} call The call */ this.emit("callDelete", channel.lastCall); break; } case "CHANNEL_RECIPIENT_ADD": { const channel = this.client.groupChannels.get(packet.d.channel_id); /** * Fired when a user joins a group channel * @event Client#channelRecipientAdd * @prop {GroupChannel} channel The channel * @prop {User} user The user */ this.emit("channelRecipientAdd", channel, channel.recipients.add(this.client.users.update(packet.d.user, this.client))); break; } case "CHANNEL_RECIPIENT_REMOVE": { const channel = this.client.groupChannels.get(packet.d.channel_id); /** * Fired when a user leaves a group channel * @event Client#channelRecipientRemove * @prop {GroupChannel} channel The channel * @prop {User} user The user */ this.emit("channelRecipientRemove", channel, channel.recipients.remove(packet.d.user)); break; } case "FRIEND_SUGGESTION_CREATE": { /** * Fired when a client receives a friend suggestion * @event Client#friendSuggestionCreate * @prop {User} user The suggested user * @prop {Array} reasons Array of reasons why this suggestion was made * @prop {String} reasons.name Username of suggested user on that platform * @prop {String} reasons.platform_type Platform you share with the user * @prop {Number} reasons.type Type of reason? */ this.emit("friendSuggestionCreate", new User(packet.d.suggested_user, this.client), packet.d.reasons); break; } case "FRIEND_SUGGESTION_DELETE": { /** * Fired when a client's friend suggestion is removed for any reason * @event Client#friendSuggestionDelete * @prop {User} user The suggested user */ this.emit("friendSuggestionDelete", this.client.users.get(packet.d.suggested_user_id)); 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} members The members in the chunk */ this.emit("guildMemberChunk", guild, members); this.lastHeartbeatAck = true; break; } case "GUILD_SYNC": {// (╯°□°)╯︵ ┻━┻ thx Discord devs const guild = this.client.guilds.get(packet.d.id); for(const member of packet.d.members) { member.id = member.user.id; guild.members.add(member, guild); } for(const presence of packet.d.presences) { if(!guild.members.get(presence.user.id)) { let userData = this.client.users.get(presence.user.id); if(userData) { userData = `{username: ${userData.username}, id: ${userData.id}, discriminator: ${userData.discriminator}}`; } this.emit("debug", `Presence without member. ${presence.user.id}. In global user cache: ${userData}. ` + JSON.stringify(presence), this.id); continue; } presence.id = presence.user.id; guild.members.update(presence); } if(guild.pendingVoiceStates && guild.pendingVoiceStates.length > 0) { for(const voiceState of guild.pendingVoiceStates) { if(!guild.members.get(voiceState.user_id)) { continue; } voiceState.id = voiceState.user_id; const channel = guild.channels.get(voiceState.channel_id); if(channel) { channel.voiceMembers.add(guild.members.update(voiceState)); if(this.client.options.seedVoiceConnections && voiceState.id === this.client.user.id && !this.client.voiceConnections.get(channel.guild ? channel.guild.id : "call")) { this.client.joinVoiceChannel(channel.id); } } else { // Phantom voice states from connected users in deleted channels (╯°□°)╯︵ ┻━┻ this.client.emit("debug", "Phantom voice state received but channel not found | Guild: " + guild.id + " | Channel: " + voiceState.channel_id); } } } guild.pendingVoiceStates = null; --this.unsyncedGuilds; this.checkReady(); 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.client.bot = false; this.client.userGuildSettings = {}; if(packet.d.user_guild_settings) { packet.d.user_guild_settings.forEach((guildSettings) => { this.client.userGuildSettings[guildSettings.guild_id] = guildSettings; }); } this.client.userSettings = packet.d.user_settings; } 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 === ChannelTypes.DM) { this.client.privateChannelMap[channel.recipients[0].id] = channel.id; this.client.privateChannels.add(channel, this.client, true); } else if(channel.type === ChannelTypes.GROUP_DM) { this.client.groupChannels.add(channel, this.client, true); } else { this.emit("warn", new Error("Unhandled READY private_channel type: " + JSON.stringify(channel, null, 2))); } }); if(packet.d.relationships) { packet.d.relationships.forEach((relationship) => { this.client.relationships.add(relationship, this.client, true); }); } if(packet.d.presences) { packet.d.presences.forEach((presence) => { if(this.client.relationships.get(presence.user.id)) { // Avoid DM channel presences which are also in here presence.id = presence.user.id; this.client.relationships.update(presence, null, true); } }); } if(packet.d.notes) { this.client.notes = packet.d.notes; } 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 }; } user = this.client.users.update(packet.d, this.client); this.emit("userUpdate", user, oldUser); break; } case "RELATIONSHIP_ADD": { if(this.client.bot) { break; } const relationship = this.client.relationships.get(packet.d.id); if(relationship) { const oldRelationship = { type: relationship.type }; /** * Fired when a relationship is updated * @event Client#relationshipUpdate * @prop {Relationship} relationship The relationship * @prop {Object} oldRelationship The old relationship data * @prop {Number} oldRelationship.type The old type of the relationship */ this.emit("relationshipUpdate", this.client.relationships.update(packet.d), oldRelationship); } else { /** * Fired when a relationship is added * @event Client#relationshipAdd * @prop {Relationship} relationship The relationship */ this.emit("relationshipAdd", this.client.relationships.add(packet.d, this.client)); } break; } case "RELATIONSHIP_REMOVE": { if(this.client.bot) { break; } /** * Fired when a relationship is removed * @event Client#relationshipRemove * @prop {Relationship} relationship The relationship */ this.emit("relationshipRemove", this.client.relationships.remove(packet.d)); 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 {PrivateChannel | 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 "PRESENCES_REPLACE": { for(const presence of packet.d) { const guild = this.client.guilds.get(presence.guild_id); if(!guild) { this.emit("debug", "Rogue presences replace: " + JSON.stringify(presence), this.id); continue; } const member = guild.members.get(presence.user.id); if(!member && presence.user.username) { presence.id = presence.user.id; member.update(presence); } } break; } case "USER_NOTE_UPDATE": { if(packet.d.note) { this.client.notes[packet.d.id] = packet.d.note; } else { delete this.client.notes[packet.d.id]; } break; } case "USER_GUILD_SETTINGS_UPDATE": { this.client.userGuildSettings[packet.d.guild_id] = packet.d; 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; /** * 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 = { 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 {String} oldChannel.name The name of the channel * @prop {Number} oldChannel.rateLimitPerUser The ratelimit of the channel, in seconds. 0 means no ratelimit is enabled * @prop {Object} oldChannel.threadMetadata Metadata for the thread * @prop {Number} oldChannel.threadMetadata.archiveTimestamp Timestamp when the thread's archive status was last changed, used for calculating recent activity * @prop {Boolean} oldChannel.threadMetadata.archived Whether the thread is archived * @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} 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} activeThreads An array of synced active threads that the current user can access * @prop {Array} 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} addedMembers An array of members that were added to the thread channel * @prop {Array} 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, enitityMetadata: 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.enitityMetadata.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 "MESSAGE_ACK": // Ignore these case "GUILD_INTEGRATIONS_UPDATE": case "USER_SETTINGS_UPDATE": case "CHANNEL_PINS_ACK": { break; } case "INTERACTION_CREATE": { /** * Fired when an interaction is created * @event Client#interactionCreate * @prop {PingInteraction | CommandInteraction | ComponentInteraction | AutocompleteInteraction | UnknownInteraction} Interaction The Interaction that was created */ this.emit("interactionCreate", Interaction.from(packet.d, this.client)); 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; } } /* eslint-enable no-redeclare */ } _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, [ "connecting", "ready", "discordServerTrace", "status", "lastHeartbeatReceived", "lastHeartbeatSent", "latency", "preReady", "getAllUsersCount", "getAllUsersQueue", "getAllUsersLength", "guildSyncQueue", "guildSyncQueueLength", "unsyncedGuilds", "lastHeartbeatAck", "seq", "sessionID", "reconnectInterval", "connectAttempts", ...props ]); } } module.exports = Shard;