UNPKG

lilybird

Version:
215 lines (214 loc) 6.86 kB
import { setTimeout } from "node:timers/promises"; export class WebSocketManager { #debug; #options; #dispatch; #sequenceNumber = null; #isResuming = false; #ws; #gatewayInfo; #timer; #gotACK = true; resumeInfo = {}; constructor(options, dispatch, debug) { if (typeof options.intents !== "number" && Number.isNaN(options.intents)) throw new Error("Invalid intents"); this.#dispatch = dispatch; this.#debug = debug; this.#options = options; } init(token, dispatch) { this.#options.token = token; this.#dispatch = dispatch; } close() { this.#ws.close(3000); } async connect(url) { if (typeof this.#dispatch === "undefined") throw new Error("You need to pass the dispatch function via 'init' before connecting."); if (typeof this.#gatewayInfo === "undefined") { const response = await fetch("https://discord.com/api/v10/gateway/bot", { headers: { Authorization: `Bot ${this.#options.token}` } }); if (!response.ok) throw new Error("An invalid Token was provided"); const data = await response.json(); data.url = `${data.url}/?v=10&encoding=json`; this.#gatewayInfo = data; } this.#ws = new WebSocket(url ?? this.#gatewayInfo.url); this.#ws.addEventListener("error", (err) => { this.#debug?.(8, err); }); this.#ws.addEventListener("close", async ({ code }) => { this.#debug?.(12, code); this.#clearTimer(); if (code === 3000) return; if (code === 4010) throw new Error("Invalid Shard"); if (code === 4011) throw new Error("Sharding Required"); if (code === 4012) throw new Error("Invalid API Version"); if (code === 4013) throw new Error("Invalid intent(s)"); if (code === 4014) throw new Error("Disallowed intent(s)"); if (typeof code === "undefined" || code === 1001 || closeCodeAllowsReconnection(code)) { await this.#attemptResume(); return; } this.#isResuming = false; await this.connect(); }); this.#ws.addEventListener("message", (event) => { this.#debug?.(0, event.data); const payload = JSON.parse(event.data.toString()); if (typeof payload.s === "number") this.#sequenceNumber = payload.s; switch (payload.op) { case 0: { this.#dispatch(payload); break; } case 10: { const interval = this.#getInterval(payload.d.heartbeat_interval); this.#startTimer(interval); if (!this.#isResuming) this.#identify(); else this.#resume(); break; } case 1: { this.#debug?.(3); this.#sendHeartbeatPayload(); break; } case 7: { this.#debug?.(7); this.#ws.close(1001); break; } case 9: { this.#debug?.(6); if (payload.d) this.#ws.close(1001); else this.#ws.close(1000); break; } case 11: { this.#gotACK = true; this.#debug?.(2); break; } default: break; } return; }); } #getInterval(interval) { let res = 0; let i = 0; do { res = Math.round((interval * Math.random()) + i); i++; } while (res < interval / 2); return res; } #sendHeartbeatPayload() { this.#gotACK = false; this.#ws.send(`{ "op": 1, "d": ${this.#sequenceNumber}, "s": null, "t": null }`); } #identify() { if (typeof this.#options.token === "undefined") throw new Error("No token was found"); const payload = { op: 2, d: { token: this.#options.token, intents: this.#options.intents, properties: { os: process.platform, browser: "Lilybird", device: "Lilybird" }, presence: this.#options.presence }, s: null, t: null }; this.#debug?.(4); this.#ws.send(JSON.stringify(payload)); } #resume() { const payload = { op: 6, d: { token: this.#options.token, session_id: this.resumeInfo.id, seq: this.#sequenceNumber ?? 0 }, s: null, t: null }; this.#debug?.(5); this.#ws.send(JSON.stringify(payload)); } #startTimer(interval) { this.#gotACK = true; this.#timer = setInterval(async () => { if (!this.#gotACK) { this.#debug?.(9); await setTimeout(500); if (!this.#gotACK) { this.#debug?.(10); this.#ws.close(1001); return; } } this.#debug?.(1); this.#sendHeartbeatPayload(); }, interval); } #clearTimer() { if (typeof this.#timer === "undefined") return; clearInterval(this.#timer); } async #attemptResume() { this.#debug?.(11); this.#isResuming = true; await this.connect(`${this.resumeInfo.url}/?v=10&encoding=json`); } async ping() { return new Promise((res) => { this.#ws.addEventListener("pong", () => { res(performance.now() - start); }, { once: true }); const start = performance.now(); this.#ws.ping(); }); } updatePresence(presence) { const options = { op: 3, d: presence, s: null, t: null }; this.#ws.send(JSON.stringify(options)); } get options() { return this.#options; } } function closeCodeAllowsReconnection(code) { return code >= 4000 && code !== 4004 && code < 4010; }