@sonoransoftware/sonoran.js 1.0.34 → 1.0.35

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.
Files changed (38) hide show
  1. package/.github/workflows/auto-pr-on-branch-push.yml +89 -0
  2. package/.github/workflows/codex_instructions.md +24 -0
  3. package/.github/workflows/push-pr-nudge-codex.yml +50 -0
  4. package/dist/constants.d.ts +200 -1
  5. package/dist/constants.js +1 -0
  6. package/dist/index.d.ts +1 -1
  7. package/dist/instance/Instance.d.ts +6 -0
  8. package/dist/instance/Instance.js +27 -0
  9. package/dist/instance/instance.types.d.ts +3 -0
  10. package/dist/libs/rest/src/lib/REST.d.ts +2 -1
  11. package/dist/libs/rest/src/lib/REST.js +108 -0
  12. package/dist/libs/rest/src/lib/RequestManager.d.ts +2 -0
  13. package/dist/libs/rest/src/lib/RequestManager.js +201 -0
  14. package/dist/libs/rest/src/lib/errors/RateLimitError.js +19 -1
  15. package/dist/libs/rest/src/lib/utils/constants.d.ts +102 -22
  16. package/dist/libs/rest/src/lib/utils/constants.js +106 -2
  17. package/dist/managers/CADManager.d.ts +28 -0
  18. package/dist/managers/CADManager.js +90 -0
  19. package/dist/managers/CMSManager.d.ts +54 -0
  20. package/dist/managers/CMSManager.js +134 -0
  21. package/dist/managers/CMSServerManager.d.ts +3 -0
  22. package/dist/managers/CMSServerManager.js +36 -2
  23. package/dist/managers/RadioManager.d.ts +55 -0
  24. package/dist/managers/RadioManager.js +224 -0
  25. package/package.json +1 -1
  26. package/readme.md +170 -0
  27. package/src/constants.ts +232 -1
  28. package/src/index.ts +35 -1
  29. package/src/instance/Instance.ts +30 -1
  30. package/src/instance/instance.types.ts +4 -1
  31. package/src/libs/rest/src/lib/REST.ts +107 -1
  32. package/src/libs/rest/src/lib/RequestManager.ts +221 -10
  33. package/src/libs/rest/src/lib/errors/RateLimitError.ts +20 -2
  34. package/src/libs/rest/src/lib/utils/constants.ts +205 -24
  35. package/src/managers/CADManager.ts +86 -1
  36. package/src/managers/CMSManager.ts +121 -0
  37. package/src/managers/CMSServerManager.ts +39 -6
  38. package/src/managers/RadioManager.ts +187 -0
@@ -14,6 +14,7 @@ import type { RequestInit, Response } from 'node-fetch';
14
14
  import { Instance } from '../../../../instance/Instance';
15
15
  import { CADManager } from '../../../../managers/CADManager';
16
16
  import { CMSManager } from '../../../../managers/CMSManager';
17
+ import { RadioManager } from '../../../../managers/RadioManager';
17
18
 
