UNPKG

discord-user-bots

Version:

A library that allows you to use the full potential of Discords API to create and operate powerful user bots

348 lines (318 loc) 11.3 kB
/** * * ## OVERVIEW * * Defines the Gateway class. * Everything here handles all gateway/ws logic. * * ## WHEN CONTRIBUTING: * * Please read Discord's documentation on its gateway before contributing. * */ const Events = require("events"); const WebSocket = require("ws"); const ProxyHTTPS = require("https-proxy-agent").HttpsProxyAgent; const util = require("../auth/util.js"); const constructs = require("./constructs.js"); const def = require("./def.js"); class GatewayEvent { constructor(op, d, s, t) { this.opcode = op; this.data = d; this.sessionNonce = s; this.type = t; } static fromMsg(message) { return new GatewayEvent(message.op, message.d, message.s, message.t); } } class ChunkCompletion { constructor(init, onfinish) { this.data = init; this.onfinish = (isError) => { onfinish(this.data, isError); }; } } class GatewayHandler extends Events { constructor(client, proxy, config) { super(); this.client = client; this.url = `${config.wsurl}/?v=${config.api}&encoding=json`; this.origin = config.url; this.proxy = proxy; this.config = config; this.currentNonce = 0; this.jitter = 1; this.heartbeatTimer = 45000; // default heartbeat on documentation this.allowReconnection = true; this.lastSequence = null; this.hasConnected = false; this.heartbeatInterval = undefined; this.info = {}; this.chunkProcs = {}; } async ws_request(op, d) { if (this.ws?.readyState === WebSocket.OPEN) { const data = JSON.stringify({ op, d }); this.ws.send(data); // console.log(`Sending: ${data}`); } } identify() { this.ws_request(def.GatewayOpcodes.Identify, { token: this.client.token, capabilities: 16381, properties: { os: this.client.clientData.os, browser: this.client.clientData.browser, device: "", system_locale: "en-US", browser_user_agent: this.client.clientData.ua, browser_version: "118.0.0.0", os_version: "", referrer: "", referring_domain: "", referrer_current: "", referring_domain_current: "", release_channel: "stable", client_build_number: 250759, client_event_source: null, }, presence: { status: "online", since: 0, activities: [], afk: false, }, compress: false, client_state: { guild_versions: {}, highest_last_message_id: "0", read_state_version: 0, user_guild_settings_version: -1, user_settings_version: -1, private_channels_version: "0", api_code_version: 0, }, }); } update_voice_status() { this.ws_request(def.GatewayOpcodes.Voice_State_Update, { guild_id: null, channel_id: null, self_mute: true, self_deaf: false, self_video: false, flags: 2, }); } DM_focus(channel_id) { this.ws_request(def.GatewayOpcodes.DM_Focus, { channel_id: channel_id, }); } heartbeat() { this.ws_request(def.GatewayOpcodes.Heartbeat, this.lastSequence); } resume() { this.ws_request(def.GatewayOpcodes.Resume, { token: this.client.token, session_id: this.info.session_id, seq: this.lastSequence, }); } request_guild_members(options) { return new Promise((resolve) => { options = { ...constructs.RequestGuildMembers, ...options }; const nonce = (this.currentNonce++).toString(36); this.chunkProcs[nonce] = new ChunkCompletion( { members: [], presences: [] }, (data, isError) => { if (isError) resolve(undefined); else resolve(data); } ); util.removeNulls(options); this.ws_request(def.GatewayOpcodes.Request_Guild_Members, { ...options, nonce, }); }); } connectWS(shouldResume = false) { this.ws = new WebSocket(this.url, { origin: this.origin, agent: this.proxy ? new ProxyHTTPS(this.proxy.href) : undefined, }); this.initGateway(shouldResume); } stopEvents() { if (this.heartbeatInterval !== undefined) clearInterval(this.heartbeatInterval); this.heartbeatInterval = undefined; } disconnectWS(isGraceful = true) { this.stopEvents(); if (this.ws?.readyState == WebSocket.OPEN) { if (isGraceful) this.ws.close(); else this.ws.terminate(); } } initGateway(isResume) { if (isResume) { isResume = this.lastSequence !== null; } else { this.lastSequence = null; } this.ws.on("open", () => { // console.log("Connect success"); }); this.ws.on("message", (rawMessage) => { const message = GatewayEvent.fromMsg(JSON.parse(rawMessage)); // console.log(message); switch (message.opcode) { case def.GatewayOpcodes.Dispatch: { // Dispatch // Gateway event with info this.handleEvent(message); break; } case def.GatewayOpcodes.Heartbeat: { // Heartbeat // Forces a heartbeat check this.heartbeat(); break; } case def.GatewayOpcodes.Reconnect: { // Reconnect // Forces the client to reconnect (Probably server change) this.disconnectWS(); break; } case def.GatewayOpcodes.Invalid_Session: { // Invalid session // Forces a restart of the connection this.disconnectWS(); break; } case def.GatewayOpcodes.Hello: { // Hello // Sent after connection this.heartbeatTimer = message.data.heartbeat_interval; this.heartbeatInterval = setInterval(() => { this.heartbeat(); }, this.heartbeatTimer * this.jitter); this.heartbeat(); if (isResume) this.resume(); else this.identify(); this.update_voice_status(); break; } case def.GatewayOpcodes.Heartbeat_ACK: { // Heartbeat ACK // Ignore, no actions need to be taken break; } } }); this.ws.on("close", (code) => { this.stopEvents(); if (!this.allowReconnection) return; switch (code) { case 4000: // Unknown error case 4001: // Unknown opcode case 4002: // Decode error case 4005: // Already authenticated case 4008: // Rate limited case 4009: { // Session timed out // Reconnect with a resumed session this.connectWS(true); break; } case 4003: // Not authenticated case 4007: { // Invalid seq // Attempt to reconnect if a session already happened this.connectWS(true); break; } case 4004: // Authentication failed (cannot reconnect) case 4010: // Invalid shard (cannot reconnect) case 4011: // Sharding required (cannot reconnect) case 4012: // Invalid API version (cannot reconnect) case 4013: // Invalid intent(s) (cannot reconnect) case 4014: { // Disallowed intent(s) (cannot reconnect) // Error from programmer, don't reconnect this.emit("error", `Discord gateway send an error code of "${code}"`); break; } default: { // Undocumented error this.connectWS(true); break; } } }); } handleEvent(message) { this.lastSequence = message.sessionNonce; this.client.emit("dispatch", message); switch (message.type) { case "READY": { // Gateway res this.info = message.data; break; } case "READY_SUPPLEMENTAL": { // Extra splash screen info this.info.supplemental = message.data; if (!this.hasConnected) this.emit("ready"); this.hasConnected = true; this.heartbeat(); break; } case "MESSAGE_CREATE": { // First emit a generic message event this.client.emit("message", message.data); // Next emit a specialized event if (def.MessageTypes?.[message.data.type]?.length) this.client.emit( def.MessageTypes[message.data.type].toLowerCase(), message.data ); break; } /* case "MESSAGE_UPDATE": { this.client.emit(`update_${def.MessageTypes[message.data.type].toLowerCase()}`, message.data); break; } */ case "GUILD_MEMBERS_CHUNK": { let chunkData = this.chunkProcs[message.data.nonce]; if (message.data.not_found !== undefined) { chunkData.onfinish(true); break; } chunkData.data.members = chunkData.data.members.concat(message.data.members); if (message.data.presences) chunkData.data.presences = chunkData.data.presences.concat( message.data.presences ); if (message.data.chunk_count - 1 <= message.data.chunk_index) { chunkData.onfinish(false); } break; } default: { if (message?.type?.length) this.client.emit(message.type.toLowerCase(), message.data); break; } } } } module.exports = GatewayHandler;