This commit is contained in:
1 parent
8efd032cc6
commit
df38dccbf1
735 files changed
+128321
-120870
No files matched your search
+123
-117
@@ -1,11 +1,11 @@
|
||||
import net from "node:net";
|
||||
import { CurrencyType } from "@/lib/services/currency";
|
||||
import { env } from "@/env";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { CurrencyType } from "@/lib/services/currency";
|
||||
|
||||
export interface RconPayload {
|
||||
key: string;
|
||||
data: Record<string, unknown> | null;
|
||||
key: string;
|
||||
data: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export type RconTransport = (payload: RconPayload) => Promise<boolean>;
|
||||
@@ -17,40 +17,43 @@ export type RconTransport = (payload: RconPayload) => Promise<boolean>;
|
||||
* connect timeout + exponential-backoff retry so delivery is best-effort.
|
||||
*/
|
||||
export function tcpTransport(opts: {
|
||||
host: string;
|
||||
port: number;
|
||||
timeoutMs: number;
|
||||
maxRetries: number;
|
||||
host: string;
|
||||
port: number;
|
||||
timeoutMs: number;
|
||||
maxRetries: number;
|
||||
}): RconTransport {
|
||||
const sendOnce = (payload: RconPayload): Promise<boolean> =>
|
||||
new Promise((resolve) => {
|
||||
const socket = new net.Socket();
|
||||
const json = JSON.stringify(payload);
|
||||
let settled = false;
|
||||
const finish = (ok: boolean) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
socket.destroy();
|
||||
resolve(ok);
|
||||
};
|
||||
const timer = setTimeout(() => finish(false), opts.timeoutMs);
|
||||
socket.once("error", () => finish(false));
|
||||
socket.connect(opts.port, opts.host, () => {
|
||||
socket.write(json, () => finish(true));
|
||||
});
|
||||
});
|
||||
const sendOnce = (payload: RconPayload): Promise<boolean> =>
|
||||
new Promise((resolve) => {
|
||||
const socket = new net.Socket();
|
||||
const json = JSON.stringify(payload);
|
||||
let settled = false;
|
||||
const finish = (ok: boolean) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
socket.destroy();
|
||||
resolve(ok);
|
||||
};
|
||||
const timer = setTimeout(() => finish(false), opts.timeoutMs);
|
||||
socket.once("error", () => finish(false));
|
||||
socket.connect(opts.port, opts.host, () => {
|
||||
socket.write(json, () => finish(true));
|
||||
});
|
||||
});
|
||||
|
||||
return async (payload) => {
|
||||
for (let attempt = 0; attempt < opts.maxRetries; attempt++) {
|
||||
if (await sendOnce(payload)) return true;
|
||||
if (attempt < opts.maxRetries - 1) {
|
||||
await new Promise((r) => setTimeout(r, 200 * 2 ** attempt));
|
||||
}
|
||||
}
|
||||
logger.error("RCON delivery failed after retries", { module: "rcon", key: payload.key });
|
||||
return false;
|
||||
};
|
||||
return async (payload) => {
|
||||
for (let attempt = 0; attempt < opts.maxRetries; attempt++) {
|
||||
if (await sendOnce(payload)) return true;
|
||||
if (attempt < opts.maxRetries - 1) {
|
||||
await new Promise((r) => setTimeout(r, 200 * 2 ** attempt));
|
||||
}
|
||||
}
|
||||
logger.error("RCON delivery failed after retries", {
|
||||
module: "rcon",
|
||||
key: payload.key,
|
||||
});
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,94 +62,97 @@ export function tcpTransport(opts: {
|
||||
* send `data: null`, givepoints `type` is the numeric CurrencyType value).
|
||||
*/
|
||||
export class RconClient {
|
||||
constructor(private readonly transport: RconTransport) {}
|
||||
constructor(private readonly transport: RconTransport) {}
|
||||
|
||||
send(key: string, data: Record<string, unknown> | null = null): Promise<boolean> {
|
||||
return this.transport({ key, data });
|
||||
}
|
||||
send(
|
||||
key: string,
|
||||
data: Record<string, unknown> | null = null,
|
||||
): Promise<boolean> {
|
||||
return this.transport({ key, data });
|
||||
}
|
||||
|
||||
giveCredits(userId: number, credits: number) {
|
||||
return this.send("givecredits", { user_id: userId, credits });
|
||||
}
|
||||
givePoints(userId: number, amount: number, type: CurrencyType) {
|
||||
return this.send("givepoints", { user_id: userId, points: amount, type });
|
||||
}
|
||||
giveDuckets(userId: number, amount: number) {
|
||||
return this.givePoints(userId, amount, CurrencyType.Duckets);
|
||||
}
|
||||
giveDiamonds(userId: number, amount: number) {
|
||||
return this.givePoints(userId, amount, CurrencyType.Diamonds);
|
||||
}
|
||||
givePointsGotw(userId: number, amount: number) {
|
||||
return this.givePoints(userId, amount, CurrencyType.Points);
|
||||
}
|
||||
giveBadge(userId: number, badge: string) {
|
||||
return this.send("givebadge", { user_id: userId, badge });
|
||||
}
|
||||
setMotto(userId: number, motto: string) {
|
||||
return this.send("setmotto", { user_id: userId, motto });
|
||||
}
|
||||
setRank(userId: number, rank: number) {
|
||||
return this.send("setrank", { user_id: userId, rank });
|
||||
}
|
||||
sendGift(userId: number, itemId: number, message = "Here is a gift.") {
|
||||
return this.send("sendgift", { user_id: userId, itemid: itemId, message });
|
||||
}
|
||||
disconnectUser(userId: number, username = "") {
|
||||
return this.send("disconnect", { user_id: userId, username });
|
||||
}
|
||||
alertUser(userId: number, message: string) {
|
||||
return this.send("alertuser", { user_id: userId, message });
|
||||
}
|
||||
forwardUser(userId: number, roomId: number) {
|
||||
return this.send("forwarduser", { user_id: userId, room_id: roomId });
|
||||
}
|
||||
kickAll(roomId: number) {
|
||||
return this.send("kickall", { room_id: roomId });
|
||||
}
|
||||
hotelAlert(message: string) {
|
||||
return this.send("hotelalert", { message });
|
||||
}
|
||||
staffAlert(message: string) {
|
||||
return this.send("staffalert", { message });
|
||||
}
|
||||
executeCommand(userId: number, command: string) {
|
||||
return this.send("executecommand", { user_id: userId, command });
|
||||
}
|
||||
updateWordFilter() {
|
||||
return this.send("updatewordfilter");
|
||||
}
|
||||
updateCatalog() {
|
||||
return this.send("updatecatalog");
|
||||
}
|
||||
updateItems() {
|
||||
return this.send("updateitems");
|
||||
}
|
||||
updateConfig() {
|
||||
return this.send("updateconfig");
|
||||
}
|
||||
removeBadge(userId: number, badge: string) {
|
||||
return this.send("removebadge", { user_id: userId, badge });
|
||||
}
|
||||
muteUser(userId: number, duration: number) {
|
||||
return this.send("muteuser", { user_id: userId, duration });
|
||||
}
|
||||
unmuteUser(userId: number) {
|
||||
return this.send("unmuteuser", { user_id: userId });
|
||||
}
|
||||
giveCredits(userId: number, credits: number) {
|
||||
return this.send("givecredits", { user_id: userId, credits });
|
||||
}
|
||||
givePoints(userId: number, amount: number, type: CurrencyType) {
|
||||
return this.send("givepoints", { user_id: userId, points: amount, type });
|
||||
}
|
||||
giveDuckets(userId: number, amount: number) {
|
||||
return this.givePoints(userId, amount, CurrencyType.Duckets);
|
||||
}
|
||||
giveDiamonds(userId: number, amount: number) {
|
||||
return this.givePoints(userId, amount, CurrencyType.Diamonds);
|
||||
}
|
||||
givePointsGotw(userId: number, amount: number) {
|
||||
return this.givePoints(userId, amount, CurrencyType.Points);
|
||||
}
|
||||
giveBadge(userId: number, badge: string) {
|
||||
return this.send("givebadge", { user_id: userId, badge });
|
||||
}
|
||||
setMotto(userId: number, motto: string) {
|
||||
return this.send("setmotto", { user_id: userId, motto });
|
||||
}
|
||||
setRank(userId: number, rank: number) {
|
||||
return this.send("setrank", { user_id: userId, rank });
|
||||
}
|
||||
sendGift(userId: number, itemId: number, message = "Here is a gift.") {
|
||||
return this.send("sendgift", { user_id: userId, itemid: itemId, message });
|
||||
}
|
||||
disconnectUser(userId: number, username = "") {
|
||||
return this.send("disconnect", { user_id: userId, username });
|
||||
}
|
||||
alertUser(userId: number, message: string) {
|
||||
return this.send("alertuser", { user_id: userId, message });
|
||||
}
|
||||
forwardUser(userId: number, roomId: number) {
|
||||
return this.send("forwarduser", { user_id: userId, room_id: roomId });
|
||||
}
|
||||
kickAll(roomId: number) {
|
||||
return this.send("kickall", { room_id: roomId });
|
||||
}
|
||||
hotelAlert(message: string) {
|
||||
return this.send("hotelalert", { message });
|
||||
}
|
||||
staffAlert(message: string) {
|
||||
return this.send("staffalert", { message });
|
||||
}
|
||||
executeCommand(userId: number, command: string) {
|
||||
return this.send("executecommand", { user_id: userId, command });
|
||||
}
|
||||
updateWordFilter() {
|
||||
return this.send("updatewordfilter");
|
||||
}
|
||||
updateCatalog() {
|
||||
return this.send("updatecatalog");
|
||||
}
|
||||
updateItems() {
|
||||
return this.send("updateitems");
|
||||
}
|
||||
updateConfig() {
|
||||
return this.send("updateconfig");
|
||||
}
|
||||
removeBadge(userId: number, badge: string) {
|
||||
return this.send("removebadge", { user_id: userId, badge });
|
||||
}
|
||||
muteUser(userId: number, duration: number) {
|
||||
return this.send("muteuser", { user_id: userId, duration });
|
||||
}
|
||||
unmuteUser(userId: number) {
|
||||
return this.send("unmuteuser", { user_id: userId });
|
||||
}
|
||||
}
|
||||
|
||||
const globalForRcon = globalThis as unknown as { rcon?: RconClient };
|
||||
|
||||
export const rcon =
|
||||
globalForRcon.rcon ??
|
||||
new RconClient(
|
||||
tcpTransport({
|
||||
host: env.RCON_HOST,
|
||||
port: env.RCON_PORT,
|
||||
timeoutMs: env.RCON_TIMEOUT_MS,
|
||||
maxRetries: env.RCON_MAX_RETRIES,
|
||||
}),
|
||||
);
|
||||
globalForRcon.rcon ??
|
||||
new RconClient(
|
||||
tcpTransport({
|
||||
host: env.RCON_HOST,
|
||||
port: env.RCON_PORT,
|
||||
timeoutMs: env.RCON_TIMEOUT_MS,
|
||||
maxRetries: env.RCON_MAX_RETRIES,
|
||||
}),
|
||||
);
|
||||
|
||||
if (env.NODE_ENV !== "production") globalForRcon.rcon = rcon;
|
||||
Reference in new issue
Block a user