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.
@@ -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 BASEURL = "https://api.policeroleplay.community/v1";
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: ErlcPlayer;
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(serverToken: string): Promise<ServerStatus>;
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: ClientConfig);
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 message = `${errorInfo.message}: ${errorInfo.description}`;
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
+ });