18
19
  /**
19
20
  * Options to be passed when creating the REST instance
@@ -106,7 +107,7 @@ export interface REST {
106
107
  (<S extends string | symbol>(event?: Exclude<S, keyof RestEvents>) => this);
107
108
  }
108
109
 
109
- export type RestManagerTypes = CADManager | CMSManager;
110
+ export type RestManagerTypes = CADManager | CMSManager | RadioManager;
110
111
 
111
112
  export class REST extends EventEmitter {
112
113
  public readonly requestManager: RequestManager;
@@ -150,6 +151,11 @@ export class REST extends EventEmitter {
150
151
  apiKey = this.instance.cmsApiKey;
151
152
  break;
152
153
  }
154
+ case productEnums.RADIO: {
155
+ communityId = this.instance.radioCommunityId;
156
+ apiKey = this.instance.radioApiKey;
157
+ break;
158
+ }
153
159
  }
154
160
  if (!communityId || !apiKey) throw new Error(`Community ID or API Key could not be found for request. P${apiType.product}`);
155
161
  // if (apiType.minVersion > this.manager.version) throw new Error(`[${type}] Subscription version too low for this API type request. Current Version: ${convertSubNumToName(this.manager.version)} Needed Version: ${convertSubNumToName(apiType.minVersion)}`); // Verifies API Subscription Level Requirement which is deprecated currently
@@ -179,6 +185,9 @@ export class REST extends EventEmitter {
179
185
  serverId: args[0]
180
186
  }
181
187
  }
188
+ case 'SET_GAME_SERVERS': {
189
+ return args[0] ?? [];
190
+ }
182
191
  case 'RSVP': {
183
192
  return {
184
193
  eventId: args[0],
@@ -206,6 +215,62 @@ export class REST extends EventEmitter {
206
215
  uniqueId: args[4]
207
216
  };
208
217
  }
218
+ case 'GET_CURRENT_CLOCK_IN': {
219
+ return {
220
+ apiId: args[0],
221
+ username: args[1],
222
+ accId: args[2],
223
+ discord: args[3],
224
+ uniqueId: args[4]
225
+ };
226
+ }
227
+ case 'GET_ACCOUNTS': {
228
+ return args[0] ?? {};
229
+ }
230
+ case 'GET_PROFILE_FIELDS': {
231
+ return {};
232
+ }
233
+ case 'SET_CLOCK': {
234
+ if (args[0] && typeof args[0] === 'object' && !Array.isArray(args[0])) {
235
+ return args[0];
236
+ }
237
+ return {
238
+ serverId: args[0],
239
+ currentUtc: args[1],
240
+ currentGame: args[2],
241
+ secondsPerHour: args[3]
242
+ };
243
+ }
244
+ case 'JOIN_COMMUNITY':
245
+ case 'LEAVE_COMMUNITY': {
246
+ const payload = args[0] && typeof args[0] === 'object' && !Array.isArray(args[0]) && 'internalKey' in args[0]
247
+ ? args[0]
248
+ : null;
249
+ const internalKey = payload ? payload.internalKey : args[0];
250
+ const accountsInput = payload ? payload.accounts : args[1];
251
+ let accounts: Array<{ account: string }> = [];
252
+ if (Array.isArray(accountsInput)) {
253
+ accounts = accountsInput.map((entry) => {
254
+ if (typeof entry === 'string') {
255
+ return { account: entry };
256
+ }
257
+ if (entry && typeof entry === 'object' && 'account' in entry) {
258
+ return entry as { account: string };
259
+ }
260
+ return { account: String(entry) };
261
+ });
262
+ } else if (accountsInput) {
263
+ if (typeof accountsInput === 'string') {
264
+ accounts = [{ account: accountsInput }];
265
+ } else if (typeof accountsInput === 'object' && 'account' in accountsInput) {
266
+ accounts = [accountsInput as { account: string }];
267
+ }
268
+ }
269
+ return {
270
+ internalKey,
271
+ accounts
272
+ };
273
+ }
209
274
  case 'CLOCK_IN_OUT': {
210
275
  return {
211
276
  apiId: args[0],
@@ -237,6 +302,13 @@ export class REST extends EventEmitter {
237
302
  secret: args[0],
238
303
  };
239
304
  }
305
+ case 'GET_FORM_TEMPLATE_SUBMISSIONS': {
306
+ return {
307
+ templateId: args[0],
308
+ skip: args[1],
309
+ take: args[2],
310
+ };
311
+ }
240
312
  case 'CHANGE_FORM_STAGE': {
241
313
  return {
242
314
  accId: args[0],
@@ -322,6 +394,40 @@ export class REST extends EventEmitter {
322
394
  points: args[6],
323
395
  }
324
396
  }
397
+ case 'RADIO_GET_COMMUNITY_CHANNELS':
398
+ case 'RADIO_GET_CONNECTED_USERS':
399
+ case 'RADIO_GET_SERVER_SUBSCRIPTION_FROM_IP': {
400
+ return undefined;
401
+ }
402
+ case 'RADIO_GET_CONNECTED_USER': {
403
+ return {
404
+ roomId: args[0],
405
+ identity: args[1]
406
+ }
407
+ }
408
+ case 'RADIO_SET_USER_CHANNELS': {
409
+ return {
410
+ identity: args[0],
411
+ options: args[1] ?? {}
412
+ }
413
+ }
414
+ case 'RADIO_SET_USER_DISPLAY_NAME': {
415
+ return {
416
+ accId: args[0],
417
+ displayName: args[1]
418
+ }
419
+ }
420
+ case 'RADIO_SET_SERVER_IP': {
421
+ return {
422
+ pushUrl: args[0]
423
+ }
424
+ }
425
+ case 'RADIO_SET_IN_GAME_SPEAKER_LOCATIONS': {
426
+ return {
427
+ locations: args[0],
428
+ token: args[1]
429
+ }
430
+ }
325
431
  default: {
326
432
  return args;
327
433
  }
@@ -5,7 +5,8 @@ import { EventEmitter } from 'events';
5
5
 
6
6
  import type { Instance } from '../../../../instance/Instance';
7
7
  import { RESTOptions, RateLimitData, RestEvents } from './REST';
8
- import { DefaultCADRestOptions, DefaultCMSRestOptions, AllAPITypes/**, RESTTypedAPIDataStructs, PossibleRequestData*/ } from './utils/constants';
8
+ import { DefaultCADRestOptions, DefaultCMSRestOptions, DefaultRadioRestOptions, AllAPITypes/**, RESTTypedAPIDataStructs, PossibleRequestData*/ } from './utils/constants';
9
+ import type { AllAPITypeData } from './utils/constants';
9
10
  import { productEnums } from '../../../../constants';
