seyfert
Version:
The most advanced framework for discord bots
336 lines (335 loc) • 13.6 kB
JavaScript
"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;