erlc-v2 1.0.0 → 1.1.0
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 +608 -380
- package/index.d.ts +143 -2
- package/package.json +7 -4
- package/src/Client.js +74 -1
- package/src/api/LocalApiServer.js +538 -0
- package/src/events/Poller.js +14 -0
- package/src/events/diff.js +10 -1
- package/src/util/constants.js +11 -0
- package/src/util/errors.js +1 -1
- package/src/util/normalize.js +1 -0
- package/src/util/vehicleSearch.js +120 -0
- package/src/util/webhook.js +204 -0
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
const http = require("http");
|
|
2
|
+
const { QUERY_FLAG_MAP } = require("../util/constants");
|
|
3
|
+
const { searchVehicles } = require("../util/vehicleSearch");
|
|
4
|
+
const {
|
|
5
|
+
getHeader,
|
|
6
|
+
verifyWebhookSignature,
|
|
7
|
+
normalizeWebhookPayload,
|
|
8
|
+
} = require("../util/webhook");
|
|
9
|
+
|
|
10
|
+
function cleanPath(value, fallback) {
|
|
11
|
+
const input = String(value || fallback || "/").trim();
|
|
12
|
+
const withLeadingSlash = input.startsWith("/") ? input : `/${input}`;
|
|
13
|
+
if (withLeadingSlash.length > 1 && withLeadingSlash.endsWith("/")) {
|
|
14
|
+
return withLeadingSlash.slice(0, -1);
|
|
15
|
+
}
|
|
16
|
+
return withLeadingSlash;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function buildUrl(base, path) {
|
|
20
|
+
if (!base) return null;
|
|
21
|
+
return new URL(path, base.endsWith("/") ? base : `${base}/`).toString();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function sendJson(res, status, payload) {
|
|
25
|
+
const body = Buffer.from(JSON.stringify(payload));
|
|
26
|
+
res.statusCode = status;
|
|
27
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
28
|
+
res.setHeader("Content-Length", body.length);
|
|
29
|
+
res.end(body);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sendEmpty(res, status) {
|
|
33
|
+
res.statusCode = status;
|
|
34
|
+
res.end();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readBody(req) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const chunks = [];
|
|
40
|
+
req.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
41
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
42
|
+
req.on("error", reject);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseJson(rawBody) {
|
|
47
|
+
if (!rawBody || rawBody.length === 0) return {};
|
|
48
|
+
return JSON.parse(rawBody.toString("utf8"));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseBool(value) {
|
|
52
|
+
if (typeof value !== "string") return false;
|
|
53
|
+
const text = value.trim().toLowerCase();
|
|
54
|
+
return text === "true" || text === "1" || text === "yes";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function bodyPreview(body) {
|
|
58
|
+
if (!body || typeof body !== "object") return body ?? null;
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(JSON.stringify(body));
|
|
61
|
+
} catch {
|
|
62
|
+
return { message: "unserializable body" };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getRoutePath(url, basePath) {
|
|
67
|
+
if (url.pathname === basePath) return "";
|
|
68
|
+
if (!url.pathname.startsWith(`${basePath}/`)) return null;
|
|
69
|
+
return url.pathname.slice(basePath.length);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class LocalApiServer {
|
|
73
|
+
constructor(client, options = {}) {
|
|
74
|
+
this.client = client;
|
|
75
|
+
this.options = options || {};
|
|
76
|
+
this.server = null;
|
|
77
|
+
this.startPromise = null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
info() {
|
|
81
|
+
const basePath = cleanPath(this.options.path, "/erlc");
|
|
82
|
+
const webhookPath = cleanPath(
|
|
83
|
+
this.options.webhookPath || `${basePath}/events`,
|
|
84
|
+
`${basePath}/events`,
|
|
85
|
+
);
|
|
86
|
+
const address = this.server?.address();
|
|
87
|
+
const host =
|
|
88
|
+
typeof this.options.host === "string" && this.options.host.trim()
|
|
89
|
+
? this.options.host.trim()
|
|
90
|
+
: "127.0.0.1";
|
|
91
|
+
const port =
|
|
92
|
+
address && typeof address === "object" ? address.port : this.options.port;
|
|
93
|
+
const localHost =
|
|
94
|
+
host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
|
|
95
|
+
const localUrl =
|
|
96
|
+
Number.isInteger(port) && port >= 0
|
|
97
|
+
? `http://${localHost}:${port}${basePath}`
|
|
98
|
+
: null;
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
running: Boolean(this.server?.listening),
|
|
102
|
+
host,
|
|
103
|
+
port: Number.isInteger(port) ? port : null,
|
|
104
|
+
path: basePath,
|
|
105
|
+
webhookPath,
|
|
106
|
+
localUrl,
|
|
107
|
+
webhookUrl: buildUrl(this.options.publicUrl, webhookPath),
|
|
108
|
+
publicUrl: this.options.publicUrl || null,
|
|
109
|
+
tokenEnabled: Boolean(this.options.token),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async start() {
|
|
114
|
+
if (this.server?.listening) {
|
|
115
|
+
return this.info();
|
|
116
|
+
}
|
|
117
|
+
if (this.startPromise) {
|
|
118
|
+
return this.startPromise;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const port = Number(this.options.port);
|
|
122
|
+
if (!Number.isInteger(port) || port < 0) {
|
|
123
|
+
throw new Error("Local API requires a valid api.port");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const host =
|
|
127
|
+
typeof this.options.host === "string" && this.options.host.trim()
|
|
128
|
+
? this.options.host.trim()
|
|
129
|
+
: "127.0.0.1";
|
|
130
|
+
|
|
131
|
+
this.server = http.createServer((req, res) => {
|
|
132
|
+
void this._handle(req, res);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
this.startPromise = new Promise((resolve, reject) => {
|
|
136
|
+
const onError = (error) => {
|
|
137
|
+
this.server?.off("listening", onListening);
|
|
138
|
+
this.startPromise = null;
|
|
139
|
+
reject(error);
|
|
140
|
+
};
|
|
141
|
+
const onListening = () => {
|
|
142
|
+
this.server?.off("error", onError);
|
|
143
|
+
this.startPromise = null;
|
|
144
|
+
this.client.logger.info({
|
|
145
|
+
msg: "local_api_started",
|
|
146
|
+
...this.info(),
|
|
147
|
+
});
|
|
148
|
+
resolve(this.info());
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
this.server.once("error", onError);
|
|
152
|
+
this.server.once("listening", onListening);
|
|
153
|
+
this.server.listen(port, host);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return this.startPromise;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
stop() {
|
|
160
|
+
if (!this.server) return;
|
|
161
|
+
const server = this.server;
|
|
162
|
+
this.server = null;
|
|
163
|
+
this.startPromise = null;
|
|
164
|
+
server.close();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async _handle(req, res) {
|
|
168
|
+
const info = this.info();
|
|
169
|
+
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
|
|
170
|
+
const startedAt = Date.now();
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
if (url.pathname === info.webhookPath && req.method === "POST") {
|
|
174
|
+
await this._handleWebhook(req, res);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const routePath = getRoutePath(url, info.path);
|
|
179
|
+
if (routePath === null) {
|
|
180
|
+
this._recordRequest({
|
|
181
|
+
req,
|
|
182
|
+
url,
|
|
183
|
+
startedAt,
|
|
184
|
+
kind: "route",
|
|
185
|
+
status: 404,
|
|
186
|
+
note: "not_found",
|
|
187
|
+
});
|
|
188
|
+
sendJson(res, 404, { message: "Not found" });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!this._isAuthorized(req)) {
|
|
193
|
+
this._recordRequest({
|
|
194
|
+
req,
|
|
195
|
+
url,
|
|
196
|
+
startedAt,
|
|
197
|
+
kind: "route",
|
|
198
|
+
status: 401,
|
|
199
|
+
note: "unauthorized",
|
|
200
|
+
});
|
|
201
|
+
sendJson(res, 401, { message: "Unauthorized" });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (req.method === "GET" && routePath === "") {
|
|
206
|
+
this._recordRequest({
|
|
207
|
+
req,
|
|
208
|
+
url,
|
|
209
|
+
startedAt,
|
|
210
|
+
kind: "route",
|
|
211
|
+
status: 200,
|
|
212
|
+
});
|
|
213
|
+
sendJson(res, 200, this.info());
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (req.method === "GET" && routePath === "/health") {
|
|
218
|
+
this._recordRequest({
|
|
219
|
+
req,
|
|
220
|
+
url,
|
|
221
|
+
startedAt,
|
|
222
|
+
kind: "route",
|
|
223
|
+
status: 200,
|
|
224
|
+
});
|
|
225
|
+
sendJson(res, 200, {
|
|
226
|
+
ok: true,
|
|
227
|
+
polling: this.client.poller?.running === true,
|
|
228
|
+
disconnected: this.client.state?.disconnected === true,
|
|
229
|
+
});
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (req.method === "GET" && routePath === "/server") {
|
|
234
|
+
const snapshot = await this.client.server.fetch(
|
|
235
|
+
this._getServerFlags(url.searchParams),
|
|
236
|
+
{
|
|
237
|
+
bypassCache: parseBool(url.searchParams.get("bypassCache")),
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
this._recordRequest({
|
|
241
|
+
req,
|
|
242
|
+
url,
|
|
243
|
+
startedAt,
|
|
244
|
+
kind: "route",
|
|
245
|
+
status: 200,
|
|
246
|
+
note: "server_snapshot",
|
|
247
|
+
});
|
|
248
|
+
sendJson(res, 200, snapshot);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (req.method === "GET" && routePath === "/players") {
|
|
253
|
+
const players = await this.client.players.list({
|
|
254
|
+
bypassCache: parseBool(url.searchParams.get("bypassCache")),
|
|
255
|
+
});
|
|
256
|
+
this._recordRequest({
|
|
257
|
+
req,
|
|
258
|
+
url,
|
|
259
|
+
startedAt,
|
|
260
|
+
kind: "route",
|
|
261
|
+
status: 200,
|
|
262
|
+
note: `players=${players.length}`,
|
|
263
|
+
});
|
|
264
|
+
sendJson(res, 200, {
|
|
265
|
+
count: players.length,
|
|
266
|
+
players,
|
|
267
|
+
});
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (req.method === "GET" && routePath === "/vehicles") {
|
|
272
|
+
const vehicles = await this.client.vehicles.list({
|
|
273
|
+
bypassCache: parseBool(url.searchParams.get("bypassCache")),
|
|
274
|
+
});
|
|
275
|
+
const filtered = searchVehicles(vehicles, {
|
|
276
|
+
query: url.searchParams.get("query") || url.searchParams.get("search"),
|
|
277
|
+
plate: url.searchParams.get("plate"),
|
|
278
|
+
owner: url.searchParams.get("owner"),
|
|
279
|
+
name: url.searchParams.get("name"),
|
|
280
|
+
color: url.searchParams.get("color"),
|
|
281
|
+
texture: url.searchParams.get("texture"),
|
|
282
|
+
exact: parseBool(url.searchParams.get("exact")),
|
|
283
|
+
limit: url.searchParams.get("limit"),
|
|
284
|
+
});
|
|
285
|
+
this._recordRequest({
|
|
286
|
+
req,
|
|
287
|
+
url,
|
|
288
|
+
startedAt,
|
|
289
|
+
kind: "route",
|
|
290
|
+
status: 200,
|
|
291
|
+
note: `vehicles=${filtered.length}`,
|
|
292
|
+
});
|
|
293
|
+
sendJson(res, 200, {
|
|
294
|
+
count: filtered.length,
|
|
295
|
+
vehicles: filtered,
|
|
296
|
+
});
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (req.method === "GET" && routePath.startsWith("/vehicles/")) {
|
|
301
|
+
const plate = decodeURIComponent(routePath.slice("/vehicles/".length));
|
|
302
|
+
const vehicle = await this.client.vehicles.findByPlate(plate, {
|
|
303
|
+
bypassCache: parseBool(url.searchParams.get("bypassCache")),
|
|
304
|
+
});
|
|
305
|
+
if (!vehicle) {
|
|
306
|
+
this._recordRequest({
|
|
307
|
+
req,
|
|
308
|
+
url,
|
|
309
|
+
startedAt,
|
|
310
|
+
kind: "route",
|
|
311
|
+
status: 404,
|
|
312
|
+
note: `plate=${plate}`,
|
|
313
|
+
});
|
|
314
|
+
sendJson(res, 404, { message: "Vehicle not found" });
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
this._recordRequest({
|
|
318
|
+
req,
|
|
319
|
+
url,
|
|
320
|
+
startedAt,
|
|
321
|
+
kind: "route",
|
|
322
|
+
status: 200,
|
|
323
|
+
note: `plate=${plate}`,
|
|
324
|
+
});
|
|
325
|
+
sendJson(res, 200, vehicle);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (req.method === "GET" && routePath === "/emergency-calls") {
|
|
330
|
+
const calls = await this.client.logs.emergencyCalls({
|
|
331
|
+
bypassCache: parseBool(url.searchParams.get("bypassCache")),
|
|
332
|
+
});
|
|
333
|
+
this._recordRequest({
|
|
334
|
+
req,
|
|
335
|
+
url,
|
|
336
|
+
startedAt,
|
|
337
|
+
kind: "route",
|
|
338
|
+
status: 200,
|
|
339
|
+
note: `emergencyCalls=${calls.length}`,
|
|
340
|
+
});
|
|
341
|
+
sendJson(res, 200, {
|
|
342
|
+
count: calls.length,
|
|
343
|
+
emergencyCalls: calls,
|
|
344
|
+
});
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (req.method === "POST" && routePath === "/command") {
|
|
349
|
+
const rawBody = await readBody(req);
|
|
350
|
+
const body = parseJson(rawBody);
|
|
351
|
+
const result = await this.client.commands.execute(body.command);
|
|
352
|
+
this._recordRequest({
|
|
353
|
+
req,
|
|
354
|
+
url,
|
|
355
|
+
startedAt,
|
|
356
|
+
kind: "route",
|
|
357
|
+
status: 200,
|
|
358
|
+
body,
|
|
359
|
+
note: "command",
|
|
360
|
+
});
|
|
361
|
+
sendJson(res, 200, result);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
this._recordRequest({
|
|
366
|
+
req,
|
|
367
|
+
url,
|
|
368
|
+
startedAt,
|
|
369
|
+
kind: "route",
|
|
370
|
+
status: 404,
|
|
371
|
+
note: "not_found",
|
|
372
|
+
});
|
|
373
|
+
sendJson(res, 404, { message: "Not found" });
|
|
374
|
+
} catch (error) {
|
|
375
|
+
const status = Number(error?.status) || 500;
|
|
376
|
+
this._recordRequest({
|
|
377
|
+
req,
|
|
378
|
+
url,
|
|
379
|
+
startedAt,
|
|
380
|
+
kind: "route",
|
|
381
|
+
status,
|
|
382
|
+
note: error?.message || "internal_error",
|
|
383
|
+
});
|
|
384
|
+
sendJson(res, status, {
|
|
385
|
+
message: error?.message || "Internal server error",
|
|
386
|
+
status,
|
|
387
|
+
code: error?.errorCode ?? null,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async _handleWebhook(req, res) {
|
|
393
|
+
const timestamp = getHeader(req, "x-signature-timestamp");
|
|
394
|
+
const signature = getHeader(req, "x-signature-ed25519");
|
|
395
|
+
const rawBody = await readBody(req);
|
|
396
|
+
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
|
|
397
|
+
const startedAt = Date.now();
|
|
398
|
+
|
|
399
|
+
if (!timestamp || !signature) {
|
|
400
|
+
this._recordRequest({
|
|
401
|
+
req,
|
|
402
|
+
url,
|
|
403
|
+
startedAt,
|
|
404
|
+
kind: "webhook",
|
|
405
|
+
status: 400,
|
|
406
|
+
note: "missing_signature_headers",
|
|
407
|
+
});
|
|
408
|
+
sendJson(res, 400, { message: "Missing signature headers" });
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (!verifyWebhookSignature(timestamp, signature, rawBody)) {
|
|
413
|
+
this._recordRequest({
|
|
414
|
+
req,
|
|
415
|
+
url,
|
|
416
|
+
startedAt,
|
|
417
|
+
kind: "webhook",
|
|
418
|
+
status: 401,
|
|
419
|
+
note: "invalid_signature",
|
|
420
|
+
});
|
|
421
|
+
sendJson(res, 401, { message: "Invalid signature" });
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
let body;
|
|
426
|
+
try {
|
|
427
|
+
body = parseJson(rawBody);
|
|
428
|
+
} catch {
|
|
429
|
+
this._recordRequest({
|
|
430
|
+
req,
|
|
431
|
+
url,
|
|
432
|
+
startedAt,
|
|
433
|
+
kind: "webhook",
|
|
434
|
+
status: 400,
|
|
435
|
+
note: "invalid_json",
|
|
436
|
+
});
|
|
437
|
+
sendJson(res, 400, { message: "Invalid JSON body" });
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const payload = normalizeWebhookPayload(body, {
|
|
442
|
+
timestamp,
|
|
443
|
+
signature,
|
|
444
|
+
rawBody,
|
|
445
|
+
headers: req.headers,
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
this._recordRequest({
|
|
449
|
+
req,
|
|
450
|
+
url,
|
|
451
|
+
startedAt,
|
|
452
|
+
kind: "webhook",
|
|
453
|
+
status: 204,
|
|
454
|
+
type: payload.type,
|
|
455
|
+
body,
|
|
456
|
+
note: "signature_valid",
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
this.client.emit("webhook", payload);
|
|
460
|
+
if (payload.type === "command") {
|
|
461
|
+
this.client.emit("webhookCommand", payload);
|
|
462
|
+
}
|
|
463
|
+
if (payload.type === "emergencyCall") {
|
|
464
|
+
this.client.emit("webhookEmergencyCall", payload);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
sendEmpty(res, 204);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
_recordRequest({
|
|
471
|
+
req,
|
|
472
|
+
url,
|
|
473
|
+
startedAt,
|
|
474
|
+
kind,
|
|
475
|
+
status,
|
|
476
|
+
type = null,
|
|
477
|
+
body = null,
|
|
478
|
+
note = "",
|
|
479
|
+
}) {
|
|
480
|
+
const payload = {
|
|
481
|
+
method: req.method || "GET",
|
|
482
|
+
path: url.pathname,
|
|
483
|
+
query: Object.fromEntries(url.searchParams.entries()),
|
|
484
|
+
kind,
|
|
485
|
+
status,
|
|
486
|
+
type,
|
|
487
|
+
body: bodyPreview(body),
|
|
488
|
+
note,
|
|
489
|
+
tookMs: Date.now() - startedAt,
|
|
490
|
+
ip:
|
|
491
|
+
req.socket?.remoteAddress ||
|
|
492
|
+
req.headers["x-forwarded-for"] ||
|
|
493
|
+
null,
|
|
494
|
+
at: new Date().toISOString(),
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
this.client.emit("apiRequest", payload);
|
|
498
|
+
|
|
499
|
+
if (this.options.logRequests === false) return;
|
|
500
|
+
|
|
501
|
+
const parts = [
|
|
502
|
+
`[ERLC API] ${payload.method} ${payload.path}`,
|
|
503
|
+
`status=${payload.status}`,
|
|
504
|
+
`kind=${payload.kind}`,
|
|
505
|
+
];
|
|
506
|
+
if (payload.type) parts.push(`type=${payload.type}`);
|
|
507
|
+
if (payload.note) parts.push(`note=${payload.note}`);
|
|
508
|
+
parts.push(`took=${payload.tookMs}ms`);
|
|
509
|
+
|
|
510
|
+
console.log(parts.join(" | "));
|
|
511
|
+
if (payload.body) {
|
|
512
|
+
console.log(JSON.stringify(payload.body, null, 2));
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
_isAuthorized(req) {
|
|
517
|
+
const token = String(this.options.token || "").trim();
|
|
518
|
+
if (!token) return true;
|
|
519
|
+
|
|
520
|
+
const auth = getHeader(req, "authorization");
|
|
521
|
+
if (auth === `Bearer ${token}`) return true;
|
|
522
|
+
|
|
523
|
+
const alt = getHeader(req, "x-api-token");
|
|
524
|
+
return alt === token;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
_getServerFlags(searchParams) {
|
|
528
|
+
const flags = {};
|
|
529
|
+
for (const [key, apiKey] of Object.entries(QUERY_FLAG_MAP)) {
|
|
530
|
+
if (parseBool(searchParams.get(key)) || parseBool(searchParams.get(apiKey))) {
|
|
531
|
+
flags[key] = true;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return flags;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
module.exports = LocalApiServer;
|
package/src/events/Poller.js
CHANGED
|
@@ -58,6 +58,7 @@ class Poller {
|
|
|
58
58
|
joins: new SeenSignatures(),
|
|
59
59
|
commands: new SeenSignatures(),
|
|
60
60
|
modCalls: new SeenSignatures(),
|
|
61
|
+
emergencyCalls: new SeenSignatures(),
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -98,6 +99,8 @@ class Poller {
|
|
|
98
99
|
this.seen.commands.seed(buildLogSignature("commands", item));
|
|
99
100
|
for (const item of snapshot.modCalls)
|
|
100
101
|
this.seen.modCalls.seed(buildLogSignature("modCalls", item));
|
|
102
|
+
for (const item of snapshot.emergencyCalls)
|
|
103
|
+
this.seen.emergencyCalls.seed(buildLogSignature("emergencyCalls", item));
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
_emitServerUpdate(current) {
|
|
@@ -241,6 +244,16 @@ class Poller {
|
|
|
241
244
|
});
|
|
242
245
|
}
|
|
243
246
|
}
|
|
247
|
+
|
|
248
|
+
for (const emergencyCall of current.emergencyCalls) {
|
|
249
|
+
const signature = buildLogSignature("emergencyCalls", emergencyCall);
|
|
250
|
+
if (!this.seen.emergencyCalls.checkAndAdd(signature)) continue;
|
|
251
|
+
this.client.emit("emergencyCall", {
|
|
252
|
+
emergencyCall,
|
|
253
|
+
signature,
|
|
254
|
+
snapshot: current,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
244
257
|
}
|
|
245
258
|
|
|
246
259
|
_handleSnapshot(current) {
|
|
@@ -280,6 +293,7 @@ class Poller {
|
|
|
280
293
|
killLogs: true,
|
|
281
294
|
commandLogs: true,
|
|
282
295
|
modCalls: true,
|
|
296
|
+
emergencyCalls: true,
|
|
283
297
|
vehicles: true,
|
|
284
298
|
},
|
|
285
299
|
{
|
package/src/events/diff.js
CHANGED
|
@@ -10,10 +10,11 @@ function getVehicleKey(vehicle) {
|
|
|
10
10
|
if (!vehicle) return "";
|
|
11
11
|
const name = vehicle.Name ?? "";
|
|
12
12
|
const owner = vehicle.Owner ?? "";
|
|
13
|
+
const plate = vehicle.Plate ?? "";
|
|
13
14
|
const texture = vehicle.Texture ?? "";
|
|
14
15
|
const colorHex = vehicle.ColorHex ?? "";
|
|
15
16
|
const colorName = vehicle.ColorName ?? "";
|
|
16
|
-
return `${name}|${owner}|${texture}|${colorHex}|${colorName}`;
|
|
17
|
+
return `${name}|${owner}|${plate}|${texture}|${colorHex}|${colorName}`;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
function diffByKey(previous, current, keyFn) {
|
|
@@ -74,6 +75,14 @@ function buildLogSignature(type, item) {
|
|
|
74
75
|
caller: item?.Caller ?? "",
|
|
75
76
|
moderator: item?.Moderator ?? "",
|
|
76
77
|
});
|
|
78
|
+
case "emergencyCalls":
|
|
79
|
+
return stableStringify({
|
|
80
|
+
startedAt: item?.StartedAt ?? null,
|
|
81
|
+
callNumber: item?.CallNumber ?? null,
|
|
82
|
+
team: item?.Team ?? "",
|
|
83
|
+
caller: item?.Caller ?? "",
|
|
84
|
+
description: item?.Description ?? "",
|
|
85
|
+
});
|
|
77
86
|
default:
|
|
78
87
|
return stableStringify(item);
|
|
79
88
|
}
|
package/src/util/constants.js
CHANGED
|
@@ -21,6 +21,7 @@ const QUERY_FLAG_MAP = {
|
|
|
21
21
|
killLogs: "KillLogs",
|
|
22
22
|
commandLogs: "CommandLogs",
|
|
23
23
|
modCalls: "ModCalls",
|
|
24
|
+
emergencyCalls: "EmergencyCalls",
|
|
24
25
|
vehicles: "Vehicles",
|
|
25
26
|
};
|
|
26
27
|
|
|
@@ -43,6 +44,16 @@ const DEFAULT_OPTIONS = {
|
|
|
43
44
|
intervalMs: 2500,
|
|
44
45
|
bypassCache: true,
|
|
45
46
|
},
|
|
47
|
+
api: {
|
|
48
|
+
enabled: false,
|
|
49
|
+
host: "127.0.0.1",
|
|
50
|
+
port: null,
|
|
51
|
+
path: "/erlc",
|
|
52
|
+
webhookPath: null,
|
|
53
|
+
publicUrl: "",
|
|
54
|
+
token: "",
|
|
55
|
+
logRequests: true,
|
|
56
|
+
},
|
|
46
57
|
};
|
|
47
58
|
|
|
48
59
|
module.exports = {
|
package/src/util/errors.js
CHANGED
package/src/util/normalize.js
CHANGED
|
@@ -15,6 +15,7 @@ function normalizeServerResponse(raw = {}) {
|
|
|
15
15
|
killLogs: Array.isArray(raw.KillLogs) ? raw.KillLogs : [],
|
|
16
16
|
commandLogs: Array.isArray(raw.CommandLogs) ? raw.CommandLogs : [],
|
|
17
17
|
modCalls: Array.isArray(raw.ModCalls) ? raw.ModCalls : [],
|
|
18
|
+
emergencyCalls: Array.isArray(raw.EmergencyCalls) ? raw.EmergencyCalls : [],
|
|
18
19
|
vehicles: Array.isArray(raw.Vehicles) ? raw.Vehicles : [],
|
|
19
20
|
raw,
|
|
20
21
|
};
|