erlc-api 3.3.0 → 3.3.2-a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -59
- package/README_ES.md +168 -0
- package/package.json +12 -5
- package/pnpm-workspace.yaml +2 -0
- package/src/classes/client.js +13 -2
- package/src/constants.js +5 -2
- package/src/erlc.js +29 -1
- package/src/errors/ErlcError.js +3 -0
- package/src/functions/global/resetGlobalKey.js +12 -8
- package/src/functions/server/getBans.js +11 -41
- package/src/functions/server/getCommandLogs.js +9 -41
- package/src/functions/server/getEmergencyCalls.js +15 -0
- package/src/functions/server/getJoinLogs.js +8 -41
- package/src/functions/server/getKillLogs.js +8 -41
- package/src/functions/server/getModcallLogs.js +8 -41
- package/src/functions/server/getPlayers.js +8 -41
- package/src/functions/server/getQueue.js +8 -41
- package/src/functions/server/getServer.js +50 -68
- package/src/functions/server/getStaff.js +8 -41
- package/src/functions/server/getVehicles.js +8 -41
- package/src/functions/server/requestServer.js +179 -0
- package/src/functions/server/runCommand.js +22 -53
- package/src/types/index.d.ts +91 -5
- package/src/utils/cache.js +63 -0
- package/src/utils/discord.js +99 -0
- package/src/utils/errorHandler.js +22 -1
- package/tests/cache.test.js +46 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
const { BASEURL, LEGACY_BASEURL } = require("../../constants.js");
|
|
2
|
+
const { processError } = require("../../utils/errorHandler.js");
|
|
3
|
+
const cache = require("../../utils/cache.js");
|
|
4
|
+
|
|
5
|
+
function assertServerToken(serverToken) {
|
|
6
|
+
if (!serverToken || typeof serverToken !== "string") {
|
|
7
|
+
throw new Error("Server token is required and must be a string");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function buildHeaders(serverToken, config, extraHeaders = {}) {
|
|
12
|
+
const headers = {
|
|
13
|
+
"Server-Key": serverToken,
|
|
14
|
+
...extraHeaders,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
if (config?.globalToken) {
|
|
18
|
+
headers["Authorization"] = config.globalToken;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return headers;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function getFetch() {
|
|
25
|
+
const { config } = require("../../erlc.js");
|
|
26
|
+
|
|
27
|
+
if (config?.fetch) {
|
|
28
|
+
return {
|
|
29
|
+
fetch: config.fetch,
|
|
30
|
+
config,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (typeof globalThis.fetch === "function") {
|
|
35
|
+
return {
|
|
36
|
+
fetch: globalThis.fetch,
|
|
37
|
+
config,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const fetch = await import("node-fetch");
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
fetch: fetch.default,
|
|
45
|
+
config,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function readError(res) {
|
|
50
|
+
return res.json().catch(() => ({ error: "Unknown API error" }));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildServerUrl(includes = []) {
|
|
54
|
+
const url = new URL(`${BASEURL}/server`);
|
|
55
|
+
|
|
56
|
+
for (const include of includes) {
|
|
57
|
+
url.searchParams.set(include, "true");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return url.toString();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function requestJson(url, options) {
|
|
64
|
+
const res = await options.fetch(url, options.init);
|
|
65
|
+
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
const errorData = await readError(res);
|
|
68
|
+
throw await processError(res, errorData);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return res.json();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function requestServer(serverToken, options = {}) {
|
|
75
|
+
assertServerToken(serverToken);
|
|
76
|
+
|
|
77
|
+
const {
|
|
78
|
+
endpoint = "server",
|
|
79
|
+
includes = [],
|
|
80
|
+
defaultValue = {},
|
|
81
|
+
transform = (data) => data,
|
|
82
|
+
} = options;
|
|
83
|
+
|
|
84
|
+
const { fetch, config } = await getFetch();
|
|
85
|
+
const useCache = !!config?.cache?.enabled;
|
|
86
|
+
const cacheExtras = includes.length ? includes.join(",") : "";
|
|
87
|
+
const key = cache.makeKey(endpoint, serverToken, cacheExtras);
|
|
88
|
+
|
|
89
|
+
if (useCache) {
|
|
90
|
+
const cached = cache.get(key);
|
|
91
|
+
if (cached) {
|
|
92
|
+
return cached;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const data = await requestJson(buildServerUrl(includes), {
|
|
97
|
+
fetch,
|
|
98
|
+
init: {
|
|
99
|
+
headers: buildHeaders(serverToken, config),
|
|
100
|
+
timeout: 10000,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const value = (await transform(data)) ?? defaultValue;
|
|
105
|
+
|
|
106
|
+
if (useCache) {
|
|
107
|
+
const ttlMs = cache.getTTL(endpoint, config);
|
|
108
|
+
cache.set(key, value, ttlMs);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function requestApi(serverToken, path, options = {}) {
|
|
115
|
+
assertServerToken(serverToken);
|
|
116
|
+
|
|
117
|
+
const {
|
|
118
|
+
baseUrl = BASEURL,
|
|
119
|
+
method = "GET",
|
|
120
|
+
endpoint = path,
|
|
121
|
+
body,
|
|
122
|
+
defaultValue = {},
|
|
123
|
+
transform = (data) => data,
|
|
124
|
+
timeout = 10000,
|
|
125
|
+
useCache = true,
|
|
126
|
+
} = options;
|
|
127
|
+
|
|
128
|
+
const { fetch, config } = await getFetch();
|
|
129
|
+
const shouldCache = method === "GET" && useCache && !!config?.cache?.enabled;
|
|
130
|
+
const key = cache.makeKey(endpoint, serverToken);
|
|
131
|
+
|
|
132
|
+
if (shouldCache) {
|
|
133
|
+
const cached = cache.get(key);
|
|
134
|
+
if (cached) {
|
|
135
|
+
return cached;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const headers = buildHeaders(
|
|
140
|
+
serverToken,
|
|
141
|
+
config,
|
|
142
|
+
body ? { "Content-Type": "application/json" } : {},
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const data = await requestJson(`${baseUrl}${path}`, {
|
|
146
|
+
fetch,
|
|
147
|
+
init: {
|
|
148
|
+
method,
|
|
149
|
+
headers,
|
|
150
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
151
|
+
timeout,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const value = (await transform(data)) ?? defaultValue;
|
|
156
|
+
|
|
157
|
+
if (shouldCache) {
|
|
158
|
+
const ttlMs = cache.getTTL(endpoint, config);
|
|
159
|
+
cache.set(key, value, ttlMs);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function requestLegacyServer(serverToken, path, options = {}) {
|
|
166
|
+
return requestApi(serverToken, path, {
|
|
167
|
+
...options,
|
|
168
|
+
baseUrl: LEGACY_BASEURL,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
assertServerToken,
|
|
174
|
+
buildHeaders,
|
|
175
|
+
getFetch,
|
|
176
|
+
requestServer,
|
|
177
|
+
requestApi,
|
|
178
|
+
requestLegacyServer,
|
|
179
|
+
};
|
|
@@ -1,61 +1,30 @@
|
|
|
1
|
-
const {
|
|
2
|
-
const { processError } = require("../../utils/errorHandler.js");
|
|
1
|
+
const { assertServerToken, requestApi } = require("./requestServer.js");
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
|
-
* Executes a command on the server
|
|
4
|
+
* Executes a command on the server.
|
|
6
5
|
* @param {string} serverToken - The server API key
|
|
7
6
|
* @param {string} command - The command to execute
|
|
8
7
|
* @returns {Promise<boolean>} Promise that resolves to true if command was executed successfully
|
|
9
8
|
*/
|
|
10
|
-
module.exports = (serverToken, command) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const requestBody = JSON.stringify({ command: command.trim() });
|
|
30
|
-
|
|
31
|
-
const headers = {
|
|
32
|
-
"Server-Key": serverToken,
|
|
33
|
-
"Content-Type": "application/json",
|
|
34
|
-
};
|
|
35
|
-
if (config?.globalToken) {
|
|
36
|
-
headers["Authorization"] = config.globalToken;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const res = await fetch.default(`${BASEURL}/server/command`, {
|
|
40
|
-
method: "POST",
|
|
41
|
-
headers: headers,
|
|
42
|
-
body: requestBody,
|
|
43
|
-
timeout: 15000, // 15 second timeout for commands
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
if (!res.ok) {
|
|
47
|
-
const errorData = await res
|
|
48
|
-
.json()
|
|
49
|
-
.catch(() => ({ error: "Unknown API error" }));
|
|
50
|
-
const error = await processError(res, errorData);
|
|
51
|
-
return reject(error);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Command executed successfully
|
|
55
|
-
resolve(true);
|
|
56
|
-
} catch (error) {
|
|
57
|
-
const processedError = await processError(error);
|
|
58
|
-
reject(processedError);
|
|
59
|
-
}
|
|
9
|
+
module.exports = async (serverToken, command) => {
|
|
10
|
+
assertServerToken(serverToken);
|
|
11
|
+
|
|
12
|
+
if (!command || typeof command !== "string") {
|
|
13
|
+
throw new Error("Command is required and must be a string");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const trimmedCommand = command.trim();
|
|
17
|
+
if (trimmedCommand.length === 0) {
|
|
18
|
+
throw new Error("Command cannot be empty");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
await requestApi(serverToken, "/server/command", {
|
|
22
|
+
method: "POST",
|
|
23
|
+
endpoint: "command",
|
|
24
|
+
body: { command: trimmedCommand },
|
|
25
|
+
timeout: 15000,
|
|
26
|
+
useCache: false,
|
|
60
27
|
});
|
|
28
|
+
|
|
29
|
+
return true;
|
|
61
30
|
};
|
package/src/types/index.d.ts
CHANGED
|
@@ -6,6 +6,9 @@ export class ErlcError extends Error {
|
|
|
6
6
|
severity?: string;
|
|
7
7
|
suggestions?: string[];
|
|
8
8
|
retryable?: boolean;
|
|
9
|
+
retryAfter?: number;
|
|
10
|
+
bucket?: string;
|
|
11
|
+
commandId?: string;
|
|
9
12
|
timestamp: string;
|
|
10
13
|
originalError?: Error;
|
|
11
14
|
|
|
@@ -29,9 +32,23 @@ export interface ErrorInfo {
|
|
|
29
32
|
|
|
30
33
|
export interface ClientConfig {
|
|
31
34
|
globalToken?: string; // The ER:LC global API token
|
|
35
|
+
cache?: {
|
|
36
|
+
enabled?: boolean;
|
|
37
|
+
ttlMs?: Record<string, number>;
|
|
38
|
+
staleWhileRevalidate?: boolean;
|
|
39
|
+
};
|
|
40
|
+
logger?: {
|
|
41
|
+
info?: (...args: any[]) => void;
|
|
42
|
+
warn?: (...args: any[]) => void;
|
|
43
|
+
error?: (...args: any[]) => void;
|
|
44
|
+
};
|
|
45
|
+
fetch?: (url: string, init?: any) => Promise<any>;
|
|
32
46
|
}
|
|
33
47
|
|
|
34
|
-
export const
|
|
48
|
+
export const API_ORIGIN = "https://api.erlc.gg";
|
|
49
|
+
export const API_VERSION = "v2";
|
|
50
|
+
export const BASEURL = "https://api.erlc.gg/v2";
|
|
51
|
+
export const LEGACY_BASEURL = "https://api.erlc.gg/v1";
|
|
35
52
|
|
|
36
53
|
type PlayerId = string;
|
|
37
54
|
type PlayerName = string;
|
|
@@ -55,11 +72,30 @@ export interface ServerStatus {
|
|
|
55
72
|
AccVerifiedReq: "Disabled" | "Email" | "Phone/ID"; // The level of verification roblox accounts need to join the private server
|
|
56
73
|
TeamBalance: boolean; // If team balance is enabled or not
|
|
57
74
|
VanityURL: string; // The vanity URL to join the server
|
|
75
|
+
Players?: ServerPlayer[];
|
|
76
|
+
Staff?: ServerStaff;
|
|
77
|
+
JoinLogs?: JoinLog[];
|
|
78
|
+
Queue?: number[];
|
|
79
|
+
KillLogs?: KillLog[];
|
|
80
|
+
CommandLogs?: CommandLog[];
|
|
81
|
+
ModCalls?: ModcallLog[];
|
|
82
|
+
EmergencyCalls?: EmergencyCall[];
|
|
83
|
+
Vehicles?: VehiclesLog[];
|
|
58
84
|
}
|
|
59
85
|
|
|
60
86
|
export interface ServerPlayer {
|
|
61
87
|
Player: ErlcPlayer;
|
|
88
|
+
Team?: string;
|
|
89
|
+
Callsign?: string | null;
|
|
90
|
+
Location?: {
|
|
91
|
+
LocationX?: number;
|
|
92
|
+
LocationZ?: number;
|
|
93
|
+
PostalCode?: string;
|
|
94
|
+
StreetName?: string;
|
|
95
|
+
BuildingNumber?: string;
|
|
96
|
+
};
|
|
62
97
|
Permission: ErlcPlayerPermission;
|
|
98
|
+
WantedStars?: number;
|
|
63
99
|
}
|
|
64
100
|
|
|
65
101
|
export interface JoinLog {
|
|
@@ -91,13 +127,39 @@ export type ServerBan = Record<PlayerId, PlayerName>;
|
|
|
91
127
|
export interface VehiclesLog {
|
|
92
128
|
Texture: string | null;
|
|
93
129
|
Name: string;
|
|
94
|
-
Owner:
|
|
130
|
+
Owner: string;
|
|
131
|
+
Plate?: string;
|
|
132
|
+
ColorHex?: string;
|
|
133
|
+
ColorName?: string;
|
|
95
134
|
}
|
|
96
135
|
|
|
97
136
|
export interface ServerStaff {
|
|
98
|
-
CoOwners: number[];
|
|
99
137
|
Admins: Record<string, string>;
|
|
100
138
|
Mods: Record<string, string>;
|
|
139
|
+
Helpers?: Record<string, string>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface EmergencyCall {
|
|
143
|
+
Team: string;
|
|
144
|
+
Caller: number;
|
|
145
|
+
Players: number[];
|
|
146
|
+
Position: [number, number];
|
|
147
|
+
StartedAt: number;
|
|
148
|
+
CallNumber: number;
|
|
149
|
+
Description: string;
|
|
150
|
+
PositionDescriptor: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface ServerIncludeOptions {
|
|
154
|
+
players?: boolean;
|
|
155
|
+
staff?: boolean;
|
|
156
|
+
joinLogs?: boolean;
|
|
157
|
+
queue?: boolean;
|
|
158
|
+
killLogs?: boolean;
|
|
159
|
+
commandLogs?: boolean;
|
|
160
|
+
modCalls?: boolean;
|
|
161
|
+
emergencyCalls?: boolean;
|
|
162
|
+
vehicles?: boolean;
|
|
101
163
|
}
|
|
102
164
|
|
|
103
165
|
export interface VSMCommandBody {
|
|
@@ -109,9 +171,13 @@ export function getCommandLogs(serverToken: string): Promise<CommandLog[]>;
|
|
|
109
171
|
export function getJoinLogs(serverToken: string): Promise<JoinLog[]>;
|
|
110
172
|
export function getKillLogs(serverToken: string): Promise<KillLog[]>;
|
|
111
173
|
export function getModcallLogs(serverToken: string): Promise<ModcallLog[]>;
|
|
174
|
+
export function getEmergencyCalls(serverToken: string): Promise<EmergencyCall[]>;
|
|
112
175
|
export function getPlayers(serverToken: string): Promise<ServerPlayer[]>;
|
|
113
176
|
export function getQueue(serverToken: string): Promise<number[]>;
|
|
114
|
-
export function getServer(
|
|
177
|
+
export function getServer(
|
|
178
|
+
serverToken: string,
|
|
179
|
+
options?: ServerIncludeOptions,
|
|
180
|
+
): Promise<ServerStatus>;
|
|
115
181
|
export function getStaff(serverToken: string): Promise<ServerStaff>;
|
|
116
182
|
export function getVehicles(serverToken: string): Promise<VehiclesLog[]>;
|
|
117
183
|
export function runCommand(
|
|
@@ -122,6 +188,26 @@ export function runCommand(
|
|
|
122
188
|
export function resetGlobalKey(): Promise<any>;
|
|
123
189
|
|
|
124
190
|
export class Client {
|
|
125
|
-
constructor(options
|
|
191
|
+
constructor(options?: ClientConfig);
|
|
126
192
|
config(): ClientConfig;
|
|
127
193
|
}
|
|
194
|
+
|
|
195
|
+
export const utils: {
|
|
196
|
+
cache: {
|
|
197
|
+
DEFAULT_TTLS: Record<string, number>;
|
|
198
|
+
makeKey: (endpoint: string, serverToken: string, extras?: string) => string;
|
|
199
|
+
getTTL: (endpoint: string, config: ClientConfig) => number;
|
|
200
|
+
get: (key: string) => any;
|
|
201
|
+
set: (key: string, value: any, ttlMs: number) => void;
|
|
202
|
+
invalidate: (key: string) => void;
|
|
203
|
+
invalidateByPrefix: (prefix: string) => void;
|
|
204
|
+
};
|
|
205
|
+
discord: {
|
|
206
|
+
formatServerStatus: (server: ServerStatus) => any;
|
|
207
|
+
formatPlayers: (players: ServerPlayer[]) => any;
|
|
208
|
+
formatKillLog: (log: KillLog) => any;
|
|
209
|
+
formatJoinLog: (log: JoinLog) => any;
|
|
210
|
+
formatCommandLog: (log: CommandLog) => any;
|
|
211
|
+
formatModCall: (log: ModcallLog) => any;
|
|
212
|
+
};
|
|
213
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const DEFAULT_TTLS = {
|
|
2
|
+
server: 10000,
|
|
3
|
+
players: 3000,
|
|
4
|
+
vehicles: 5000,
|
|
5
|
+
joinlogs: 3000,
|
|
6
|
+
killlogs: 3000,
|
|
7
|
+
commandlogs: 3000,
|
|
8
|
+
modcalls: 3000,
|
|
9
|
+
emergencycalls: 3000,
|
|
10
|
+
staff: 10000,
|
|
11
|
+
queue: 2000,
|
|
12
|
+
bans: 10000,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const store = new Map();
|
|
16
|
+
|
|
17
|
+
function now() {
|
|
18
|
+
return Date.now();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function makeKey(endpoint, serverToken, extras = "") {
|
|
22
|
+
return `${endpoint}:${serverToken}${extras ? `:${extras}` : ""}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getTTL(endpoint, config) {
|
|
26
|
+
const ttlMap = config?.cache?.ttlMs || DEFAULT_TTLS;
|
|
27
|
+
const ttl = ttlMap[endpoint];
|
|
28
|
+
return typeof ttl === "number" && ttl > 0 ? ttl : 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function get(key) {
|
|
32
|
+
const entry = store.get(key);
|
|
33
|
+
if (!entry) return null;
|
|
34
|
+
if (entry.expiresAt && entry.expiresAt > now()) {
|
|
35
|
+
return entry.value;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function set(key, value, ttlMs) {
|
|
41
|
+
if (!ttlMs || ttlMs <= 0) return;
|
|
42
|
+
store.set(key, { value, expiresAt: now() + ttlMs });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function invalidate(key) {
|
|
46
|
+
store.delete(key);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function invalidateByPrefix(prefix) {
|
|
50
|
+
for (const k of store.keys()) {
|
|
51
|
+
if (k.startsWith(prefix)) store.delete(k);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = {
|
|
56
|
+
DEFAULT_TTLS,
|
|
57
|
+
makeKey,
|
|
58
|
+
getTTL,
|
|
59
|
+
get,
|
|
60
|
+
set,
|
|
61
|
+
invalidate,
|
|
62
|
+
invalidateByPrefix,
|
|
63
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
function formatServerStatus(server) {
|
|
2
|
+
return {
|
|
3
|
+
title: "Server Status",
|
|
4
|
+
color: 0x1f8b4c,
|
|
5
|
+
fields: [
|
|
6
|
+
{ name: "Name", value: server.Name || "-", inline: true },
|
|
7
|
+
{
|
|
8
|
+
name: "Players",
|
|
9
|
+
value: `${server.CurrentPlayers}/${server.MaxPlayers}`,
|
|
10
|
+
inline: true,
|
|
11
|
+
},
|
|
12
|
+
{ name: "Owner", value: server.OwnerUsername || "-", inline: true },
|
|
13
|
+
{
|
|
14
|
+
name: "Co-Owners",
|
|
15
|
+
value:
|
|
16
|
+
Array.isArray(server.CoOwnerUsernames) &&
|
|
17
|
+
server.CoOwnerUsernames.length
|
|
18
|
+
? server.CoOwnerUsernames.join(", ")
|
|
19
|
+
: "None",
|
|
20
|
+
inline: false,
|
|
21
|
+
},
|
|
22
|
+
{ name: "Join Key", value: server.JoinKey || "-", inline: true },
|
|
23
|
+
{ name: "Vanity URL", value: server.VanityURL || "-", inline: false },
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function formatPlayers(players) {
|
|
29
|
+
const total = Array.isArray(players) ? players.length : 0;
|
|
30
|
+
const value =
|
|
31
|
+
total > 0
|
|
32
|
+
? players
|
|
33
|
+
.slice(0, 10)
|
|
34
|
+
.map((p) => `• ${p.Player} (${p.Permission})`)
|
|
35
|
+
.join("\n")
|
|
36
|
+
: "No players online";
|
|
37
|
+
return {
|
|
38
|
+
title: `Players (${total})`,
|
|
39
|
+
color: 0x2f6fed,
|
|
40
|
+
description: value,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatKillLog(log) {
|
|
45
|
+
return {
|
|
46
|
+
title: "Kill Log",
|
|
47
|
+
color: 0xff5555,
|
|
48
|
+
fields: [
|
|
49
|
+
{ name: "Killer", value: log.Killer || "-", inline: true },
|
|
50
|
+
{ name: "Killed", value: log.Killed || "-", inline: true },
|
|
51
|
+
{ name: "Timestamp", value: String(log.Timestamp || "-"), inline: true },
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatJoinLog(log) {
|
|
57
|
+
return {
|
|
58
|
+
title: log.Join ? "Player Joined" : "Player Left",
|
|
59
|
+
color: log.Join ? 0x50fa7b : 0xf1fa8c,
|
|
60
|
+
fields: [
|
|
61
|
+
{ name: "Player", value: log.Player || "-", inline: true },
|
|
62
|
+
{ name: "Timestamp", value: String(log.Timestamp || "-"), inline: true },
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function formatCommandLog(log) {
|
|
68
|
+
return {
|
|
69
|
+
title: "Command Executed",
|
|
70
|
+
color: 0xbd93f9,
|
|
71
|
+
fields: [
|
|
72
|
+
{ name: "Player", value: log.Player || "-", inline: true },
|
|
73
|
+
{ name: "Command", value: log.Command || "-", inline: false },
|
|
74
|
+
{ name: "Timestamp", value: String(log.Timestamp || "-"), inline: true },
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatModCall(log) {
|
|
80
|
+
return {
|
|
81
|
+
title: "Mod Call",
|
|
82
|
+
color: 0x8be9fd,
|
|
83
|
+
fields: [
|
|
84
|
+
{ name: "Caller", value: log.Caller || "-", inline: true },
|
|
85
|
+
{ name: "Moderator", value: log.Moderator || "Unanswered", inline: true },
|
|
86
|
+
{ name: "Timestamp", value: String(log.Timestamp || "-"), inline: true },
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
formatServerStatus,
|
|
93
|
+
formatPlayers,
|
|
94
|
+
formatKillLog,
|
|
95
|
+
formatJoinLog,
|
|
96
|
+
formatCommandLog,
|
|
97
|
+
formatModCall,
|
|
98
|
+
};
|
|
99
|
+
|
|
@@ -19,12 +19,24 @@ function handleApiError(response, errorData) {
|
|
|
19
19
|
const errorInfo = getErrorInfo(errorData.code);
|
|
20
20
|
const suggestions = getSuggestedActions(errorData.code);
|
|
21
21
|
|
|
22
|
-
const
|
|
22
|
+
const apiMessage = errorData.message || errorData.error;
|
|
23
|
+
const message = apiMessage
|
|
24
|
+
? `${errorInfo.message}: ${apiMessage}`
|
|
25
|
+
: `${errorInfo.message}: ${errorInfo.description}`;
|
|
23
26
|
const error = new ErlcError(message, errorData.code, status);
|
|
24
27
|
error.category = errorInfo.category;
|
|
25
28
|
error.severity = errorInfo.severity;
|
|
26
29
|
error.suggestions = suggestions;
|
|
27
30
|
error.retryable = isRetryableError(errorData.code);
|
|
31
|
+
if (typeof errorData.retry_after === "number") {
|
|
32
|
+
error.retryAfter = errorData.retry_after;
|
|
33
|
+
}
|
|
34
|
+
if (errorData.bucket) {
|
|
35
|
+
error.bucket = errorData.bucket;
|
|
36
|
+
}
|
|
37
|
+
if (errorData.commandId) {
|
|
38
|
+
error.commandId = errorData.commandId;
|
|
39
|
+
}
|
|
28
40
|
|
|
29
41
|
return error;
|
|
30
42
|
}
|
|
@@ -80,6 +92,15 @@ function handleApiError(response, errorData) {
|
|
|
80
92
|
error.category = "HTTP_ERROR";
|
|
81
93
|
error.severity = status >= 500 ? "HIGH" : "MEDIUM";
|
|
82
94
|
error.retryable = [429, 500, 502, 503].includes(status);
|
|
95
|
+
if (typeof errorData?.retry_after === "number") {
|
|
96
|
+
error.retryAfter = errorData.retry_after;
|
|
97
|
+
}
|
|
98
|
+
if (errorData?.bucket) {
|
|
99
|
+
error.bucket = errorData.bucket;
|
|
100
|
+
}
|
|
101
|
+
if (errorData?.commandId) {
|
|
102
|
+
error.commandId = errorData.commandId;
|
|
103
|
+
}
|
|
83
104
|
|
|
84
105
|
return error;
|
|
85
106
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const erlc = require("../src/erlc.js");
|
|
2
|
+
const getPlayers = require("../src/functions/server/getPlayers.js");
|
|
3
|
+
|
|
4
|
+
describe("Optional cache", () => {
|
|
5
|
+
const serverToken = "test-server";
|
|
6
|
+
let fetchCalls = 0;
|
|
7
|
+
let lastUrl = "";
|
|
8
|
+
|
|
9
|
+
const mockFetch = async (url, opts) => {
|
|
10
|
+
fetchCalls += 1;
|
|
11
|
+
lastUrl = url;
|
|
12
|
+
return {
|
|
13
|
+
ok: true,
|
|
14
|
+
status: 200,
|
|
15
|
+
json: async () => ({
|
|
16
|
+
Players: [{ Player: "User:123", Permission: "Normal" }],
|
|
17
|
+
}),
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
fetchCalls = 0;
|
|
23
|
+
lastUrl = "";
|
|
24
|
+
erlc.config.cache.enabled = false;
|
|
25
|
+
erlc.config.fetch = mockFetch;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("Cache disabled by default (no cache hits)", async () => {
|
|
29
|
+
const res1 = await getPlayers(serverToken);
|
|
30
|
+
const res2 = await getPlayers(serverToken);
|
|
31
|
+
expect(Array.isArray(res1)).toBe(true);
|
|
32
|
+
expect(Array.isArray(res2)).toBe(true);
|
|
33
|
+
expect(lastUrl).toBe("https://api.erlc.gg/v2/server?Players=true");
|
|
34
|
+
expect(fetchCalls).toBe(2);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("Cache enabled returns cached data on subsequent call", async () => {
|
|
38
|
+
erlc.config.cache.enabled = true;
|
|
39
|
+
erlc.config.cache.ttlMs.players = 5000;
|
|
40
|
+
const res1 = await getPlayers(serverToken);
|
|
41
|
+
const res2 = await getPlayers(serverToken);
|
|
42
|
+
expect(Array.isArray(res1)).toBe(true);
|
|
43
|
+
expect(Array.isArray(res2)).toBe(true);
|
|
44
|
+
expect(fetchCalls).toBe(1);
|
|
45
|
+
});
|
|
46
|
+
});
|