10
11
  // import { APIError, HTTPError } from './errors';
11
12
  import { IHandler } from './handlers/IHandler';
@@ -29,6 +30,7 @@ export interface RequestData {
29
30
  key: string;
30
31
  type: string;
31
32
  data: any;
33
+ internalKey?: string;
32
34
  }
33
35
 
34
36
  export interface InternalRequestData extends RequestData {
@@ -87,6 +89,10 @@ export class RequestManager extends EventEmitter {
87
89
  this.options = { ...DefaultCMSRestOptions, ...options };
88
90
  break;
89
91
  }
92
+ case productEnums.RADIO: {
93
+ this.options = { ...DefaultRadioRestOptions, ...options };
94
+ break;
95
+ }
90
96
  default: {
91
97
  throw new Error('No Product provided for RequestManager initialization');
92
98
  }
@@ -134,10 +140,16 @@ export class RequestManager extends EventEmitter {
134
140
  case productEnums.CMS:
135
141
  apiURL = instance.cmsApiUrl;
136
142
  break;
143
+ case productEnums.RADIO:
144
+ apiURL = instance.radioApiUrl;
145
+ break;
137
146
  }
138
147
 
139
148
  const findType = AllAPITypes.find((_type) => _type.type === type);
140
149
  if (findType) {
150
+ if (product === productEnums.RADIO) {
151
+ return RequestManager.resolveRadioRequest(instance, apiURL, findType, data, apiData);
152
+ }
141
153
  apiData.fullUrl = `${apiURL}/${findType.path}`;
142
154
  apiData.method = findType.method;
143
155
  apiData.fetchOptions.method = findType.method;
@@ -150,6 +162,10 @@ export class RequestManager extends EventEmitter {
150
162
  apiData.data.data = clonedData;
151
163
  break;
152
164
  }
165
+ case 'SET_GAME_SERVERS': {
166
+ apiData.data.data = clonedData;
167
+ break;
168
+ }
153
169
  case 'SET_PENAL_CODES': {
154
170
  apiData.data.data = [clonedData[0]];
155
171
  break;
@@ -186,14 +202,37 @@ export class RequestManager extends EventEmitter {
186
202
  apiData.data.data = clonedData;
187
203
  break;
188
204
  }
189
- case 'SET_POSTALS': {
190
- apiData.data.data = [clonedData[0]];
191
- break;
192
- }
193
- case 'NEW_CHARACTER': {
194
- apiData.data.data = [clonedData[0]];
195
- break;
196
- }
205
+ case 'SET_POSTALS': {
206
+ apiData.data.data = [clonedData[0]];
207
+ break;
208
+ }
209
+ case 'SET_CLOCK': {
210
+ apiData.data.data = Array.isArray(clonedData) ? clonedData : [clonedData];
211
+ break;
212
+ }
213
+ case 'JOIN_COMMUNITY':
214
+ case 'LEAVE_COMMUNITY': {
215
+ const internalKey = clonedData?.internalKey;
216
+ if (internalKey !== undefined) {
217
+ apiData.data.internalKey = internalKey;
218
+ }
219
+ const accountsSource = clonedData?.accounts;
220
+ const accountsArray = Array.isArray(accountsSource) ? accountsSource : accountsSource ? [accountsSource] : [];
221
+ apiData.data.data = accountsArray.map((entry: any) => {
222
+ if (typeof entry === 'string') {
223
+ return { account: entry };
224
+ }
225
+ if (entry && typeof entry === 'object' && 'account' in entry) {
226
+ return entry;
227
+ }
228
+ return { account: String(entry) };
229
+ });
230
+ break;
231
+ }
232
+ case 'NEW_CHARACTER': {
233
+ apiData.data.data = [clonedData[0]];
234
+ break;
235
+ }
197
236
  case 'EDIT_CHARACTER': {
198
237
  apiData.data.data = [clonedData[0]];
199
238
  break;
@@ -255,7 +294,179 @@ export class RequestManager extends EventEmitter {
255
294
  return apiData;
256
295
  }
257
296
 
297
+ private static resolveRadioRequest(instance: Instance, apiURL: string | boolean, apiType: AllAPITypeData, request: RequestData, apiData: APIData): APIData {
298
+ if (!apiURL || typeof apiURL !== 'string') {
299
+ throw new Error('Radio API URL could not be resolved for request.');
300
+ }
301
+
302
+ const rawData = request.data;
303
+ const payload: any =
304
+ rawData == null ? {} : (typeof rawData === 'object' ? cloneObject(rawData) : rawData);
305
+ const headers: Record<string, string> = {
306
+ Accept: 'application/json'
307
+ };
308
+
309
+ const applyHeaders = (source: unknown) => {
310
+ if (!source) return;
311
+ if (Array.isArray(source)) {
312
+ for (const [key, value] of source) {
313
+ headers[key] = value;
314
+ }
315
+ return;
316
+ }
317
+ if (typeof source === 'object' && source !== null && 'forEach' in source && typeof (source as any).forEach === 'function') {
318
+ (source as any).forEach((value: string, key: string) => {
319
+ headers[key] = value;
320
+ });
321
+ return;
322
+ }
323
+ Object.assign(headers, source as Record<string, string>);
324
+ };
325
+
326
+ applyHeaders(instance.apiHeaders);
327
+
328
+ let method = apiType.method;
329
+ let path = apiType.path;
330
+ let body: unknown;
331
+
332
+ const ensureAuth = () => {
333
+ if (!request.id || !request.key) {
334
+ throw new Error('Community ID or API Key could not be found for request.');
335
+ }
336
+ return {
337
+ id: request.id,
338
+ key: request.key,
339
+ encodedId: encodeURIComponent(request.id),
340
+ encodedKey: encodeURIComponent(request.key)
341
+ };
342
+ };
343
+
344
+ const encodeSegment = (value: string | number) => encodeURIComponent(String(value));
345
+
346
+ switch (apiType.type) {
347
+ case 'RADIO_GET_COMMUNITY_CHANNELS': {
348
+ const auth = ensureAuth();
349
+ path = `${apiType.path}/${auth.encodedId}/${auth.encodedKey}`;
350
+ method = 'GET';
351
+ break;
352
+ }
353
+ case 'RADIO_GET_CONNECTED_USERS': {
354
+ const auth = ensureAuth();
355
+ path = `${apiType.path}/${auth.encodedId}/${auth.encodedKey}`;
356
+ method = 'GET';
357
+ break;
358
+ }
359
+ case 'RADIO_GET_CONNECTED_USER': {
360
+ const auth = ensureAuth();
361
+ const roomIdRaw = payload?.roomId ?? payload?.roomID;
362
+ if (roomIdRaw === undefined) {
363
+ throw new Error('roomId is required for RADIO_GET_CONNECTED_USER requests.');
364
+ }
365
+ const roomIdNumeric = typeof roomIdRaw === 'number' ? roomIdRaw : Number(roomIdRaw);
366
+ if (Number.isNaN(roomIdNumeric)) {
367
+ throw new Error('roomId must be a number for RADIO_GET_CONNECTED_USER requests.');
368
+ }
369
+ const identity = payload?.identity;
370
+ if (!identity) {
371
+ throw new Error('identity is required for RADIO_GET_CONNECTED_USER requests.');
372
+ }
373
+ path = `${apiType.path}/${auth.encodedId}/${auth.encodedKey}/${encodeSegment(roomIdNumeric)}/${encodeSegment(identity)}`;
374
+ method = 'GET';
375
+ break;
376
+ }
377
+ case 'RADIO_SET_USER_CHANNELS': {
378
+ const auth = ensureAuth();
379
+ const identity = payload?.identity;
380
+ if (!identity) {
381
+ throw new Error('identity is required for RADIO_SET_USER_CHANNELS requests.');
382
+ }
383
+ const options = payload?.options ?? {};
384
+ path = `${apiType.path}/${auth.encodedId}/${auth.encodedKey}/${encodeSegment(identity)}`;
385
+ method = 'POST';
386
+ const requestBody: Record<string, unknown> = {};
387
+ if (options?.transmit !== undefined) {
388
+ requestBody.transmit = options.transmit;
389
+ }
390
+ if (options?.scan !== undefined) {
391
+ requestBody.scan = options.scan;
392
+ }
393
+ body = requestBody;
394
+ break;
395
+ }
396
+ case 'RADIO_SET_USER_DISPLAY_NAME': {
397
+ const auth = ensureAuth();
398
+ const accId = payload?.accId;
399
+ const displayName = payload?.displayName;
400
+ if (!accId || !displayName) {
401
+ throw new Error('accId and displayName are required for RADIO_SET_USER_DISPLAY_NAME requests.');
402
+ }
403
+ method = 'POST';
404
+ body = {
405
+ id: auth.id,
406
+ key: auth.key,
407
+ accId,
408
+ displayName
409
+ };
410
+ path = apiType.path;
411
+ break;
412
+ }
413
+ case 'RADIO_GET_SERVER_SUBSCRIPTION_FROM_IP': {
414
+ method = 'GET';
415
+ path = apiType.path;
416
+ break;
417
+ }
418
+ case 'RADIO_SET_SERVER_IP': {
419
+ const auth = ensureAuth();
420
+ const pushUrl = payload?.pushUrl;
421
+ if (!pushUrl) {
422
+ throw new Error('pushUrl is required for RADIO_SET_SERVER_IP requests.');
423
+ }
424
+ method = 'POST';
425
+ body = {
426
+ id: auth.id,
427
+ key: auth.key,
428
+ pushUrl
429
+ };
430
+ path = apiType.path;
431
+ break;
432
+ }
433
+ case 'RADIO_SET_IN_GAME_SPEAKER_LOCATIONS': {
434
+ const auth = ensureAuth();
435
+ const locations = payload?.locations;
436
+ if (!Array.isArray(locations)) {
437
+ throw new Error('locations array is required for RADIO_SET_IN_GAME_SPEAKER_LOCATIONS requests.');
438
+ }
439
+ method = 'POST';
440
+ body = {
441
+ id: auth.id,
442
+ key: auth.key,
443
+ locations
444
+ };
445
+ const token = payload?.token ?? auth.key;
446
+ if (token) {
447
+ headers.Authorization = `Bearer ${token}`;
448
+ }
449
+ path = apiType.path;
450
+ break;
451
+ }
452
+ default: {
453
+ throw new Error(`Unsupported radio API type received: ${apiType.type}`);
454
+ }
455
+ }
456
+
457
+ apiData.typePath = path;
458
+ apiData.fullUrl = `${apiURL}/${path}`;
459
+ apiData.method = method;
460
+ apiData.fetchOptions.method = method;
461
+ if (body !== undefined) {
462
+ headers['Content-Type'] = 'application/json';
463
+ apiData.fetchOptions.body = JSON.stringify(body);
464
+ }
465
+ apiData.fetchOptions.headers = headers;
466
+ return apiData;
467
+ }
468
+
258
469
  debug(log: string) {
259
470
  return this.instance._debugLog(log);
260
471
  }
261
- }
472
+ }
@@ -16,6 +16,24 @@ export class RateLimitError extends Error implements RateLimitData {
16
16
  * The name of the error
17
17
  */
18
18
  public override get name(): string {
19
- return `Ratelimit Hit - [${this.product === productEnums.CAD ? 'Sonoran CAD' : this.product === productEnums.CMS ? 'Sonoran CMS' : 'Invalid Product' } '${this.type}']`;
19
+ let productName: string;
20
+ switch (this.product) {
21
+ case productEnums.CAD: {
22
+ productName = 'Sonoran CAD';
23
+ break;
24
+ }
25
+ case productEnums.CMS: {
26
+ productName = 'Sonoran CMS';
27
+ break;
28
+ }
29
+ case productEnums.RADIO: {
30
+ productName = 'Sonoran Radio';
31
+ break;
32
+ }
33
+ default: {
34
+ productName = 'Invalid Product';
35
+ }
36
+ }
37
+ return `Ratelimit Hit - [${productName} '${this.type}']`;
20
38
  }
21
- }
39
+ }