UNPKG

websockets-topgg

Version:

A real-time WebSocket client for Top.gg that converts webhooks into a persistent connection, enabling seamless vote tracking, reminders, and enhanced community engagement.

279 lines 11.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TopWebsocket = void 0; const events_1 = require("events"); const ws_1 = require("ws"); const typings_js_1 = require("./typings.js"); const lru_cache_1 = require("lru-cache"); const ApiError_js_1 = require("./ApiError.js"); const baseUrl = "api.websockets-topgg.com/v0"; // const baseUrl = "localhost:4100" const websoscketBaseUrl = `wss://${baseUrl}/websocket`; const apiBaseUrl = `https://${baseUrl}/api`; var StatusCodes; (function (StatusCodes) { StatusCodes[StatusCodes["UNKNOWN_ERROR"] = 4000] = "UNKNOWN_ERROR"; StatusCodes[StatusCodes["AUTHENTICATION_FAILED"] = 4007] = "AUTHENTICATION_FAILED"; StatusCodes[StatusCodes["TOO_MANY_CONNECTIONS"] = 4005] = "TOO_MANY_CONNECTIONS"; })(StatusCodes || (StatusCodes = {})); var EmitEvents; (function (EmitEvents) { EmitEvents["READY"] = "ready"; EmitEvents["ERROR"] = "error"; EmitEvents["DISCONNECTED"] = "disconnected"; EmitEvents["VOTE"] = "vote"; EmitEvents["TEST"] = "test"; EmitEvents["REMINDER"] = "reminder"; })(EmitEvents || (EmitEvents = {})); class TopWebsocket extends events_1.EventEmitter { /* I'm not entirely sure how to handle this without using any. I'd love feedback to anyone looking at this mess. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any on(event, listener) { return super.on(event, listener); } /** * Create a new TopWebsocket instance * * @param {string} socketToken Socket token * @param {Options} [options] Websocket options and configuration of userCache */ constructor(socketToken, options = {}) { var _a; super(); this._status = "Disconnected"; this.userCache = new lru_cache_1.LRUCache({ ttl: 21600, max: 1000, maxSize: 1000 * 60 * 60 }); this.reconnectionAttempts = 0; this.reconnectionDelay = 10000; // Set of status codes for which reconnection should not be attempted this.noReconnectStatusCodes = new Set([ StatusCodes.AUTHENTICATION_FAILED, StatusCodes.TOO_MANY_CONNECTIONS, ]); this.heartbeat = () => { clearTimeout(this.pingTimeout); this.pingTimeout = setTimeout(() => { var _a; (_a = this.client) === null || _a === void 0 ? void 0 : _a.terminate(); this.emit(EmitEvents.ERROR, new Error("Connection timed out")); }, 30000 + 2000); }; this.setupEventHandlers = () => { if (!this.client) return; this.client.on("open", () => { this.heartbeat(); }); this.client.on("error", (error) => { this.emit(EmitEvents.ERROR, error); }); this.client.on("ping", this.heartbeat); this.client.on("close", this.handleClose); this.client.on("message", this.handleMessage); }; this.handleClose = (code, reason) => { this._status = "Disconnected"; let message = "Connection closed"; let documentationLink; switch (code) { case StatusCodes.AUTHENTICATION_FAILED: message += ": Authentication failed"; documentationLink = "http://localhost:3000/docs/status-codes#authentication-failed"; break; case StatusCodes.TOO_MANY_CONNECTIONS: message += ": Too many connections"; documentationLink = "http://localhost:3000/docs/status-codes#connection-limit"; break; default: message += ": Unknown reason"; break; } clearTimeout(this.pingTimeout); const willReconnect = !this.noReconnectStatusCodes.has(code) && this.reconnectionAttempts < 5; this.emit(EmitEvents.DISCONNECTED, { code, reason: reason.toString(), message, documentationLink, reconnecting: willReconnect, reconnectionAttempts: this.reconnectionAttempts, reconnectionDelay: this.reconnectionDelay, }); if (willReconnect) { setTimeout(() => { if (this.options.resume && this.lastMessageTimestamp) this.connect(this.lastMessageTimestamp); else this.connect(); }, this.reconnectionDelay); this.reconnectionDelay *= 2; this.reconnectionAttempts += 1; } }; this.handleMessage = (data) => { const receivedMessage = this.parseBufferToJson(data); switch (receivedMessage.op) { case typings_js_1.OpCodes.READY: this._status = "Ready"; this.entityId = receivedMessage.d.entityId; this.reconnectionAttempts = 0; this.emit(EmitEvents.READY, receivedMessage.d); break; case typings_js_1.OpCodes.VOTE: this.emit(EmitEvents.VOTE, receivedMessage.d, receivedMessage.ts, this.lastMessageTimestamp); this.lastMessageTimestamp = receivedMessage.ts; this.userCache.set(receivedMessage.d.user.id, receivedMessage.d.user); break; case typings_js_1.OpCodes.TEST: this.emit(EmitEvents.TEST, receivedMessage.d, receivedMessage.ts, this.lastMessageTimestamp); this.lastMessageTimestamp = receivedMessage.ts; break; case typings_js_1.OpCodes.REMINDER: this.emit(EmitEvents.REMINDER, receivedMessage.d, receivedMessage.ts, this.lastMessageTimestamp); this.lastMessageTimestamp = receivedMessage.ts; this.userCache.set(receivedMessage.d.user.id, receivedMessage.d.user); break; default: this.emit(EmitEvents.ERROR, { error: new Error("unknown message received"), message: receivedMessage, }); break; } }; /** * Get User * @param {string} userId The user's ID * @param {boolean} [ignoreCache] When true ignore the cache and fetch directly from the API * @returns {Promise<User>} The user */ this.getUser = async (userId, ignoreCache = false) => { if (!ignoreCache) { // Check if the user is in the cache const cachedUser = this.userCache.get(userId); if (cachedUser) return cachedUser; } // send http request to get user const res = await this.fetchRequest(`/user/${userId}`, "GET"); const user = await res.json(); // Store the user in the cache this.userCache.set(userId, user); return user; }; /** * Get Entity * @returns {Promise<Entity>} and entity */ this.getEntity = async () => { const res = await this.fetchRequest(`/entity`, "GET"); return await res.json(); }; /** * User Voted within past 12 hours * @param {string} userId The user's ID * @param {boolean} ignoreCache When true ignore the cache and fetch directly from the API * @returns {Promise<boolean>} Whether the user has voted in the past 12 hours */ this.userVoted = async (userId, ignoreCache = false) => { const user = await this.getUser(userId, ignoreCache); const lastVoted = new Date(user.lastVoted); const twelveHoursAgo = new Date(Date.now() - 12 * 60 * 60 * 1000); return lastVoted > twelveHoursAgo; }; /** * Set user reminders * @param {string} userId The user's ID * @param {boolean} enable Whether to enable or disable reminders * @returns {Promise<User | undefined>} */ this.setReminders = async (userId, enable) => { const res = await this.fetchRequest(`/user/${userId}/reminders`, "PATCH", { enable, }); const user = await res.json(); this.userCache.set(userId, user); return user; }; /** * Function send a fetch request * @param {string} path The URL to send the request to * @param {string} method The method of the request * @param {object} body The body of the request */ this.fetchRequest = async (path, method, body) => { const res = await fetch(`${apiBaseUrl}${path}`, { method, headers: { Authorization: this.socketToken, "Content-Type": "application/json", }, body: body ? JSON.stringify(body) : null, }); if (!res.ok) await (0, ApiError_js_1.throwApiError)(res); return res; }; this.socketToken = socketToken; this.options = options; if (options.resume === undefined) options.resume = true; if (this.options.userCacheOptions) { this.userCache = new lru_cache_1.LRUCache(this.options.userCacheOptions); } else { this.userCache = new lru_cache_1.LRUCache({ max: 1000 }); } if (options.name && ((_a = options.name) === null || _a === void 0 ? void 0 : _a.length) > 32) { throw new Error("Name is too long, must be 32 characters or less"); } } /** * Get the current status of the websocket * @returns {string} The current status of the websocket */ get status() { return this._status; } /** * Connect to the websocket * @param {number} [lastMessageTimestamp] The timestamp of the last message received, used to resume a connection. IE all messages after the provided timestamp will be sent * @returns {void} */ connect(lastMessageTimestamp = undefined) { this._status = "Connecting"; if (lastMessageTimestamp) { this.client = new ws_1.WebSocket(websoscketBaseUrl, { headers: { authorization: this.socketToken, name: this.options.name || "Unnamed Websocket Client", lastMessageTimestamp, resume: "true" }, }); } else { this.client = new ws_1.WebSocket(websoscketBaseUrl, { headers: { authorization: this.socketToken, name: this.options.name || "Unnamed Websocket Client", }, }); } this.setupEventHandlers(); } ; parseBufferToJson(buffer) { try { return JSON.parse(buffer.toString("utf-8")); } catch (error) { console.error("Invalid JSON:", error); return null; } } } exports.TopWebsocket = TopWebsocket; //# sourceMappingURL=index.js.map