frost-api-wrapper
Version:
A wrapper that allows you to use the websocket and API from G-Portal.
965 lines (960 loc) • 56.9 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
const ws_1 = require("ws");
const Logger_1 = __importDefault(require("./Logger"));
const types_1 = require("../types");
const Helper_1 = __importDefault(require("./Helper"));
const cheerio_1 = require("cheerio");
class RCEManager extends types_1.RCEEvents {
email;
password;
logger;
auth = {
refresh_token: "",
access_token: "",
token_type: "Bearer",
expires_in: 0,
};
tokenRefreshing = false;
servers = new Map();
socket;
requests = new Map();
commands = [];
queue = [];
kaInterval;
connectionAttempt = 0;
/*
* Create a new RCEManager instance
* @param {AuthOptions} auth - The authentication options
* @memberof RCEManager
* @example
* const rce = new RCEManager({ email: "", password: "", servers: [{ identifier: "server1", region: "US", serverId: 12345 }] }, { logLevel: LogLevel.Info });
*/
constructor(auth, logger = {}) {
super();
this.logger = new Logger_1.default(this, logger);
this.email = auth.email;
this.password = auth.password;
const servers = auth.servers || [];
servers.forEach((server) => {
this.servers.set(server.identifier, {
identifier: server.identifier,
serverId: server.serverId,
region: server.region,
refreshPlayers: server.refreshPlayers || 0,
rfBroadcasting: server.rfBroadcasting || false,
currentRfBroadcasts: [],
heliFeeds: server.heliFeeds || false,
bradFeeds: server.bradFeeds || false,
state: server.state || [],
flags: [],
players: [],
added: false,
ready: false,
serviceState: "UNKNOWN",
});
});
}
/*
* Login to GPORTAL and establish a websocket connection
* @param {number} [timeout=60_000] - The timeout for the websocket connection
* @returns {Promise<void>}
* @memberof RCEManager
* @example
* await rce.init();
* @example
* await rce.init(30_000);
*/
async init(timeout = 60_000) {
this.on(constants_1.RCEEvent.Error, (payload) => {
this.logger.error(payload.error);
});
const login = await this.login();
if (!login) {
throw new Error("Failed To Login");
}
await this.authenticate(timeout);
}
/*
* Close the RCEManager and clear all intervals
* @returns {Promise<void>}
* @memberof RCEManager
* @example
* await rce.close();
*/
async close() {
this.clean();
this.logger.info("RCEManager Closed Successfully!");
}
async authenticate(timeout) {
this.logger.debug("Attempting To Authenticate...");
const s = await this.refreshToken();
if (s) {
this.logger.info("Successfully Authenticated!");
await this.connectWebsocket(timeout);
}
else {
this.logError("Failed to authenticate");
setTimeout(() => this.authenticate(timeout), 60_000);
}
}
async login() {
const loginRes = await fetch(constants_1.GPORTALRoutes.Login);
const loginRaw = await loginRes.text();
const cookies = loginRes.headers.get("set-cookie");
const $ = (0, cheerio_1.load)(loginRaw);
const loginUrl = $("#kc-form-login").attr("action");
if (!loginUrl) {
this.logError("Failed To Login: No Login URL Found!");
return false;
}
const postRes = await fetch(loginUrl, {
method: "POST",
headers: {
Cookie: cookies,
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
username: this.email,
password: this.password,
credentialId: "",
}),
});
if (!postRes.ok) {
this.logger.debug(`Failed To Login: ${postRes.status}`);
this.logError(`Failed To Login: ${postRes.statusText}`);
return false;
}
const code = new URLSearchParams(new URL(postRes.url).search).get("code");
if (!code) {
this.logError("Failed To Login: No Code Found!");
return false;
}
const tokenRes = await fetch(constants_1.GPORTALRoutes.Token, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: "website",
code,
redirect_uri: "https://www.g-portal.com/en",
}),
});
if (!tokenRes.ok) {
this.logger.debug(`Failed To Login: ${tokenRes.status}`);
this.logError(`Failed To Login: ${tokenRes.statusText}`);
return false;
}
this.auth = await tokenRes.json();
setTimeout(() => this.refreshToken(), this.auth.expires_in * 1000);
return true;
}
async refreshToken() {
this.tokenRefreshing = true;
this.logger.debug("Attempting To Refresh Token...");
if (!this.auth?.refresh_token) {
this.logError("Failed To Refresh Token: No Refresh Token!");
return false;
}
try {
const response = await fetch(constants_1.GPORTALRoutes.Token, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "refresh_token",
client_id: "website",
refresh_token: this.auth.refresh_token,
}),
});
if (!response.ok) {
this.logger.debug(this.auth);
this.logger.debug(response.body);
this.logError(`Failed To Refresh Token: ${response.statusText}`);
return false;
}
this.auth = await response.json();
setTimeout(() => this.refreshToken(), this.auth.expires_in * 1000);
this.logger.debug("Token Successfully Refreshed!");
this.tokenRefreshing = false;
return true;
}
catch (err) {
this.logError(`Failed To Refresh Token: ${err}`);
return false;
}
}
logError(message, server) {
this.emit(constants_1.RCEEvent.Error, { server, error: message });
}
clean() {
this.logger.debug("Cleaning Up All Data...");
this.servers.forEach((server) => {
clearInterval(server.refreshPlayersInterval);
clearInterval(server.rfBroadcastingInterval);
clearInterval(server.bradFeedsInterval);
clearInterval(server.heliFeedsInterval);
server.players = [];
server.added = false;
server.ready = false;
server.trueServerId = undefined;
});
this.requests.clear();
this.commands = [];
this.queue = [];
if (this.socket?.OPEN)
this.socket.close(1000);
this.socket = undefined;
clearInterval(this.kaInterval);
this.logger.debug("Cleaned Up All Data Successfully!");
}
async connectWebsocket(timeout) {
this.logger.debug("Connecting To Websocket...");
this.connectionAttempt++;
this.socket = new ws_1.WebSocket(constants_1.GPORTALRoutes.WebSocket, ["graphql-ws"], {
headers: {
origin: constants_1.GPORTALRoutes.Origin,
host: "www.g-portal.com",
},
timeout,
});
this.socket.on("open", async () => {
this.logger.debug("Websocket Connection Established!");
await this.authenticateWebsocket(timeout);
this.servers.forEach(async (server) => {
if (!server.added)
await this.addServer(server);
});
});
this.socket.on("error", (err) => {
this.logError(`Websocket Error: ${err.message}`);
this.clean();
if (this.connectionAttempt < 5) {
this.logger.warn(`Websocket Error: Attempting To Reconnect In ${(this.connectionAttempt + 1) * 10} Second(s) (Attempt ${this.connectionAttempt + 1} Of 5)`);
setTimeout(() => this.connectWebsocket(timeout), this.connectionAttempt * 10_000);
}
else {
this.logError("Failed To Connect To Websocket: Too Many Attempts!");
}
});
this.socket.on("close", (code, reason) => {
this.clean();
if (code !== 1000) {
if (this.connectionAttempt < 5) {
this.logger.warn(`Websocket closed: Attempting To Reconnect In ${(this.connectionAttempt + 1) * 10} Second(s) (Attempt ${this.connectionAttempt + 1} Of 5)`);
setTimeout(() => this.connectWebsocket(timeout), this.connectionAttempt * 10_000);
}
else {
this.logError("Failed To Connect To Websocket: Too Many Attempts!");
}
}
else {
this.logError(`Websocket Closed: ${reason}`);
}
});
this.socket.on("message", (data) => {
try {
const message = JSON.parse(data.toString());
if (message.type === "ka")
return;
this.logger.debug(`Received Message: ${JSON.stringify(message)}`);
if (message.type === "error") {
return this.logError(`Websocket Error: ${message?.payload?.message}`);
}
if (message.type === "connection_ack") {
this.connectionAttempt = 0;
return this.logger.debug("Websocket Authenticated Successfully!");
}
if (message.type === "data") {
const request = this.requests.get(message.id);
if (!request) {
return this.logError(`Failed To Handle Message: No Request Found For ID ${message.id}!`);
}
const server = this.getServer(request.identifier);
if (!server) {
return this.logError(`Failed To Handle Message: No Server Found For ID ${request.identifier}!`);
}
if (message?.payload?.errors?.length) {
const error = message.payload.errors[0].message;
const aioRpcErrorMatch = error.match(/status\s*=\s*([^\n]+)\s+details\s*=\s*"([^"]+)"/);
if (aioRpcErrorMatch) {
const status = aioRpcErrorMatch[1].trim();
// const details = aioRpcErrorMatch[2].trim();
if (status === "StatusCode.UNAVAILABLE") {
this.logError("AioRpcError: Server Is UUnavailable", server);
this.clean();
this.logger.warn("AioRpcError: Will Attempt To Reconnect In 10 Seconds!");
setTimeout(() => {
this.connectWebsocket(timeout);
}, 10_000);
}
}
return this.logError(message.payload.errors[0].message, server);
}
if (message?.payload?.data?.consoleMessages) {
this.handleWebsocketMessage(message, server);
}
else if (message?.payload?.data?.serviceState) {
this.handleServiceState(message, server);
}
}
}
catch (err) {
this.logError(`Failed To Handle Message: ${err}`);
}
});
}
async authenticateWebsocket(timeout) {
this.logger.debug("Attempting To Authenticate Websocket");
if (!this.auth?.access_token) {
return this.logError("Failed To Authenticate Websocket: No Access Token!");
}
if (this.socket?.OPEN) {
this.socket.send(JSON.stringify({
type: constants_1.GPORTALWebsocketTypes.Init,
payload: {
authorization: this.auth.access_token,
},
}));
this.kaInterval = setInterval(() => {
if (this.socket && this.socket.OPEN) {
this.logger.debug("Sending keep-alive Message");
this.socket.send(JSON.stringify({ type: "ka" }));
}
}, 30_000);
}
else {
this.logError("Failed To Authenticate Websocket: No Websocket Connection!");
this.clean();
this.connectWebsocket(timeout);
}
}
async fetchServiceState(sid, region) {
if (!this.auth?.access_token) {
this.logError("Failed To Fetch Service State: No Access Token!");
return null;
}
if (this.tokenRefreshing) {
this.logger.warn("Token Is Refreshing, Retrying In A Few Seconds!");
await this.sleep(3_000);
return this.fetchServiceState(sid, region);
}
try {
const response = await fetch(constants_1.GPORTALRoutes.Api, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `${this.auth.token_type} ${this.auth.access_token}`,
},
body: JSON.stringify({
operationName: "ctx",
variables: {
sid,
region,
},
query: "query ctx($sid: Int!, $region: REGION!) {\n cfgContext(rsid: {id: $sid, region: $region}) {\n ns {\n ...CtxFields\n __typename\n }\n errors {\n mutator\n affectedPaths\n error {\n class_\n args\n __typename\n }\n scope\n __typename\n }\n __typename\n }\n}\n\nfragment GameServerFields on GameServer {\n id\n serverName\n serverPort\n serverIp\n __typename\n}\n\nfragment PermissionFields on Permission {\n userName\n created\n __typename\n}\n\nfragment MysqlDbFields on CustomerMysqlDb {\n httpUrl\n host\n port\n database\n username\n password\n __typename\n}\n\nfragment ServiceStateFields on ServiceState {\n state\n fsmState\n fsmIsTransitioning\n fsmIsExclusiveLocked\n fsmFileAccess\n fsmLastStateChange\n fsmStateLiveProgress {\n ... on InstallProgress {\n action\n percentage\n __typename\n }\n ... on BroadcastProgress {\n nextMessageAt\n stateExitAt\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment RestartTaskFields on RestartTask {\n id\n runOnWeekday\n runOnDayofmonth\n runAtTimeofday\n runInTimezone\n schedule\n data {\n description\n args\n scheduleExtended\n nextFireTime\n __typename\n }\n __typename\n}\n\nfragment DisplayPortFields on DisplayPorts {\n rconPort\n queryPort\n __typename\n}\n\nfragment SteamWorkshopItemFields on SteamWorkshopItem {\n id\n appId\n itemType\n name\n links {\n websiteUrl\n __typename\n }\n summary\n logo {\n url\n __typename\n }\n maps {\n workshopId\n mapName\n __typename\n }\n dateCreated\n dateModified\n __typename\n}\n\nfragment SevenDaysModFields on SevenDaysMod {\n id\n name\n repoKey\n active\n created\n modified\n __typename\n}\n\nfragment MapParams on FarmingSimulatorMapParamsObject {\n serverIp\n webServerPort\n webStatsCode\n token\n __typename\n}\n\nfragment CtxFields on RootNamespace {\n sys {\n game {\n name\n key\n platform\n forumBoardId\n supportedPlatforms\n __typename\n }\n extraGameTranslationKeys\n gameServer {\n ...GameServerFields\n __typename\n }\n permissionsOwner {\n ...PermissionFields\n __typename\n }\n permissions {\n ...PermissionFields\n __typename\n }\n mysqlDb {\n ...MysqlDbFields\n __typename\n }\n __typename\n }\n service {\n config {\n rsid {\n id\n region\n __typename\n }\n type\n hwId\n state\n ftpUser\n ftpPort\n ftpPassword\n ftpReadOnly\n ipAddress\n rconPort\n queryPort\n autoBackup\n dnsNames\n currentVersion\n targetVersion\n __typename\n }\n latestRev {\n id\n created\n __typename\n }\n maxSlots\n files\n memory {\n base\n effective\n __typename\n }\n currentState {\n ...ServiceStateFields\n __typename\n }\n backups {\n id\n userSize\n created\n isAutoBackup\n __typename\n }\n restartSchedule {\n ...RestartTaskFields\n __typename\n }\n dnsAvailableTlds\n __typename\n }\n admin {\n hardwareGuacamoleConnection {\n url\n __typename\n }\n __typename\n }\n profile {\n __typename\n ... on ProfileNamespace {\n name\n cfgFiles\n logFiles\n publicConfigs\n configDefinition\n displayPorts {\n ...DisplayPortFields\n __typename\n }\n enableCustomerDb\n enableCustomHostnames\n __typename\n }\n ... on MinecraftProfileNamespace {\n name\n cfgFiles\n logFiles\n publicConfigs\n configDefinition\n displayPorts {\n rconPort\n queryPort\n additionalPorts\n __typename\n }\n enableCustomerDb\n enableCustomHostnames\n worlds\n addonRam\n isRamServer\n ramOrderCreationDate\n ramStopTimeUtc\n isConnectedToBungeecord\n bungeecordServerUrl\n executables {\n id\n name\n key\n default\n __typename\n }\n mods {\n id\n repoKey\n name\n image\n mindRam\n projectUrl\n revisions {\n id\n created\n executableId\n extraData\n __typename\n }\n __typename\n }\n __typename\n }\n ... on CsgoProfileNamespace {\n name\n cfgFiles\n logFiles\n publicConfigs\n configDefinition\n displayPorts {\n rconPort\n queryPort\n gotvPort\n __typename\n }\n enableCustomerDb\n enableCustomHostnames\n selectedWorkshopItems {\n ...SteamWorkshopItemFields\n __typename\n }\n installedMaps {\n name\n displayName\n workshopItem {\n ...SteamWorkshopItemFields\n __typename\n }\n __typename\n }\n __typename\n }\n ... on ValheimProfileNamespace {\n name\n cfgFiles\n clientLink\n logFiles\n publicConfigs\n configDefinition\n displayPorts {\n ...DisplayPortFields\n __typename\n }\n enableCustomerDb\n enableCustomHostnames\n __typename\n }\n ... on HellLetLooseProfileNamespace {\n name\n cfgFiles\n logFiles\n publicConfigs\n configDefinition\n displayPorts {\n rconPort\n queryPort\n statsPort\n beaconPort\n __typename\n }\n enableCustomerDb\n enableCustomHostnames\n __typename\n }\n ... on SevenDaysToDieProfileNamespace {\n name\n cfgFiles\n logFiles\n publicConfigs\n configDefinition\n displayPorts {\n rconPort\n queryPort\n telnetPort\n webDashboardPort\n __typename\n }\n enableCustomerDb\n enableCustomHostnames\n availableMods {\n ...SevenDaysModFields\n __typename\n }\n isModUpdateAvailable\n __typename\n }\n ... on SoulmaskProfileNamespace {\n name\n cfgFiles\n gameUid\n logFiles\n publicConfigs\n configDefinition\n displayPorts {\n ...DisplayPortFields\n __typename\n }\n enableCustomerDb\n enableCustomHostnames\n __typename\n }\n ... on VRisingProfileNamespace {\n name\n cfgFiles\n isLaunchServer\n isOfficialServer\n logFiles\n publicConfigs\n configDefinition\n displayPorts {\n ...DisplayPortFields\n __typename\n }\n enableCustomerDb\n enableCustomHostnames\n __typename\n }\n ... on RustConsoleProfileNamespace {\n name\n cfgFiles\n logFiles\n publicConfigs\n configDefinition\n displayPorts {\n ...DisplayPortFields\n __typename\n }\n enableCustomerDb\n modifyActionHints\n __typename\n }\n ... on FarmingSimulatorProfileNamespace {\n name\n cfgFiles\n logFiles\n publicConfigs\n configDefinition\n wiLink\n defaultModSpace\n masterWiLink\n displayPorts {\n rconPort\n queryPort\n webPort\n __typename\n }\n mapParams {\n ...MapParams\n __typename\n }\n __typename\n }\n ... on BungeecordProfileNamespace {\n name\n cfgFiles\n logFiles\n publicConfigs\n configDefinition\n displayPorts {\n ...DisplayPortFields\n __typename\n }\n enableCustomerDb\n enableCustomHostnames\n gpServers\n accessibleMinecraftServers {\n ...GameServerFields\n __typename\n }\n __typename\n }\n }\n __typename\n}",
}),
});
if (!response.ok) {
this.logError(`Failed To Fetch Service State: ${response.statusText}`);
return null;
}
const data = await response.json();
return data.data.cfgContext.ns.service.currentState.state;
}
catch (err) {
this.logError(`Failed To Fetch Service State: ${err}`);
}
}
handleServiceState(message, server) {
// Possible states: "STOPPING", "MAINTENANCE", "UPDATING", "STOPPED", "STARTING", "RUNNING", "SUSPENDED"
const serviceState = message?.payload?.data?.serviceState?.state;
if (server.serviceState === serviceState)
return;
this.servers.set(server.identifier, {
...server,
serviceState,
});
if (serviceState === "RUNNING" && !server.ready) {
this.markServerAsReady(server);
}
if (serviceState === "STOPPED" && server.ready) {
this.markServerAsUnready(server);
}
this.emit(constants_1.RCEEvent.ServiceState, { server, state: serviceState });
if (serviceState === "SUSPENDED") {
this.logger.warn(`Server ${server.identifier} Is Suspended! Removing...`);
this.removeServer(server.identifier);
}
}
handleWebsocketMessage(message, server) {
const logMessages = message?.payload?.data?.consoleMessages?.message
?.split("\n")
.filter((e) => e !== "") || [];
if (logMessages?.length > 3)
return;
logMessages?.forEach((logMessage) => {
const logMatch = logMessage.match(/(\d{2}\/\d{2}\/\d{4} \d{2}:\d{2}:\d{2}):LOG:[^:]+: (.+)$/);
if (!logMatch)
return;
const logMessageDate = logMatch[1];
const logMessageContent = logMatch[2];
const log = logMessageContent.trim();
if (!log)
return;
/*
The following events are "refreshing" events
*/
// EVENT_START - Bradley APC Debris
if (log.includes("servergibs_bradley") &&
log.startsWith("realm ;entity ;group ;parent ;name") &&
!server.flags.includes("bradley")) {
server.flags.push("bradley");
this.servers.set(server.identifier, {
...server,
flags: server.flags,
});
setTimeout(() => {
server.flags = server.flags.filter((f) => f !== "bradley");
this.servers.set(server.identifier, {
...server,
flags: server.flags,
});
}, 360_000);
return this.emit(constants_1.RCEEvent.EventStart, {
server,
event: "Bradley APC Debris",
special: true,
});
}
// EVENT_START - Patrol Helicopter Debris
if (log.includes("servergibs_patrolhelicopter") &&
log.startsWith("realm ;entity ;group ;parent ;name") &&
!server.flags.includes("heli")) {
server.flags.push("heli");
this.servers.set(server.identifier, {
...server,
flags: server.flags,
});
setTimeout(() => {
server.flags = server.flags.filter((f) => f !== "heli");
this.servers.set(server.identifier, {
...server,
flags: server.flags,
});
}, 360_000);
return this.emit(constants_1.RCEEvent.EventStart, {
server,
event: "Patrol Helicopter Debris",
special: true,
});
}
// USERS
const usersMatch = log.match(/"(.*?)"/g);
if (usersMatch && log.startsWith('<slot:"name">')) {
const players = usersMatch.map((plr) => plr.replace(/"/g, ""));
players.shift();
const s = this.getServer(server.identifier);
const { joined, left } = this.comparePopulation(s.players, players);
if (s.flags.includes("init-refresh")) {
joined.forEach((ign) => {
this.emit(constants_1.RCEEvent.PlayerJoined, { server, ign });
});
left.forEach((ign) => {
this.emit(constants_1.RCEEvent.PlayerLeft, { server, ign });
});
}
if (!s.flags.includes("init-refresh")) {
s.flags.push("init-refresh");
}
this.servers.set(server.identifier, {
...s,
players,
});
this.emit(constants_1.RCEEvent.PlayerListUpdate, {
server,
players,
joined,
left,
});
this.logger.debug(`Refreshed Players For ${server.identifier}`);
}
// BROADCAST_RECEIVED
const broadcastMatch = [
...log.matchAll(/\[(\d+)\sMHz\]\sPosition:\s\(([-\d.]+),\s([-\d.]+),\s([-\d.]+)\),\sRange:\s(\d+)/g),
];
if (broadcastMatch?.length) {
const broadcasts = [];
for (const match of broadcastMatch) {
broadcasts.push({
frequency: parseInt(match[1], 10),
coordinates: [
parseFloat(match[2]),
parseFloat(match[3]),
parseFloat(match[4]),
],
range: parseInt(match[5], 10),
});
}
server.currentRfBroadcasts.forEach((frequency) => {
if (!broadcasts.find((b) => parseInt(b.frequency) === frequency)) {
this.emit(constants_1.RCEEvent.FrequencyLost, {
server,
frequency,
});
server.currentRfBroadcasts = server.currentRfBroadcasts.filter((f) => f !== frequency);
}
});
broadcasts.forEach((broadcast) => {
if (server.currentRfBroadcasts.includes(broadcast.frequency))
return;
server.currentRfBroadcasts.push(broadcast.frequency);
if (broadcast.frequency === 4765) {
this.emit(constants_1.RCEEvent.EventStart, {
server,
event: "Small Oil Rig",
special: false,
});
}
else if (broadcast.frequency === 4768) {
this.emit(constants_1.RCEEvent.EventStart, {
server,
event: "Oil Rig",
special: false,
});
}
this.emit(constants_1.RCEEvent.FrequencyReceived, {
server,
frequency: broadcast.frequency,
coordinates: broadcast.coordinates,
range: broadcast.range,
});
});
return this.servers.set(server.identifier, {
...server,
currentRfBroadcasts: server.currentRfBroadcasts,
});
}
// Check for a command being executed
const executingMatch = log.match(/Executing console system command '([^']+)'/);
if (executingMatch) {
this.logger.debug(`Executing Message Found For: ${executingMatch[1]}`);
const command = executingMatch[1];
this.emit(constants_1.RCEEvent.ExecutingCommand, {
server,
command,
});
// Handle command responses (if using sendCommand function)
const commandRequest = this.commands.find((req) => req.command === command &&
req.identifier === server.identifier &&
!req.timestamp);
if (commandRequest) {
this.logger.debug(`Command "${command}" Found In The Queue!`);
commandRequest.timestamp = logMessageDate;
this.commands = this.commands.map((req) => req.command === command ? commandRequest : req);
this.logger.debug(`Command "${command}" Updated With Timestamp!`);
return;
}
}
// Check for a command response
const commandRequest = this.commands.find((req) => req.identifier === server.identifier &&
req.timestamp === logMessageDate);
if (commandRequest && !log.startsWith("[ SAVE ]")) {
this.logger.debug(`Command Response Found For: ${commandRequest.command}`);
commandRequest.resolve(log);
clearTimeout(commandRequest.timeout);
this.commands = this.commands.filter((req) => !(req.command === commandRequest.command &&
req.identifier === commandRequest.identifier &&
req.timestamp === commandRequest.timestamp));
}
this.emit(constants_1.RCEEvent.Message, { server, message: log });
this.logger.debug(`Received Message: ${log} From ${server.identifier}`);
// PLAYER_KILL event
if (log.includes(" was killed by ")) {
const [victim, killer] = log
.split(" was killed by ")
.map((str) => str.trim());
const victimData = Helper_1.default.getKillInformation(victim);
const killerData = Helper_1.default.getKillInformation(killer);
this.emit(constants_1.RCEEvent.PlayerKill, {
server,
victim: victimData,
killer: killerData,
});
}
// VENDING_MACHINE_NAME event
const vendingMachineMatch = log.match(/\[VENDING MACHINE\] Player \[ ([^\]]+) \] changed name from \[ ([^\]]+) \] to \[ ([^\]]+) \]/);
if (vendingMachineMatch) {
const ign = vendingMachineMatch[1];
const oldName = vendingMachineMatch[2];
const newName = vendingMachineMatch[3];
this.emit(constants_1.RCEEvent.VendingMachineName, {
server,
ign,
oldName,
newName,
});
}
// QUICK_CHAT event
const quickChatMatch = log.match(/(\[CHAT (TEAM|SERVER|LOCAL)\]) ([\w\s\-_]+) : (.+)/);
if (quickChatMatch) {
const quickChatTypes = {
"[CHAT TEAM]": "team",
"[CHAT SERVER]": "server",
"[CHAT LOCAL]": "local",
};
const type = quickChatTypes[quickChatMatch[1]];
const ign = quickChatMatch[3];
let msg = quickChatMatch[4];
this.emit(constants_1.RCEEvent.QuickChat, {
server,
type,
ign,
message: msg,
});
}
if (log.includes("was suicide by Suicide")) {
const ign = log.split(" was suicide by Suicide")[0];
this.emit(constants_1.RCEEvent.PlayerSuicide, { server, ign });
}
// PLAYER_SUICIDE event
if (log.includes("was suicide by Suicide")) {
const ign = log.split(" was suicide by Suicide")[0];
this.emit(constants_1.RCEEvent.PlayerSuicide, { server, ign });
}
// PLAYER_RESPAWNED event
if (log.includes("has entered the game")) {
const ign = log.split(" [")[0];
const platform = log.includes("[xboxone]") ? "XBL" : "PS";
this.emit(constants_1.RCEEvent.PlayerRespawned, { server, ign, platform });
}
// CUSTOM_ZONE_ADDED event
const customZoneAddedMatch = log.match(/Successfully created zone \[([\w\d\s_-]+)\]/);
if (customZoneAddedMatch && customZoneAddedMatch[1]) {
const name = customZoneAddedMatch[1];
this.emit(constants_1.RCEEvent.CustomZoneAdded, { server, name });
}
// CUSTOM_ZONE_REMOVED event
const customZoneRemovedMatch = log.match(/Successfully removed zone \[([\w\d\s_-]+)\]/);
if (customZoneRemovedMatch && customZoneRemovedMatch[1]) {
const name = customZoneRemovedMatch[1];
this.emit(constants_1.RCEEvent.CustomZoneAdded, { server, name });
}
// PLAYER_ROLE_ADD event
const roleMatch = log.match(/\[?SERVER\]?\s*Added\s*\[([^\]]+)\](?::\[([^\]]+)\])?\s*(?:to\s*(?:Group\s*)?)?\[(\w+)\]/i);
if (roleMatch && log.includes("Added")) {
const ign = roleMatch[1];
const role = roleMatch[3];
this.emit(constants_1.RCEEvent.PlayerRoleAdd, { server, ign, role });
}
// ITEM_SPAWN event
const itemSpawnMatch = log.match(/\bgiving ([\w\s_-]+) ([\d.]+) x ([\w\s-]+(?: [\w\s-]+)*)\b/);
if (itemSpawnMatch) {
const ign = itemSpawnMatch[1];
const quantity = Number(itemSpawnMatch[2]);
const item = itemSpawnMatch[3];
this.emit(constants_1.RCEEvent.ItemSpawn, { server, ign, item, quantity });
}
// NOTE_EDIT event
const noteMatch = log.match(/\[NOTE PANEL\] Player \[ ([^\]]+) \] changed name from \[\s*([\s\S]*?)\s*\] to \[\s*([\s\S]*?)\s*\]/);
if (noteMatch) {
const ign = noteMatch[1].trim();
const oldContent = noteMatch[2].trim().split("\\n")[0];
const newContent = noteMatch[3].trim().split("\\n")[0];
if (newContent.length > 0 && oldContent !== newContent) {
this.emit(constants_1.RCEEvent.NoteEdit, {
server,
ign,
oldContent,
newContent,
});
}
}
// TEAM_CREATE event
const teamCreateMatch = log.match(/\[([^\]]+)\] created a new team, ID: (\d+)/);
if (teamCreateMatch) {
const owner = teamCreateMatch[1];
const id = Number(teamCreateMatch[2]);
this.emit(constants_1.RCEEvent.TeamCreate, { server, owner, id });
}
// TEAM_JOIN event
const teamJoinMatch = log.match(/\[([^\]]+)\] has joined \[([^\]]+)]s team, ID: \[(\d+)\]/);
if (teamJoinMatch) {
const ign = teamJoinMatch[1];
const owner = teamJoinMatch[2];
const id = Number(teamJoinMatch[3]);
this.emit(constants_1.RCEEvent.TeamJoin, { server, ign, owner, id });
}
// TEAM_LEAVE event
const teamLeaveMatch = log.match(/\[([^\]]+)\] has left \[([^\]]+)]s team, ID: \[(\d+)\]/);
if (teamLeaveMatch) {
const ign = teamLeaveMatch[1];
const owner = teamLeaveMatch[2];
const id = Number(teamLeaveMatch[3]);
this.emit(constants_1.RCEEvent.TeamLeave, { server, ign, owner, id });
}
// KIT_SPAWN event
const kitSpawnMatch = log.match(/SERVER giving (.+?) kit (\w+)/);
if (kitSpawnMatch) {
const ign = kitSpawnMatch[1];
const kit = kitSpawnMatch[2];
this.emit(constants_1.RCEEvent.KitSpawn, { server, ign, kit });
}
// KIT_GIVE event
const kitGiveMatch = log.match(/\[ServerVar\] ([\w\s_-]+) giving ([\w\s_-]+) kit ([\w\s_-]+)/);
if (kitGiveMatch) {
const admin = kitGiveMatch[1];
const ign = kitGiveMatch[2];
const kit = kitGiveMatch[3];
this.emit(constants_1.RCEEvent.KitGive, { server, admin, ign, kit });
}
// SPECIAL_EVENT_START event
const specialEventStartMatch = log.match(/Setting event as :(\w+)/);
if (specialEventStartMatch) {
const event = specialEventStartMatch[1];
this.emit(constants_1.RCEEvent.SpecialEventStart, { server, event });
}
// SPECIAL_EVENT_END event
if (log.startsWith("Event set as: none")) {
this.emit(constants_1.RCEEvent.SpecialEventEnd, { server });
}
// EVENT_START event
if (log.startsWith("[event]")) {
for (const [key, options] of Object.entries(constants_1.EVENTS)) {
if (log.includes(key)) {
this.emit(constants_1.RCEEvent.EventStart, {
server,
event: options.name,
special: options.special,
});
}
}
}
});
}
async resolveServerId(region, serverId, identifier) {
if (!this.auth?.access_token) {
this.logError("Failed To Resolve Server ID: No Access Token!");
return undefined;
}
if (this.tokenRefreshing) {
this.logger.warn("Token Is Refreshing, Retrying In A Few Seconds!");
await this.sleep(3_000);
return this.resolveServerId(region, serverId, identifier);
}
try {
const response = await fetch(constants_1.GPORTALRoutes.Api, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `${this.auth.token_type} ${this.auth.access_token}`,
},
body: JSON.stringify({
operationName: "sid",
variables: {
gameserverId: serverId,
region,
},
query: "query sid($gameserverId: Int!, $region: REGION!) {\n sid(gameserverId: $gameserverId, region: $region)\n}",
}),
});
if (!response.ok) {
this.logger.debug(`Resolve Server ID Status: ${response.status}`);
this.logger.debug(response.body);
this.logError(`[${identifier}] Failed To Resolve Server ID: ${response.statusText}`);
return undefined;
}
const data = await response.json();
if (data?.errors?.length) {
this.logger.warn(`[${identifier}] Resolve Server ID Error: ${data.errors[0].message}`);
return undefined;
}
const sid = data?.data?.sid;
if (!sid) {
this.logger.warn(`[${identifier}] Invalid Server ID!`);
return undefined;
}
return sid;
}
catch (err) {
this.logError(`[${identifier}] Failed To Resolve Server ID: ${err}`);
return undefined;
}
}
markServerAsUnready(server) {
this.servers.set(server.identifier, {
...server,
ready: false,
});
this.logger.info(`Server "${server.identifier}" Is Not Ready!`);
}
markServerAsReady(server) {
this.servers.set(server.identifier, {
...server,
ready: true,
});
this.logger.info(`[${server.identifier}] Server Is Ready!`);
this.processQueue();
}
async processQueue() {
this.servers.forEach((server) => {
if (server.ready) {
const queuedCommands = this.queue.filter((cmd) => cmd.identifier === server.identifier);
this.queue = this.queue.filter((cmd) => cmd.identifier !== server.identifier);
queuedCommands.forEach(({ identifier, command, response, resolve, reject }) => {
const s = this.getServer(identifier);
this.sendCommandInternal(s, command, response)
.then(resolve)
.catch(reject);
});
}
});
}
async sendCommandInternal(server, command, response) {
if (!this.auth?.access_token) {
this.logError("Failed To Send Command: No Access Token!");
return null;
}
if (!this.socket || !this.socket.OPEN) {
this.logError("Failed To Send Command: No Websocket Connection!");
return null;
}
if (this.tokenRefreshing) {
this.logger.warn("Token Is Refreshing, Retrying In A Few Seconds!");
await this.sleep(3_000);
return this.sendCommandInternal(server, command, response);
}
this.logger.debug(`Sending command "${command}" to ${server.identifier}`);
const payload = {
operationName: "sendConsoleMessage",
variables: {
sid: server.trueServerId,
region: server.region,
message: command,
},
query: "mutation sendConsoleMessage($sid: Int!, $region: REGION!, $message: String!) {\n sendConsoleMessage(rsid: {id: $sid, region: $region}, message: $message) {\n ok\n __typename\n }\n}",
};
if (response) {
return new Promise((resolve, reject) => {
this.commands.push({
identifier: server.identifier,
command,
resolve,
reject,
timeout: undefined,
});
this.logger.debug(`Command "${command}" Added To Queue!`);
try {
fetch(constants_1.GPORTALRoutes.Api, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `${this.auth.token_type} ${this.auth.access_token}`,
},
body: JSON.stringify(payload),
})
.then((res) => {
if (!res.ok) {
this.logger.debug(`Send Command Status: ${res.status}`);
this.logError(`Failed To Send Command: ${res.statusText}`, server);
return null;
}
this.logger.debug(`Command "${command}" Sent Successfully!`);
this.logger.debug(`Starting Timeout For Command "${command}"`);
const cmd = this.commands.find((req) => req.command === command &&
req.identifier === server.identifier);
if (cmd) {
cmd.timeout = setTimeout(() => {
this.commands = this.commands.filter((req) => req.command !== command &&
req.identifier !== server.identifier);
resolve(undefined);
}, 3_000);
}
})
.catch((err) => {
this.commands = this.commands.filter((req) => req.command !== command &&
req.identifier !== server.identifier);
reject(err);
});
}
catch (err) {
this.commands = this.commands.filter((req) => req.command !== command && req.identifier !== server.identifier);
reject(err);
}
});
}
else {
try {
const response = await fetch(constants_1.GPORTALRoutes.Api, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `${this.auth.token_type} ${this.auth.access_token}`,
},
body: JSON.stringify(payload),
});
if (!response.ok) {
this.logger.debug(`Send Command Status: ${response.status}`);
this.logError(`Failed To Send Command: ${response.statusText}`, server);
return null;
}
this.logger.debug(`Command "${command}" Sent Successfully!`);
return undefined;
}
catch (err) {
this.logError(`Failed To Send Command: ${err}`, server);
return null;
}
}
}
/*
* Send a command to a Rust server
* @param {string} identifier - The server identifier
* @param {string} command - The command to send
* @param {boolean} [response=false] - Whether to wait for a response
* @returns {Promise<string | undefined | null>}
* @memberof RCEManager
* @example
* await rce.sendCommand("server1", "RemoveOwner username");
* @example
* await rce.sendCommand("server1", "BanID username", true);
*/
async sendCommand(identifier, command, response = false) {
return new Promise((resolve, reject) => {
const server = this.getServer(identifier);
if (!server) {
this.logError(`[${identifier}] Failed To Send Command: No Server Found For ID ${identifier}`);
return null;
}
if (server.ready) {
this.logger.debug(`[${identifier}] The Server Is Ready, Sending Command Immediately`);
this.sendCommandInternal(server, command, response)
.then(resolve)
.catch(reject);
}
else {
this.logger.debug(`[${identifier}] The Server Is Not Ready, Adding Command To Queue`);
this.queue.push({ identifier, command, response, resolve, reject });
}
});
}
/*
* Add a Rust server to the manager
* @param {ServerOptions} opts - The server options
* @returns {Promise<boolean>}
* @memberof RCEManager
* @example
* await rce.addServer({ identifier: "server1", region: "US", serverId: 12345 });
* @example
* await rce.addServer({ identifier: "server2", region: "EU", serverId: 54321, refreshPlayers: 5 });
*/
async addServer(opts) {
this.logger.debug(`[${opts.identifier}] Adding Server!`);
const sid = await th