erlc-api 3.3.1 → 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 +16 -6
- package/README_ES.md +33 -13
- 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
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
|
+
});
|