UNPKG

ts-discord-wrapper

Version:
261 lines (226 loc) 11 kB
import {platform} from 'os'; import {client, connection} from 'websocket'; // Import the necessary types/interfaces from 'websocket'. import {Heartbeat} from './util/Heartbeat.ts'; import {TSDiscordWrapper} from '../TSDiscordWrapper.ts'; import {TSDiscordWrapperInfo} from '../TSDiscordWrapperInfo.ts'; import {get, OpCode} from './util/OpCode.ts'; import GateWayIntent from "../GateWayIntent.ts"; import {ReadyHandler} from "../events/handlers/ReadyHandler.ts"; import {CloseCode, CloseCodeInfo} from "./util/CloseCode.ts"; import {EventNames} from "./util/EventNames.ts"; import * as process from "process"; import {InteractionHandler} from "../events/handlers/InteractionHandler.ts"; /** * Handles the websocket connection to the Discord API. */ export default class TSDiscordWrapperWS { private wsClient = new client(); private token: string | null = null; private heartbeat: Heartbeat private readonly tsDiscordWrapper: TSDiscordWrapper private identifyRateLimit: boolean = false; private identifyTimeout: number = 0; private attemptedToResume: boolean = false; private alreadySentConnectMessageOnce: boolean = false; private gatewayIntents: GateWayIntent[] = []; //data needed private resumeUrl: string | null = null; private sessionId: string | null = null; private sequenceNumber: number | null = null; constructor(tsDiscordWrapper: TSDiscordWrapper) { this.tsDiscordWrapper = tsDiscordWrapper; this.heartbeat = new Heartbeat(this, tsDiscordWrapper); } async connect(token: string, intents: GateWayIntent[]) { this.token = token; this.gatewayIntents = intents; const url: string = (this.resumeUrl || TSDiscordWrapperInfo.DISCORD_GATEWAY_URL + TSDiscordWrapperInfo.DISCORD_GATEWAY_VERSION + TSDiscordWrapperInfo.JSON_ENCODING); this.wsClient.connect(url); this.wsClient.on('connectFailed', (error: any) => { this.tsDiscordWrapper.logger.error('Connect Error: ' + error.toString()); }); this.wsClient.on('connect', async (connection: connection) => { this.tsDiscordWrapper.logger.info('WebSocket Client Connected'); if (this.sessionId == null) { if (!this.alreadySentConnectMessageOnce) { this.tsDiscordWrapper.logger.info(` _____ _ _ _ _______ _____ _____ _ _ __ __ / ____| | | | | | | |__ __/ ____| | __ \\(_) | | \\ \\ / / | | ___ _ __ _ __ ___ ___| |_ ___ __| | | |_ ___ | | | (___ | | | |_ ___ ___ ___ _ __ __| | \\ \\ /\\ / / __ __ _ _ __ _ __ ___ _ __ | | / _ \\| '_ \\| '_ \\ / _ \\/ __| __/ _ \\/ _\` | | __/ _ \\ | | \\___ \\ | | | | / __|/ __/ _ \\| '__/ _\` | \\ \\/ \\/ / '__/ _\` | '_ \\| '_ \\ / _ \\ '__| | |___| (_) | | | | | | | __/ (__| || __/ (_| | | || (_) | | | ____) | | |__| | \\__ \\ (_| (_) | | | (_| | \\ /\\ /| | | (_| | |_) | |_) | __/ | \\_____\\___/|_| |_|_| |_|\\___|\\___|\\__\\___|\\__,_| \\__\\___/ |_| |_____/ |_____/|_|___/\\___\\___/|_| \\__,_| \\/ \\/ |_| \\__,_| .__/| .__/ \\___|_| | | | | |_| |_| `.trim()); this.alreadySentConnectMessageOnce = true; } else { this.tsDiscordWrapper.logger.info("Reconnected to TSDiscordWrapper"); } } else { this.tsDiscordWrapper.logger.info("Resuming session " + this.sessionId); } this.attemptedToResume = false; if (this.sessionId === null) { await this.identify(connection, intents); } else { await this.resume(connection); } connection.on('error', (error: any) => { this.tsDiscordWrapper.logger.error("Connection Error: " + error.toString()); }); connection.on('close', (code, desc) => { this.disconnect(connection, code, this.gatewayIntents); }); connection.on('message', (message: any) => { if (message.type === 'utf8') { const data = message.utf8Data; this.onEvent(JSON.parse(data), connection); } else { this.tsDiscordWrapper.logger.error("Received: '" + message + "'"); } }); }); } async disconnect(connection: connection, code: number, intents: GateWayIntent[]) { let closeCodeInfo = CloseCodeInfo.get(code); this.tsDiscordWrapper.logger.info("Disconnected from TSDiscordWrapper, code: " + code + " " + closeCodeInfo.reason); this.heartbeat.cancel(); let closeCode if (closeCodeInfo.code != null) { closeCode = closeCodeInfo.code; } else { closeCode = 1000; } if (closeCode == closeCodeInfo.code && closeCodeInfo.shouldReconnect()) { this.tsDiscordWrapper.logger.info("Reconnecting to TSDiscordWrapper in 5 seconds"); await new Promise(resolve => setTimeout(resolve, 5000)); await this.connect(this.token as string, intents); } else { this.tsDiscordWrapper.logger.info("Not reconnecting to TSDiscordWrapper, code: " + code + " " + closeCodeInfo.reason); await this.invalidate(code); } } async invalidate(code: number) { this.sessionId = null; this.resumeUrl = null; //exit process.exit(code); } async resume(connection: connection) { const data = { "op": OpCode.RESUME, "d": { "token": this.token, "session_id": this.sessionId, "seq": this.sequenceNumber } } await this.send(connection, JSON.stringify(data)); } async identify(connection1: connection, intents: GateWayIntent[]) { const data = { "op": OpCode.IDENTIFY, "d": { "token": this.token, "intents": GateWayIntent.calculateBitmask(intents), "properties": { "$os": platform(), "$browser": "TSDiscordWrapper", "$device": "TSDiscordWrapper" } } } this.identifyRateLimit = true; await this.send(connection1, JSON.stringify(data)); } async onEvent(payload: any, connection: connection) { const sequenceNumber = payload.s; if (sequenceNumber !== null) { this.sequenceNumber = sequenceNumber; } const data = payload.d; const opCode = payload.op; switch (get(opCode)) { case OpCode.DISPATCH: { const eventName = payload.t; await this.handleEvent(eventName, data); break; } case OpCode.RECONNECT: { await this.sendClose(connection, CloseCode.RECONNECT, CloseCodeInfo.get(CloseCode.RECONNECT).reason); break; } case OpCode.HELLO: { const heartbeatInterval = data.heartbeat_interval as number; await this.heartbeat.start(heartbeatInterval, true, this.sequenceNumber, connection); break; } case OpCode.HEARTBEAT_ACK: { this.heartbeat.receivedHeartbeatAck(); break; } case OpCode.INVALID_SESSION: { this.identifyRateLimit = this.identifyRateLimit && Date.now() - this.identifyTimeout < 5000; if (this.identifyRateLimit) { this.tsDiscordWrapper.logger.warn("Identify rate limit hit, waiting 5 seconds"); await new Promise(resolve => setTimeout(resolve, 5000)); } else if (this.attemptedToResume) { const oneToFiveSeconds: number = Math.floor(Math.random() * 5) + 1; this.tsDiscordWrapper.logger.warn("Invalid session, waiting " + oneToFiveSeconds + " seconds before attempting to to reconnect"); await new Promise(resolve => setTimeout(resolve, oneToFiveSeconds * 1000)); } this.sessionId = null; this.resumeUrl = null; await this.sendClose(connection, CloseCode.INVALID_SESSION, CloseCodeInfo.get(CloseCode.INVALID_SESSION).reason); } } } async sendClose(connection: connection, code?: number, reason?: string) { connection.sendCloseFrame(code, reason); } async send(connection: connection, json: string) { if (json === null) { this.tsDiscordWrapper.logger.error("Error sending message: json is null"); return; } if (connection === null) { this.tsDiscordWrapper.logger.error("Error sending message: connection is null"); return; } connection.send(json, err => { if (err != undefined) { this.tsDiscordWrapper.logger.error("Error sending message: " + err); } }); } async handleEvent(eventName: string, data: any) { switch (EventNames.get(eventName)) { case EventNames.READY: { this.sessionId = data.session_id as string; this.resumeUrl = data.resume_gateway_url as string; this.attemptedToResume = false; new ReadyHandler(this.tsDiscordWrapper, data); break; } case EventNames.INVALID_SESSION: { this.sessionId = null; this.resumeUrl = null; break; } case EventNames.RECONNECT: { this.attemptedToResume = false; break } case EventNames.RESUMED: { this.attemptedToResume = false; break; } case EventNames.INTERACTION_CREATE: { new InteractionHandler(this.tsDiscordWrapper, data); } } } }