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
JavaScript
"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