erlc-v2 1.1.0 → 1.1.1
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 +574 -581
- package/index.d.ts +429 -430
- package/package.json +7 -7
- package/src/Client.js +460 -465
- package/src/api/LocalApiServer.js +530 -533
package/README.md
CHANGED
|
@@ -1,609 +1,602 @@
|
|
|
1
|
-
# erlc-v2
|
|
2
|
-
|
|
3
|
-
JavaScript client for the ER:LC API v2.
|
|
4
|
-
|
|
5
|
-
Built for Node 18+.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
console.log("
|
|
59
|
-
console.log("
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
- `
|
|
174
|
-
- `
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
- `await client.
|
|
181
|
-
- `await client.
|
|
182
|
-
- `await client.
|
|
183
|
-
- `await client.
|
|
184
|
-
- `await client.logs.
|
|
185
|
-
- `await client.logs.
|
|
186
|
-
- `await client.logs.
|
|
187
|
-
- `await client.
|
|
188
|
-
- `await client.
|
|
189
|
-
- `await client.vehicles.
|
|
190
|
-
- `await client.vehicles.
|
|
191
|
-
- `await client.vehicles.
|
|
192
|
-
- `await client.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
- `
|
|
199
|
-
- `
|
|
200
|
-
- `
|
|
201
|
-
- `
|
|
202
|
-
- `
|
|
203
|
-
- `
|
|
204
|
-
- `
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
- `
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
- `:
|
|
273
|
-
- `:
|
|
274
|
-
- `:
|
|
275
|
-
- `:
|
|
276
|
-
- `:
|
|
277
|
-
- `:
|
|
278
|
-
- `:
|
|
279
|
-
-
|
|
280
|
-
- `:
|
|
281
|
-
- `:
|
|
282
|
-
- `:
|
|
283
|
-
- `:
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
By default it logs incoming route hits and verified webhook payloads to the console.
|
|
316
|
-
|
|
1
|
+
# erlc-v2
|
|
2
|
+
|
|
3
|
+
JavaScript client for the ER:LC API v2.
|
|
4
|
+
|
|
5
|
+
Built for Node 18+.
|
|
6
|
+
|
|
7
|
+
## New Features
|
|
8
|
+
|
|
9
|
+
- `client.commands.execute()` now uses `/v2/server/command`
|
|
10
|
+
- emergency calls are supported
|
|
11
|
+
- vehicle lookup helpers are built in
|
|
12
|
+
- you can start a small local API with `api: { port: 3001 }`
|
|
13
|
+
- event webhooks are supported and signature-checked before they fire events
|
|
14
|
+
|
|
15
|
+
## Stable Release
|
|
16
|
+
|
|
17
|
+
This wrapper is on a stable release track (`1.0.0+`).
|
|
18
|
+
|
|
19
|
+
## Responsibility and API Safety
|
|
20
|
+
|
|
21
|
+
Use your keys like a normal person. If you spam requests, ignore rate limits, or build dumb abuse tools and PRC or Cloudflare blocks you, that is on you.
|
|
22
|
+
|
|
23
|
+
This project is provided as-is. Keep an eye on your own integration and follow the PRC API rules.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install erlc-v2
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Start (CommonJS)
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
const { Client } = require("erlc-v2");
|
|
35
|
+
|
|
36
|
+
const client = new Client({
|
|
37
|
+
serverKey: "YOUR_SERVER_KEY",
|
|
38
|
+
polling: {
|
|
39
|
+
enabled: true,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
client.on("disconnect", ({ reason, error }) => {
|
|
44
|
+
console.error("Disconnected:", reason, error?.message);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
async function main() {
|
|
48
|
+
const snapshot = await client.server.fetch({
|
|
49
|
+
players: true,
|
|
50
|
+
staff: true,
|
|
51
|
+
queue: true,
|
|
52
|
+
vehicles: true,
|
|
53
|
+
emergencyCalls: true,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
console.log("Server:", snapshot.name);
|
|
57
|
+
console.log("Players:", `${snapshot.currentPlayers}/${snapshot.maxPlayers}`);
|
|
58
|
+
console.log("Vehicles:", snapshot.vehicles.length);
|
|
59
|
+
console.log("Emergency calls:", snapshot.emergencyCalls.length);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
main()
|
|
63
|
+
.catch(console.error)
|
|
64
|
+
.finally(() => client.destroy());
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Quick Start (ESM)
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
import { Client } from "erlc-v2";
|
|
71
|
+
|
|
72
|
+
const client = new Client({
|
|
73
|
+
serverKey: "YOUR_SERVER_KEY",
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Options
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
new Client({
|
|
81
|
+
serverKey: string, // required
|
|
82
|
+
globalKey?: string, // optional
|
|
83
|
+
logging?: boolean, // default: false
|
|
84
|
+
logger?: { info, warn, error, debug },
|
|
85
|
+
cache?: {
|
|
86
|
+
enabled?: boolean, // default: true
|
|
87
|
+
ttlMs?: number, // default: 1500
|
|
88
|
+
maxSize?: number, // default: 500
|
|
89
|
+
provider?: "memory" | "redis", // default: auto
|
|
90
|
+
redisUrl?: string,
|
|
91
|
+
redisPrefix?: string, // default: "erlc-v2:cache"
|
|
92
|
+
redisClient?: object,
|
|
93
|
+
},
|
|
94
|
+
rateLimit?: {
|
|
95
|
+
enabled?: boolean, // default: true
|
|
96
|
+
strictSerial?: boolean, // default: true
|
|
97
|
+
bucketLimit?: number, // default: 1
|
|
98
|
+
totalLimit?: number, // default: 1
|
|
99
|
+
unauthLimit?: number, // default: 3
|
|
100
|
+
},
|
|
101
|
+
polling?: {
|
|
102
|
+
enabled?: boolean, // default: true
|
|
103
|
+
intervalMs?: number, // default: 2500
|
|
104
|
+
bypassCache?: boolean, // default: true
|
|
105
|
+
},
|
|
106
|
+
api?: {
|
|
107
|
+
enabled?: boolean, // default: false unless port is set
|
|
108
|
+
host?: string, // default: "127.0.0.1"
|
|
109
|
+
port?: number, // required if you want the local API server
|
|
110
|
+
path?: string, // default: "/erlc"
|
|
111
|
+
webhookPath?: string, // default: `${path}/events`
|
|
112
|
+
publicUrl?: string, // optional, used to build webhookUrl in client.api.info()
|
|
113
|
+
token?: string, // optional bearer token for built-in routes
|
|
114
|
+
logRequests?: boolean, // default: true, logs route/webhook hits to the console
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Legacy aliases (`perBucketConcurrency`, `globalConcurrency`, `unauthorizedThreshold`) are still accepted.
|
|
120
|
+
|
|
121
|
+
## Redis Cache (Optional)
|
|
122
|
+
|
|
123
|
+
You can use Redis instead of in-memory cache by passing either `cache.redisUrl` or `cache.redisClient`.
|
|
124
|
+
|
|
125
|
+
If you use `redisUrl`, install the Redis client package:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npm i redis
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Example with connection URL:
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
const { Client } = require("erlc-v2");
|
|
135
|
+
|
|
136
|
+
const client = new Client({
|
|
137
|
+
serverKey: "YOUR_SERVER_KEY",
|
|
138
|
+
cache: {
|
|
139
|
+
provider: "redis",
|
|
140
|
+
redisUrl: "redis://localhost:6379",
|
|
141
|
+
redisPrefix: "myapp:erlc",
|
|
142
|
+
ttlMs: 2000,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Example with your own Redis client instance:
|
|
148
|
+
|
|
149
|
+
```js
|
|
150
|
+
const { createClient } = require("redis");
|
|
151
|
+
const { Client } = require("erlc-v2");
|
|
152
|
+
|
|
153
|
+
(async () => {
|
|
154
|
+
const redis = createClient({ url: process.env.REDIS_URL });
|
|
155
|
+
await redis.connect();
|
|
156
|
+
|
|
157
|
+
const client = new Client({
|
|
158
|
+
serverKey: "YOUR_SERVER_KEY",
|
|
159
|
+
cache: {
|
|
160
|
+
redisClient: redis,
|
|
161
|
+
redisPrefix: "myapp:erlc",
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
})();
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## API
|
|
168
|
+
|
|
169
|
+
Core:
|
|
170
|
+
|
|
171
|
+
- `await client.server.fetch(flags, requestOptions?)`
|
|
172
|
+
- `await client.commands.execute(command)`
|
|
173
|
+
- `client.destroy()`
|
|
174
|
+
- `client.cache.clear()`
|
|
175
|
+
|
|
176
|
+
Convenience methods:
|
|
177
|
+
|
|
178
|
+
- `await client.players.list()`
|
|
179
|
+
- `await client.map.render(options?)`
|
|
180
|
+
- `await client.map.renderUser(userId, options?)`
|
|
181
|
+
- `await client.staff.list()`
|
|
182
|
+
- `await client.logs.kills()`
|
|
183
|
+
- `await client.logs.joins()`
|
|
184
|
+
- `await client.logs.commands()`
|
|
185
|
+
- `await client.logs.modCalls()`
|
|
186
|
+
- `await client.logs.emergencyCalls()`
|
|
187
|
+
- `await client.vehicles.list()`
|
|
188
|
+
- `await client.vehicles.search(filters)`
|
|
189
|
+
- `await client.vehicles.findByPlate(plate)`
|
|
190
|
+
- `await client.vehicles.findByOwner(owner)`
|
|
191
|
+
- `await client.vehicles.findOne(filters)`
|
|
192
|
+
- `await client.queue.get()`
|
|
193
|
+
|
|
194
|
+
### Fetch Flags
|
|
195
|
+
|
|
196
|
+
- `players` -> `Players`
|
|
197
|
+
- `staff` -> `Staff`
|
|
198
|
+
- `joinLogs` -> `JoinLogs`
|
|
199
|
+
- `queue` -> `Queue`
|
|
200
|
+
- `killLogs` -> `KillLogs`
|
|
201
|
+
- `commandLogs` -> `CommandLogs`
|
|
202
|
+
- `modCalls` -> `ModCalls`
|
|
203
|
+
- `emergencyCalls` -> `EmergencyCalls`
|
|
204
|
+
- `vehicles` -> `Vehicles`
|
|
205
|
+
|
|
206
|
+
### Request Options
|
|
207
|
+
|
|
208
|
+
- `bypassCache?: boolean`
|
|
209
|
+
- `cacheTtlMs?: number`
|
|
210
|
+
- `dedupe?: boolean`
|
|
211
|
+
|
|
212
|
+
## Vehicle Search Helpers
|
|
213
|
+
|
|
214
|
+
Find one exact plate:
|
|
215
|
+
|
|
216
|
+
```js
|
|
217
|
+
const car = await client.vehicles.findByPlate("LINCOLN7");
|
|
218
|
+
|
|
219
|
+
if (car) {
|
|
220
|
+
console.log(car.Owner, car.Name, car.Plate);
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Search across plate, owner, name, color, and texture:
|
|
225
|
+
|
|
226
|
+
```js
|
|
227
|
+
const matches = await client.vehicles.search({
|
|
228
|
+
query: "lincoln",
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const ownerCars = await client.vehicles.findByOwner("lando");
|
|
232
|
+
|
|
233
|
+
const blackTahoes = await client.vehicles.search({
|
|
234
|
+
name: "tahoe",
|
|
235
|
+
color: "black",
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Exact matching is supported too:
|
|
240
|
+
|
|
241
|
+
```js
|
|
242
|
+
const exact = await client.vehicles.findOne({
|
|
243
|
+
plate: "A12BCD",
|
|
244
|
+
owner: "SomePlayer",
|
|
245
|
+
exact: true,
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Emergency Calls
|
|
250
|
+
|
|
251
|
+
```js
|
|
252
|
+
const calls = await client.logs.emergencyCalls();
|
|
253
|
+
|
|
254
|
+
for (const call of calls) {
|
|
255
|
+
console.log(call.CallNumber, call.Team, call.Description);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
client.on("emergencyCall", ({ emergencyCall }) => {
|
|
259
|
+
console.log("New emergency call:", emergencyCall.Description);
|
|
260
|
+
});
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Command Execution
|
|
264
|
+
|
|
265
|
+
`client.commands.execute(command)` sends a POST request to `/v2/server/command`.
|
|
266
|
+
Command execution is FIFO-queued client-side, so commands run one-at-a-time in order.
|
|
267
|
+
|
|
268
|
+
Blocked by client policy:
|
|
269
|
+
|
|
270
|
+
- `:view`
|
|
271
|
+
- `:to`
|
|
272
|
+
- `:tocar`
|
|
273
|
+
- `:toatv`
|
|
274
|
+
- `:logs`
|
|
275
|
+
- `:mods`
|
|
276
|
+
- `:admins`
|
|
277
|
+
- `helpers` / `:helpers`
|
|
278
|
+
- `:administrators`
|
|
279
|
+
- `:moderators`
|
|
280
|
+
- `:killlogs`
|
|
281
|
+
- `:kl`
|
|
282
|
+
- `:cmds`
|
|
283
|
+
- `:commands`
|
|
284
|
+
|
|
285
|
+
Example:
|
|
286
|
+
|
|
287
|
+
```js
|
|
288
|
+
const result = await client.commands.execute(":h Hey everyone!");
|
|
289
|
+
console.log(result.message);
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Built-in Local API Server
|
|
293
|
+
|
|
294
|
+
If you want the wrapper to expose a small HTTP server, it can do that too.
|
|
295
|
+
|
|
296
|
+
```js
|
|
297
|
+
const client = new Client({
|
|
298
|
+
serverKey: process.env.ERLC_SERVER_KEY,
|
|
299
|
+
api: {
|
|
300
|
+
port: 3001,
|
|
301
|
+
host: "127.0.0.1",
|
|
302
|
+
path: "/erlc",
|
|
303
|
+
publicUrl: "https://hooks.example.com",
|
|
304
|
+
token: process.env.ERLC_LOCAL_API_TOKEN,
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
client.api.info();
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
If `api.port` is set, the local API auto-starts with the client. You can also call `await client.api.start()` yourself.
|
|
312
|
+
|
|
313
|
+
By default it logs incoming route hits and verified webhook payloads to the console.
|
|
314
|
+
|
|
317
315
|
If you want to react to ER:LC webhooks in your own code, use:
|
|
318
316
|
|
|
319
317
|
- `client.onWebhook(...)`
|
|
320
|
-
- `client.onWebhookCommand(...)`
|
|
321
318
|
- `client.onWebhookEmergencyCall(...)`
|
|
322
319
|
|
|
323
320
|
Those only fire after the webhook signature checks out.
|
|
324
|
-
|
|
325
|
-
Built-in routes:
|
|
326
|
-
|
|
327
|
-
- `GET /erlc`
|
|
328
|
-
- `GET /erlc/health`
|
|
329
|
-
- `GET /erlc/server`
|
|
330
|
-
- `GET /erlc/players`
|
|
331
|
-
- `GET /erlc/vehicles`
|
|
332
|
-
- `GET /erlc/vehicles/:plate`
|
|
333
|
-
- `GET /erlc/emergency-calls`
|
|
334
|
-
- `POST /erlc/command`
|
|
335
|
-
- `POST /erlc/events`
|
|
336
|
-
|
|
337
|
-
`/erlc/vehicles` accepts query params like `search`, `plate`, `owner`, `name`, `color`, `texture`, `exact`, and `limit`.
|
|
338
|
-
|
|
339
|
-
If you set `api.token`, send either:
|
|
340
|
-
|
|
341
|
-
- `Authorization: Bearer YOUR_TOKEN`
|
|
342
|
-
- `X-API-Token: YOUR_TOKEN`
|
|
343
|
-
|
|
344
|
-
## Event Webhook Support
|
|
345
|
-
|
|
346
|
-
The built-in API can take ER:LC event webhooks and verify the signatures for you.
|
|
347
|
-
|
|
348
|
-
```js
|
|
349
|
-
const client = new Client({
|
|
350
|
-
serverKey: process.env.ERLC_SERVER_KEY,
|
|
351
|
-
api: {
|
|
352
|
-
port: 3001,
|
|
353
|
-
publicUrl: "https://hooks.example.com",
|
|
354
|
-
},
|
|
355
|
-
});
|
|
356
|
-
|
|
321
|
+
|
|
322
|
+
Built-in routes:
|
|
323
|
+
|
|
324
|
+
- `GET /erlc`
|
|
325
|
+
- `GET /erlc/health`
|
|
326
|
+
- `GET /erlc/server`
|
|
327
|
+
- `GET /erlc/players`
|
|
328
|
+
- `GET /erlc/vehicles`
|
|
329
|
+
- `GET /erlc/vehicles/:plate`
|
|
330
|
+
- `GET /erlc/emergency-calls`
|
|
331
|
+
- `POST /erlc/command`
|
|
332
|
+
- `POST /erlc/events`
|
|
333
|
+
|
|
334
|
+
`/erlc/vehicles` accepts query params like `search`, `plate`, `owner`, `name`, `color`, `texture`, `exact`, and `limit`.
|
|
335
|
+
|
|
336
|
+
If you set `api.token`, send either:
|
|
337
|
+
|
|
338
|
+
- `Authorization: Bearer YOUR_TOKEN`
|
|
339
|
+
- `X-API-Token: YOUR_TOKEN`
|
|
340
|
+
|
|
341
|
+
## Event Webhook Support
|
|
342
|
+
|
|
343
|
+
The built-in API can take ER:LC event webhooks and verify the signatures for you.
|
|
344
|
+
|
|
345
|
+
```js
|
|
346
|
+
const client = new Client({
|
|
347
|
+
serverKey: process.env.ERLC_SERVER_KEY,
|
|
348
|
+
api: {
|
|
349
|
+
port: 3001,
|
|
350
|
+
publicUrl: "https://hooks.example.com",
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
|
|
357
354
|
client.onWebhook((payload) => {
|
|
358
355
|
console.log("Webhook type:", payload.type);
|
|
359
356
|
console.log("Event name:", payload.event);
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
client.onWebhookCommand((payload) => {
|
|
363
357
|
console.log("Command:", payload.command);
|
|
364
358
|
console.log("Args:", payload.args);
|
|
365
359
|
console.log("Origin:", payload.origin);
|
|
366
|
-
// react to in-game ; commands here
|
|
367
360
|
});
|
|
368
361
|
|
|
369
362
|
client.onWebhookEmergencyCall((payload) => {
|
|
370
363
|
console.log("Event:", payload.event);
|
|
371
|
-
console.log("Origin:", payload.origin);
|
|
372
|
-
console.log("Data:", payload.data);
|
|
373
|
-
// react to emergency calls here
|
|
374
|
-
});
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
Useful flattened webhook fields:
|
|
378
|
-
|
|
379
|
-
- `payload.type`
|
|
380
|
-
- `payload.event`
|
|
381
|
-
- `payload.origin`
|
|
382
|
-
- `payload.server`
|
|
383
|
-
- `payload.eventTimestamp`
|
|
384
|
-
- `payload.data`
|
|
385
|
-
- `payload.command`
|
|
386
|
-
- `payload.args`
|
|
364
|
+
console.log("Origin:", payload.origin);
|
|
365
|
+
console.log("Data:", payload.data);
|
|
366
|
+
// react to emergency calls here
|
|
367
|
+
});
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Useful flattened webhook fields:
|
|
371
|
+
|
|
372
|
+
- `payload.type`
|
|
373
|
+
- `payload.event`
|
|
374
|
+
- `payload.origin`
|
|
375
|
+
- `payload.server`
|
|
376
|
+
- `payload.eventTimestamp`
|
|
377
|
+
- `payload.data`
|
|
378
|
+
- `payload.command`
|
|
379
|
+
- `payload.args`
|
|
387
380
|
- `payload.argument`
|
|
388
381
|
- `payload.entry`
|
|
389
382
|
- `payload.events`
|
|
390
383
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
```
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
-
|
|
432
|
-
-
|
|
433
|
-
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
- `
|
|
492
|
-
- `
|
|
493
|
-
- `
|
|
494
|
-
- `
|
|
495
|
-
- `
|
|
496
|
-
- `
|
|
497
|
-
- `
|
|
498
|
-
- `
|
|
499
|
-
- `
|
|
500
|
-
- `
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
- `
|
|
509
|
-
- `
|
|
510
|
-
- `
|
|
511
|
-
- `
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
- `
|
|
518
|
-
- `
|
|
519
|
-
- `
|
|
520
|
-
- `
|
|
521
|
-
- `
|
|
522
|
-
- `
|
|
523
|
-
- `
|
|
524
|
-
- `
|
|
525
|
-
- `
|
|
526
|
-
- `
|
|
384
|
+
This webhook is for custom `;` commands and emergency calls. It is not for normal `:` commands from the command endpoint.
|
|
385
|
+
|
|
386
|
+
For custom `;` commands, `payload.command`, `payload.args`, and `payload.origin` are usually the fields you want.
|
|
387
|
+
|
|
388
|
+
If your public URL is `https://hooks.example.com` and your API path is the default, set this in your ER:LC server settings:
|
|
389
|
+
|
|
390
|
+
```txt
|
|
391
|
+
https://hooks.example.com/erlc/events
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
If you want the longer request shape with type information, use:
|
|
395
|
+
|
|
396
|
+
```txt
|
|
397
|
+
https://hooks.example.com/erlc/events?long=true
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
## Domain, Hosting, and Reverse Proxy Notes
|
|
401
|
+
|
|
402
|
+
The event webhook has to hit a public HTTPS URL. A local port by itself is not enough.
|
|
403
|
+
|
|
404
|
+
Important:
|
|
405
|
+
|
|
406
|
+
- Most Discord bot hosts are bad for this because they do not let you expose your own API cleanly.
|
|
407
|
+
- If your host does not allow inbound HTTP traffic, PRC will never reach your webhook.
|
|
408
|
+
- You need something public in front of your wrapper.
|
|
409
|
+
|
|
410
|
+
Common setups:
|
|
411
|
+
|
|
412
|
+
- Buy a domain and point it at a VPS.
|
|
413
|
+
- Run the wrapper on a VPS and put NGINX or Caddy in front of it.
|
|
414
|
+
- Run it somewhere private and use Cloudflare Tunnel.
|
|
415
|
+
|
|
416
|
+
Common places people use for domains:
|
|
417
|
+
|
|
418
|
+
- Cloudflare Registrar: `https://www.cloudflare.com/products/registrar/`
|
|
419
|
+
- Namecheap: `https://www.namecheap.com/`
|
|
420
|
+
- Porkbun: `https://porkbun.com/`
|
|
421
|
+
|
|
422
|
+
Common places people use for public hosting or a VPS:
|
|
423
|
+
|
|
424
|
+
- DigitalOcean: `https://www.digitalocean.com/`
|
|
425
|
+
- Hetzner: `https://www.hetzner.com/`
|
|
426
|
+
- Railway: `https://railway.com/`
|
|
427
|
+
- Render: `https://render.com/`
|
|
428
|
+
- Fly.io: `https://fly.io/`
|
|
429
|
+
|
|
430
|
+
Common reverse proxy or edge options:
|
|
431
|
+
|
|
432
|
+
- NGINX: `https://nginx.org/`
|
|
433
|
+
- Caddy: `https://caddyserver.com/`
|
|
434
|
+
- Cloudflare Tunnel: `https://www.cloudflare.com/products/tunnel/`
|
|
435
|
+
|
|
436
|
+
Those are just examples. Use whatever actually gives you inbound HTTPS and a process you control.
|
|
437
|
+
|
|
438
|
+
## Map Rendering
|
|
439
|
+
|
|
440
|
+
Render an ER:LC map (`3121x3121`) with player markers that use Roblox avatars.
|
|
441
|
+
|
|
442
|
+
```js
|
|
443
|
+
const result = await client.map.render();
|
|
444
|
+
|
|
445
|
+
console.log(result.buffer);
|
|
446
|
+
console.log(result.players.length);
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
`client.map.render()` renders the full map with all players currently in the server.
|
|
450
|
+
|
|
451
|
+
Render an official season/type map preset:
|
|
452
|
+
|
|
453
|
+
```js
|
|
454
|
+
const fallBlank = await client.map.render({
|
|
455
|
+
season: "fall",
|
|
456
|
+
type: "blank",
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
const winterPostals = await client.map.render({
|
|
460
|
+
season: "winter",
|
|
461
|
+
type: "postals",
|
|
462
|
+
});
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
Use your own map image URL:
|
|
466
|
+
|
|
467
|
+
```js
|
|
468
|
+
const customMap = await client.map.render({
|
|
469
|
+
mapUrl: "https://example.com/my-map.png",
|
|
470
|
+
});
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Render only one player by Roblox user ID:
|
|
474
|
+
|
|
475
|
+
```js
|
|
476
|
+
const single = await client.map.renderUser(123456789, {
|
|
477
|
+
season: "winter",
|
|
478
|
+
type: "postals",
|
|
479
|
+
});
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
Options:
|
|
483
|
+
|
|
484
|
+
- `userId?: number | string`
|
|
485
|
+
- `userIds?: Array<number | string>`
|
|
486
|
+
- `players?: any[]`
|
|
487
|
+
- `mapUrl?: string`
|
|
488
|
+
- `season?: string`
|
|
489
|
+
- `type?: string`
|
|
490
|
+
- `mapSeason?: string`
|
|
491
|
+
- `mapType?: string`
|
|
492
|
+
- `coordinateBounds?: { minX, maxX, minY, maxY, invertY? }`
|
|
493
|
+
- `clampToMap?: boolean`
|
|
494
|
+
- `robloxHeadshotSize?: string`
|
|
495
|
+
- `marker?: { outerRadius, innerRadius, tipLength, tipWidth, fillColor, shadow }`
|
|
496
|
+
|
|
497
|
+
Map size is fixed to `3121x3121`.
|
|
498
|
+
|
|
499
|
+
Result shape:
|
|
500
|
+
|
|
501
|
+
- `buffer` (`image/png`)
|
|
502
|
+
- `map` (`{ url, season, type, width, height }`)
|
|
503
|
+
- `players`
|
|
504
|
+
- `skipped`
|
|
505
|
+
- `requestedUserIds`
|
|
506
|
+
- `unmatchedUserIds`
|
|
507
|
+
|
|
508
|
+
## Events
|
|
509
|
+
|
|
510
|
+
- `ready`
|
|
511
|
+
- `playerJoin`
|
|
512
|
+
- `playerLeave`
|
|
513
|
+
- `kill`
|
|
514
|
+
- `vehicleSpawn`
|
|
515
|
+
- `vehicleDespawn`
|
|
516
|
+
- `queueUpdate`
|
|
517
|
+
- `staffUpdate`
|
|
518
|
+
- `modCall`
|
|
519
|
+
- `emergencyCall`
|
|
520
|
+
- `commandLog`
|
|
521
|
+
- `logCommand`
|
|
527
522
|
- `serverUpdate`
|
|
528
523
|
- `webhook`
|
|
529
|
-
- `webhookCommand`
|
|
530
524
|
- `webhookEmergencyCall`
|
|
531
|
-
- `error`
|
|
532
|
-
- `disconnect`
|
|
533
|
-
|
|
534
|
-
Alias event names are also supported with `client.on(...)`:
|
|
535
|
-
|
|
536
|
-
- `onReady`
|
|
537
|
-
- `onJoin`
|
|
538
|
-
- `onLeave`
|
|
539
|
-
- `onKill`
|
|
540
|
-
- `onVehicleSpawn`
|
|
541
|
-
- `onVehicleDespawn`
|
|
542
|
-
- `onQueueUpdate`
|
|
543
|
-
- `onStaffUpdate`
|
|
544
|
-
- `onModCall`
|
|
545
|
-
- `onEmergencyCall`
|
|
546
|
-
- `onCommandLog`
|
|
547
|
-
- `onLogCommand`
|
|
525
|
+
- `error`
|
|
526
|
+
- `disconnect`
|
|
527
|
+
|
|
528
|
+
Alias event names are also supported with `client.on(...)`:
|
|
529
|
+
|
|
530
|
+
- `onReady`
|
|
531
|
+
- `onJoin`
|
|
532
|
+
- `onLeave`
|
|
533
|
+
- `onKill`
|
|
534
|
+
- `onVehicleSpawn`
|
|
535
|
+
- `onVehicleDespawn`
|
|
536
|
+
- `onQueueUpdate`
|
|
537
|
+
- `onStaffUpdate`
|
|
538
|
+
- `onModCall`
|
|
539
|
+
- `onEmergencyCall`
|
|
540
|
+
- `onCommandLog`
|
|
541
|
+
- `onLogCommand`
|
|
548
542
|
- `onServerUpdate`
|
|
549
543
|
- `onApiRequest`
|
|
550
544
|
- `onWebhook`
|
|
551
|
-
- `onWebhookCommand`
|
|
552
545
|
- `onWebhookEmergencyCall`
|
|
553
|
-
- `onError`
|
|
554
|
-
- `onDisconnect`
|
|
555
|
-
|
|
556
|
-
Shortcut methods are available too:
|
|
557
|
-
|
|
558
|
-
```js
|
|
559
|
-
client.onJoin((payload) => console.log("join", payload));
|
|
560
|
-
client.onLeave((payload) => console.log("leave", payload));
|
|
561
|
-
client.onVehicleSpawn((payload) => console.log("spawn", payload));
|
|
562
|
-
client.onLogCommand(({ command, parsed }) => {
|
|
563
|
-
console.log("raw command:", command.Command);
|
|
564
|
-
console.log("keyword:", parsed.keyword);
|
|
565
|
-
console.log("args:", parsed.args);
|
|
566
|
-
});
|
|
567
|
-
```
|
|
568
|
-
|
|
569
|
-
`logCommand` / `onLogCommand` fires when a command starts with `:log`.
|
|
570
|
-
|
|
571
|
-
Events are deduped per poll cycle so the same log entry is not emitted repeatedly.
|
|
572
|
-
|
|
573
|
-
## Rate Limits
|
|
574
|
-
|
|
575
|
-
Requests are automatically bucketed using API response headers:
|
|
576
|
-
|
|
577
|
-
- `X-RateLimit-Bucket`
|
|
578
|
-
- `X-RateLimit-Limit`
|
|
579
|
-
- `X-RateLimit-Remaining`
|
|
580
|
-
- `X-RateLimit-Reset`
|
|
581
|
-
|
|
582
|
-
On `429`, the client blocks the affected bucket until retry time or reset.
|
|
583
|
-
|
|
584
|
-
By default, requests are serialized (`strictSerial: true`) so this client does not spray parallel requests at the API.
|
|
585
|
-
|
|
586
|
-
## Errors
|
|
587
|
-
|
|
588
|
-
The client normalizes errors into classes:
|
|
589
|
-
|
|
590
|
-
- `ERLCError`
|
|
591
|
-
- `ERLCHttpError`
|
|
592
|
-
- `ERLCAPIError`
|
|
593
|
-
- `RateLimitError`
|
|
594
|
-
- `KeyExpiredError` (`2002`)
|
|
595
|
-
- `KeyBannedError` (`2004`)
|
|
596
|
-
- `InvalidGlobalKeyError` (`2003`)
|
|
597
|
-
- `ServerOfflineError` (`3002`)
|
|
598
|
-
- `RestrictedError` (`9998`)
|
|
599
|
-
- `ModuleOutOfDateError` (`9999`)
|
|
600
|
-
|
|
601
|
-
Terminal key errors (`2002`, `2004`) trigger disconnect and stop polling.
|
|
602
|
-
|
|
603
|
-
Repeated `403` responses can also trigger disconnect (`reason: "unauthorized"`).
|
|
604
|
-
|
|
605
|
-
## Notes
|
|
606
|
-
|
|
607
|
-
- API base URL: `https://api.policeroleplay.community`
|
|
608
|
-
- `server-key` is required for requests
|
|
546
|
+
- `onError`
|
|
547
|
+
- `onDisconnect`
|
|
548
|
+
|
|
549
|
+
Shortcut methods are available too:
|
|
550
|
+
|
|
551
|
+
```js
|
|
552
|
+
client.onJoin((payload) => console.log("join", payload));
|
|
553
|
+
client.onLeave((payload) => console.log("leave", payload));
|
|
554
|
+
client.onVehicleSpawn((payload) => console.log("spawn", payload));
|
|
555
|
+
client.onLogCommand(({ command, parsed }) => {
|
|
556
|
+
console.log("raw command:", command.Command);
|
|
557
|
+
console.log("keyword:", parsed.keyword);
|
|
558
|
+
console.log("args:", parsed.args);
|
|
559
|
+
});
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
`logCommand` / `onLogCommand` fires when a command starts with `:log`.
|
|
563
|
+
|
|
564
|
+
Events are deduped per poll cycle so the same log entry is not emitted repeatedly.
|
|
565
|
+
|
|
566
|
+
## Rate Limits
|
|
567
|
+
|
|
568
|
+
Requests are automatically bucketed using API response headers:
|
|
569
|
+
|
|
570
|
+
- `X-RateLimit-Bucket`
|
|
571
|
+
- `X-RateLimit-Limit`
|
|
572
|
+
- `X-RateLimit-Remaining`
|
|
573
|
+
- `X-RateLimit-Reset`
|
|
574
|
+
|
|
575
|
+
On `429`, the client blocks the affected bucket until retry time or reset.
|
|
576
|
+
|
|
577
|
+
By default, requests are serialized (`strictSerial: true`) so this client does not spray parallel requests at the API.
|
|
578
|
+
|
|
579
|
+
## Errors
|
|
580
|
+
|
|
581
|
+
The client normalizes errors into classes:
|
|
582
|
+
|
|
583
|
+
- `ERLCError`
|
|
584
|
+
- `ERLCHttpError`
|
|
585
|
+
- `ERLCAPIError`
|
|
586
|
+
- `RateLimitError`
|
|
587
|
+
- `KeyExpiredError` (`2002`)
|
|
588
|
+
- `KeyBannedError` (`2004`)
|
|
589
|
+
- `InvalidGlobalKeyError` (`2003`)
|
|
590
|
+
- `ServerOfflineError` (`3002`)
|
|
591
|
+
- `RestrictedError` (`9998`)
|
|
592
|
+
- `ModuleOutOfDateError` (`9999`)
|
|
593
|
+
|
|
594
|
+
Terminal key errors (`2002`, `2004`) trigger disconnect and stop polling.
|
|
595
|
+
|
|
596
|
+
Repeated `403` responses can also trigger disconnect (`reason: "unauthorized"`).
|
|
597
|
+
|
|
598
|
+
## Notes
|
|
599
|
+
|
|
600
|
+
- API base URL: `https://api.policeroleplay.community`
|
|
601
|
+
- `server-key` is required for requests
|
|
609
602
|
- `Authorization` is optional (`globalKey`)
|