UNPKG

seyfert

Version:

The most advanced framework for discord bots

336 lines (335 loc) 13.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Shard = void 0; const node_zlib_1 = require("node:zlib"); const common_1 = require("../../common"); const types_1 = require("../../types"); const constants_1 = require("../constants"); const structures_1 = require("../structures"); const timeout_1 = require("../structures/timeout"); const basesocket_1 = require("./basesocket"); const shared_1 = require("./shared"); class Shard { id; logger; debugger; data = { resume_seq: null, }; websocket = null; connectTimeout = new timeout_1.ConnectTimeout(); heart = { interval: 30e3, ack: true, }; bucket; offlineSendQueue = []; pendingGuilds = new Set(); options; isReady = false; constructor(id, options) { this.id = id; this.options = (0, common_1.MergeOptions)({ properties: constants_1.properties, ratelimitOptions: { rateLimitResetInterval: 60_000, maxRequestsPerRateLimitTick: 120, }, }, options); this.logger = new common_1.Logger({ name: `[Shard #${id}]`, logLevel: common_1.LogLevels.Info, }); if (options.debugger) this.debugger = options.debugger; const safe = this.calculateSafeRequests(); this.bucket = new structures_1.DynamicBucket({ refillInterval: 6e4, limit: safe, debugger: options.debugger }); } get latency() { return this.heart.lastAck && this.heart.lastBeat ? this.heart.lastAck - this.heart.lastBeat : Number.POSITIVE_INFINITY; } get isOpen() { return this.websocket?.readyState === 1 /*WebSocket.open*/; } get gatewayURL() { return this.options.info.url; } get resumeGatewayURL() { return this.data.resume_gateway_url; } get currentGatewayURL() { const url = new URL(this.resumeGatewayURL ?? this.options.info.url); url.searchParams.set('v', '10'); return url.href; } ping() { if (!this.websocket) return Promise.resolve(Number.POSITIVE_INFINITY); return this.websocket.ping(); } async connect() { await this.connectTimeout.wait(); if (this.isOpen) { this.debugger?.debug(`[Shard #${this.id}] Attempted to connect while open`); return; } clearTimeout(this.heart.nodeInterval); this.debugger?.debug(`[Shard #${this.id}] Connecting to ${this.currentGatewayURL}`); // @ts-expect-error Use native websocket when using Bun // biome-ignore lint/correctness/noUndeclaredVariables: /\ this.websocket = new basesocket_1.BaseSocket(typeof Bun === 'undefined' ? 'ws' : 'bun', this.currentGatewayURL); this.websocket.onmessage = ({ data }) => { this.handleMessage(data); }; this.websocket.onclose = (event) => this.handleClosed(event); this.websocket.onerror = (event) => this.logger.error(event); this.websocket.onopen = () => { this.heart.ack = true; }; } async send(force, message) { this.debugger?.info(`[Shard #${this.id}] Sending: ${types_1.GatewayOpcodes[message.op]} ${JSON.stringify(message.d, (_, value) => { if (typeof value === 'string') return value.replaceAll(this.options.token, v => { const split = v.split('.'); return `${split[0]}.${'*'.repeat(split[1].length)}.${'*'.repeat(split[2].length)}`; }); return value; }, 1)}`); await this.checkOffline(force); await this.bucket.acquire(force); await this.checkOffline(force); this.websocket?.send(JSON.stringify(message)); } async identify() { await this.send(true, { op: types_1.GatewayOpcodes.Identify, d: { token: `Bot ${this.options.token}`, compress: this.options.compress, properties: this.options.properties, shard: [this.id, this.options.info.shards], intents: this.options.intents, presence: this.options.presence, }, }); } get resumable() { return !!(this.data.resume_gateway_url && this.data.session_id && this.data.resume_seq !== null); } async resume() { await this.send(true, { op: types_1.GatewayOpcodes.Resume, d: { seq: this.data.resume_seq, session_id: this.data.session_id, token: `Bot ${this.options.token}`, }, }); } heartbeat(requested) { this.debugger?.debug(`[Shard #${this.id}] Sending ${requested ? '' : 'un'}requested heartbeat (Ack=${this.heart.ack})`); if (!requested) { if (!this.heart.ack) { this.close(shared_1.ShardSocketCloseCodes.ZombiedConnection, 'Zombied connection'); return; } this.heart.ack = false; } this.heart.lastBeat = Date.now(); this.websocket.send(JSON.stringify({ op: types_1.GatewayOpcodes.Heartbeat, d: this.data.resume_seq ?? null, })); } disconnect() { this.debugger?.info(`[Shard #${this.id}] Disconnecting`); this.close(shared_1.ShardSocketCloseCodes.Shutdown, 'Shard down request'); } async reconnect() { this.debugger?.info(`[Shard #${this.id}] Reconnecting`); this.disconnect(); await this.connect(); } onpacket(packet) { if (packet.s !== null) { this.data.resume_seq = packet.s; } this.debugger?.debug(`[Shard #${this.id}]`, packet.t ? packet.t : types_1.GatewayOpcodes[packet.op], this.data.resume_seq); switch (packet.op) { case types_1.GatewayOpcodes.Hello: { clearInterval(this.heart.nodeInterval); this.heart.interval = packet.d.heartbeat_interval; this.heartbeat(false); this.heart.nodeInterval = setInterval(() => this.heartbeat(false), this.heart.interval); if (this.resumable) { return this.resume(); } return this.identify(); } case types_1.GatewayOpcodes.HeartbeatAck: { this.heart.ack = true; this.heart.lastAck = Date.now(); } break; case types_1.GatewayOpcodes.Heartbeat: this.heartbeat(true); break; case types_1.GatewayOpcodes.Reconnect: return this.reconnect(); case types_1.GatewayOpcodes.InvalidSession: { if (packet.d) { if (!this.resumable) { return this.logger.fatal('This is a completely unexpected error message.'); } return this.resume(); } this.data.resume_seq = 0; this.data.session_id = undefined; return this.identify(); } case types_1.GatewayOpcodes.Dispatch: { switch (packet.t) { case types_1.GatewayDispatchEvents.Resumed: { this.isReady = true; this.offlineSendQueue.map(resolve => resolve()); this.options.handlePayload(this.id, packet); } break; case types_1.GatewayDispatchEvents.Ready: { if ((0, common_1.hasIntent)(this.options.intents, 'Guilds')) { for (let i = 0; i < packet.d.guilds.length; i++) { this.pendingGuilds.add(packet.d.guilds.at(i).id); } } this.data.resume_gateway_url = packet.d.resume_gateway_url; this.data.session_id = packet.d.session_id; this.offlineSendQueue.map(resolve => resolve()); this.options.handlePayload(this.id, packet); if (this.pendingGuilds.size === 0) { this.isReady = true; this.options.handlePayload(this.id, { t: types_1.GatewayDispatchEvents.GuildsReady, op: packet.op, s: packet.s, }); } break; } case types_1.GatewayDispatchEvents.GuildCreate: case types_1.GatewayDispatchEvents.GuildDelete: if (this.pendingGuilds.delete(packet.d.id)) { packet.t = `RAW_${packet.t}`; this.options.handlePayload(this.id, packet); if (this.pendingGuilds.size === 0) { this.isReady = true; this.options.handlePayload(this.id, { t: types_1.GatewayDispatchEvents.GuildsReady, op: packet.op, s: packet.s, }); } } else { this.options.handlePayload(this.id, packet); } break; default: this.options.handlePayload(this.id, packet); break; } } break; } } async handleClosed(close) { this.isReady = false; clearInterval(this.heart.nodeInterval); this.logger.warn(`${shared_1.ShardSocketCloseCodes[close.code] ?? types_1.GatewayCloseCodes[close.code] ?? close.code} (${close.code})`, close.reason); switch (close.code) { case shared_1.ShardSocketCloseCodes.Shutdown: //Force disconnect, ignore break; case 1000: case types_1.GatewayCloseCodes.UnknownOpcode: case types_1.GatewayCloseCodes.InvalidSeq: case types_1.GatewayCloseCodes.SessionTimedOut: { this.data.resume_seq = 0; this.data.session_id = undefined; this.data.resume_gateway_url = undefined; await this.reconnect(); } break; case 1001: case 1006: case shared_1.ShardSocketCloseCodes.ZombiedConnection: case types_1.GatewayCloseCodes.UnknownError: case types_1.GatewayCloseCodes.DecodeError: case types_1.GatewayCloseCodes.NotAuthenticated: case types_1.GatewayCloseCodes.AlreadyAuthenticated: case types_1.GatewayCloseCodes.RateLimited: { this.logger.info('Trying to reconnect'); await this.reconnect(); } break; case types_1.GatewayCloseCodes.AuthenticationFailed: case types_1.GatewayCloseCodes.DisallowedIntents: case types_1.GatewayCloseCodes.InvalidAPIVersion: case types_1.GatewayCloseCodes.InvalidIntents: case types_1.GatewayCloseCodes.InvalidShard: case types_1.GatewayCloseCodes.ShardingRequired: this.logger.fatal('Cannot reconnect'); break; default: { this.logger.warn('Unknown close code, trying to reconnect anyways'); await this.reconnect(); } break; } } close(code, reason) { clearInterval(this.heart.nodeInterval); if (!this.isOpen) { return this.debugger?.warn(`[Shard #${this.id}] Is not open, reason:`, reason); } this.debugger?.debug(`[Shard #${this.id}] Called close with reason:`, reason); this.websocket?.close(code, reason); } handleMessage(data) { let packet; try { if (data instanceof Buffer) { data = (0, node_zlib_1.inflateSync)(data); } packet = JSON.parse(data); } catch (e) { this.logger.error(e); return; } return this.onpacket(packet); } checkOffline(force) { if (!this.isOpen) { return new Promise(resolve => this.offlineSendQueue[force ? 'unshift' : 'push'](resolve)); } return Promise.resolve(); } calculateSafeRequests() { const safeRequests = this.options.ratelimitOptions.maxRequestsPerRateLimitTick - Math.ceil(this.options.ratelimitOptions.rateLimitResetInterval / this.heart.interval) * 2; if (safeRequests < 0) { return 0; } return safeRequests; } } exports.Shard = Shard;