crisp-api 9.13.0 → 10.0.2

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 (116) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +2 -2
  3. package/dist/crisp.d.ts +167 -0
  4. package/dist/crisp.js +764 -0
  5. package/dist/resources/BaseResource.d.ts +15 -0
  6. package/dist/resources/BaseResource.js +20 -0
  7. package/dist/resources/BucketURL.d.ts +28 -0
  8. package/dist/resources/BucketURL.js +29 -0
  9. package/dist/resources/MediaAnimation.d.ts +14 -0
  10. package/dist/resources/MediaAnimation.js +32 -0
  11. package/dist/resources/PluginConnect.d.ts +50 -0
  12. package/dist/resources/PluginConnect.js +73 -0
  13. package/dist/resources/PluginSubscription.d.ts +103 -0
  14. package/dist/resources/PluginSubscription.js +122 -0
  15. package/dist/resources/WebsiteAnalytics.d.ts +14 -0
  16. package/dist/resources/WebsiteAnalytics.js +29 -0
  17. package/dist/resources/WebsiteAvailability.d.ts +31 -0
  18. package/dist/resources/WebsiteAvailability.js +36 -0
  19. package/dist/resources/WebsiteBase.d.ts +60 -0
  20. package/dist/resources/WebsiteBase.js +71 -0
  21. package/dist/resources/WebsiteBatch.d.ts +52 -0
  22. package/dist/resources/WebsiteBatch.js +70 -0
  23. package/dist/resources/WebsiteCampaign.d.ts +199 -0
  24. package/dist/resources/WebsiteCampaign.js +194 -0
  25. package/dist/resources/WebsiteConversation.d.ts +701 -0
  26. package/dist/resources/WebsiteConversation.js +595 -0
  27. package/dist/resources/WebsiteHelpdesk.d.ts +347 -0
  28. package/dist/resources/WebsiteHelpdesk.js +587 -0
  29. package/dist/resources/WebsiteOperator.d.ts +79 -0
  30. package/dist/resources/WebsiteOperator.js +93 -0
  31. package/dist/resources/WebsitePeople.d.ts +248 -0
  32. package/dist/resources/WebsitePeople.js +276 -0
  33. package/dist/resources/WebsiteSettings.d.ts +159 -0
  34. package/dist/resources/WebsiteSettings.js +36 -0
  35. package/dist/resources/WebsiteVerify.d.ts +38 -0
  36. package/dist/resources/WebsiteVerify.js +50 -0
  37. package/dist/resources/WebsiteVisitors.d.ts +113 -0
  38. package/dist/resources/WebsiteVisitors.js +88 -0
  39. package/dist/resources/index.d.ts +17 -0
  40. package/dist/resources/index.js +40 -0
  41. package/dist/services/bucket.d.ts +13 -0
  42. package/dist/services/bucket.js +28 -0
  43. package/dist/services/media.d.ts +13 -0
  44. package/dist/services/media.js +28 -0
  45. package/dist/services/plugin.d.ts +14 -0
  46. package/dist/services/plugin.js +30 -0
  47. package/dist/services/website.d.ts +24 -0
  48. package/dist/services/website.js +50 -0
  49. package/eslint.config.mjs +208 -0
  50. package/lib/crisp.ts +957 -0
  51. package/lib/resources/BaseResource.ts +29 -0
  52. package/lib/resources/BucketURL.ts +49 -0
  53. package/lib/resources/MediaAnimation.ts +34 -0
  54. package/lib/resources/PluginConnect.ts +128 -0
  55. package/lib/resources/PluginSubscription.ts +208 -0
  56. package/lib/resources/WebsiteAnalytics.ts +31 -0
  57. package/lib/resources/WebsiteAvailability.ts +54 -0
  58. package/lib/resources/WebsiteBase.ts +108 -0
  59. package/lib/resources/WebsiteBatch.ts +96 -0
  60. package/lib/resources/WebsiteCampaign.ts +399 -0
  61. package/lib/resources/WebsiteConversation.ts +1416 -0
  62. package/lib/resources/WebsiteHelpdesk.ts +982 -0
  63. package/lib/resources/WebsiteOperator.ts +161 -0
  64. package/lib/resources/WebsitePeople.ts +527 -0
  65. package/lib/resources/WebsiteSettings.ts +192 -0
  66. package/lib/resources/WebsiteVerify.ts +76 -0
  67. package/lib/resources/WebsiteVisitors.ts +196 -0
  68. package/lib/resources/index.ts +25 -0
  69. package/lib/services/bucket.ts +28 -0
  70. package/lib/services/media.ts +28 -0
  71. package/lib/services/plugin.ts +32 -0
  72. package/lib/services/website.ts +62 -0
  73. package/package.json +16 -11
  74. package/tsconfig.json +12 -5
  75. package/lib/crisp.js +0 -1171
  76. package/lib/resources/BucketURL.js +0 -34
  77. package/lib/resources/MediaAnimation.js +0 -41
  78. package/lib/resources/PluginConnect.js +0 -119
  79. package/lib/resources/PluginSubscription.js +0 -234
  80. package/lib/resources/WebsiteAnalytics.js +0 -37
  81. package/lib/resources/WebsiteAvailability.js +0 -48
  82. package/lib/resources/WebsiteBase.js +0 -100
  83. package/lib/resources/WebsiteBatch.js +0 -92
  84. package/lib/resources/WebsiteCampaign.js +0 -396
  85. package/lib/resources/WebsiteConversation.js +0 -1261
  86. package/lib/resources/WebsiteHelpdesk.js +0 -1198
  87. package/lib/resources/WebsiteOperator.js +0 -167
  88. package/lib/resources/WebsitePeople.js +0 -516
  89. package/lib/resources/WebsiteSettings.js +0 -50
  90. package/lib/resources/WebsiteVerify.js +0 -79
  91. package/lib/resources/WebsiteVisitors.js +0 -148
  92. package/lib/services/Bucket.js +0 -28
  93. package/lib/services/Media.js +0 -28
  94. package/lib/services/Plugin.js +0 -29
  95. package/lib/services/Website.js +0 -39
  96. package/types/crisp.d.ts +0 -151
  97. package/types/resources/BucketURL.d.ts +0 -15
  98. package/types/resources/MediaAnimation.d.ts +0 -15
  99. package/types/resources/PluginConnect.d.ts +0 -15
  100. package/types/resources/PluginSubscription.d.ts +0 -15
  101. package/types/resources/WebsiteAnalytics.d.ts +0 -15
  102. package/types/resources/WebsiteAvailability.d.ts +0 -15
  103. package/types/resources/WebsiteBase.d.ts +0 -15
  104. package/types/resources/WebsiteBatch.d.ts +0 -15
  105. package/types/resources/WebsiteCampaign.d.ts +0 -15
  106. package/types/resources/WebsiteConversation.d.ts +0 -15
  107. package/types/resources/WebsiteHelpdesk.d.ts +0 -15
  108. package/types/resources/WebsiteOperator.d.ts +0 -15
  109. package/types/resources/WebsitePeople.d.ts +0 -15
  110. package/types/resources/WebsiteSettings.d.ts +0 -15
  111. package/types/resources/WebsiteVerify.d.ts +0 -15
  112. package/types/resources/WebsiteVisitors.d.ts +0 -15
  113. package/types/services/Bucket.d.ts +0 -14
  114. package/types/services/Media.d.ts +0 -14
  115. package/types/services/Plugin.d.ts +0 -14
  116. package/types/services/Website.d.ts +0 -14
package/lib/crisp.ts ADDED
@@ -0,0 +1,957 @@
1
+ /*
2
+ * node-crisp-api
3
+ *
4
+ * Copyright 2022, Crisp IM SAS
5
+ * Author: Baptiste Jamin <baptiste@crisp.chat>
6
+ */
7
+
8
+ /**************************************************************************
9
+ * IMPORTS
10
+ ***************************************************************************/
11
+
12
+ // NPM
13
+ import got from "got";
14
+ import { io as socketio } from "socket.io-client";
15
+ import { Socket } from "socket.io-client";
16
+
17
+ import { URL } from "url";
18
+ import Crypto from "crypto";
19
+ import mitt, { Emitter } from "mitt";
20
+
21
+ // PROJECT: SERVICES
22
+ import Bucket from "@/services/bucket";
23
+ import Media from "@/services/media";
24
+ import Plugin from "@/services/plugin";
25
+ import Website, { WebsiteServiceInterface } from "@/services/website";
26
+
27
+ // PROJECT: MAIN
28
+ // Export all types from resources
29
+ export * from "@/resources";
30
+
31
+ export type RTM_MODES = "websockets" | "webhooks";
32
+
33
+ const AVAILABLE_RTM_MODES = [
34
+ "websockets",
35
+ "webhooks"
36
+ ];
37
+
38
+ const VERSION = "__PKG_VERSION_PLACEHOLDER__";
39
+
40
+ // Base configuration
41
+ const DEFAULT_REQUEST_TIMEOUT = 10000;
42
+ const DEFAULT_SOCKET_TIMEOUT = 10000;
43
+ const DEFAULT_SOCKET_RECONNECT_DELAY = 5000;
44
+ const DEFAULT_SOCKET_RECONNECT_DELAY_MAX = 10000;
45
+ const DEFAULT_SOCKET_RECONNECT_FACTOR = 0.75;
46
+ const DEFAULT_BROKER_SCHEDULE = 500;
47
+ const DEFAULT_EVENT_REBIND_INTERVAL_MIN = 2500;
48
+ const DEFAULT_USERAGENT_PREFIX = "node-crisp-api/";
49
+
50
+ // REST API defaults
51
+ const DEFAULT_REST_HOST = "https://api.crisp.chat";
52
+ const DEFAULT_REST_BASE_PATH = "/v1/";
53
+
54
+ // RTM API defaults
55
+ const DEFAULT_RTM_MODE = "websockets";
56
+
57
+ const DEFAULT_RTM_EVENTS = [
58
+ // Session Events
59
+ "session:update_availability",
60
+ "session:update_verify",
61
+ "session:request:initiated",
62
+ "session:set_email",
63
+ "session:set_phone",
64
+ "session:set_address",
65
+ "session:set_subject",
66
+ "session:set_avatar",
67
+ "session:set_nickname",
68
+ "session:set_origin",
69
+ "session:set_data",
70
+ "session:sync:pages",
71
+ "session:sync:events",
72
+ "session:sync:capabilities",
73
+ "session:sync:geolocation",
74
+ "session:sync:system",
75
+ "session:sync:network",
76
+ "session:sync:timezone",
77
+ "session:sync:locales",
78
+ "session:sync:rating",
79
+ "session:sync:topic",
80
+ "session:set_state",
81
+ "session:set_block",
82
+ "session:set_segments",
83
+ "session:set_opened",
84
+ "session:set_closed",
85
+ "session:set_participants",
86
+ "session:set_mentions",
87
+ "session:set_routing",
88
+ "session:set_inbox",
89
+ "session:removed",
90
+ "session:error",
91
+
92
+ // Message Events
93
+ "message:updated",
94
+ "message:send",
95
+ "message:received",
96
+ "message:removed",
97
+ "message:compose:send",
98
+ "message:compose:receive",
99
+ "message:acknowledge:read:send",
100
+ "message:acknowledge:read:received",
101
+ "message:acknowledge:unread:send",
102
+ "message:acknowledge:delivered",
103
+ "message:acknowledge:ignored",
104
+ "message:notify:unread:send",
105
+ "message:notify:unread:received",
106
+
107
+ // Spam Events
108
+ "spam:message",
109
+ "spam:decision",
110
+
111
+ // People Events
112
+ "people:profile:created",
113
+ "people:profile:updated",
114
+ "people:profile:removed",
115
+ "people:bind:session",
116
+ "people:sync:profile",
117
+ "people:import:progress",
118
+ "people:import:done",
119
+
120
+ // Campaign Events
121
+ "campaign:progress",
122
+ "campaign:dispatched",
123
+ "campaign:running",
124
+
125
+ // Browsing Events
126
+ "browsing:request:initiated",
127
+ "browsing:request:rejected",
128
+
129
+ // Call Events
130
+ "call:request:initiated",
131
+ "call:request:rejected",
132
+
133
+ // Identity Events
134
+ "identity:verify:request",
135
+
136
+ // Status Events
137
+ "status:health:changed",
138
+
139
+ // Website Event
140
+ "website:update_visitors_count",
141
+ "website:update_operators_availability",
142
+ "website:users:available",
143
+
144
+ // Bucket Events
145
+ "bucket:url:upload:generated",
146
+ "bucket:url:avatar:generated",
147
+ "bucket:url:website:generated",
148
+ "bucket:url:campaign:generated",
149
+ "bucket:url:helpdesk:generated",
150
+ "bucket:url:status:generated",
151
+ "bucket:url:processing:generated",
152
+ "bucket:url:crawler:generated",
153
+
154
+ // Media Events
155
+ "media:animation:listed",
156
+
157
+ // Email Event
158
+ "email:subscribe",
159
+ "email:track:view",
160
+
161
+ // Plugin Events
162
+ "plugin:channel",
163
+ "plugin:event",
164
+ "plugin:settings:saved"
165
+ ];
166
+
167
+ // REST API services
168
+ const services = {
169
+ Bucket: Bucket,
170
+ Media: Media,
171
+ Plugin: Plugin,
172
+ Website: Website
173
+ };
174
+
175
+ interface CrispAuth {
176
+ tier: CrispTier;
177
+ identifier: string | null;
178
+ key: string | null;
179
+ token: string | null;
180
+ }
181
+
182
+ export type CrispTier = "user" | "plugin";
183
+
184
+ /**
185
+ * Crisp API Library
186
+ */
187
+ export class Crisp {
188
+ public bucket: Bucket = new Bucket();
189
+ public media: Media = new Media();
190
+ public plugin: Plugin = new Plugin();
191
+
192
+ public website: WebsiteServiceInterface = new Website() as unknown as WebsiteServiceInterface;
193
+
194
+ /**
195
+ * @deprecated Use import { RTM_MODES } instead
196
+ */
197
+ public static RTM_MODES = {
198
+ WebSockets: "websockets" as RTM_MODES,
199
+ WebHooks: "webhooks" as RTM_MODES
200
+ };
201
+
202
+ public auth: CrispAuth = {
203
+ tier: "user",
204
+ identifier: null,
205
+ key: null,
206
+ token: null
207
+ };
208
+
209
+ public _rest = {
210
+ host: DEFAULT_REST_HOST,
211
+ basePath: DEFAULT_REST_BASE_PATH
212
+ };
213
+
214
+ public _rtm = {
215
+ host: "",
216
+ mode: DEFAULT_RTM_MODE as RTM_MODES
217
+ };
218
+
219
+ public _useragent = (DEFAULT_USERAGENT_PREFIX + VERSION);
220
+
221
+ public _emitter = mitt();
222
+
223
+ public _socket: Socket | null = null;
224
+ public _loopback: Emitter<Record<string, unknown>> | null = null;
225
+
226
+ public _lastEventRebind = null;
227
+
228
+ public _brokerScheduler: typeof setTimeout | null = null;
229
+
230
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, no-unused-vars
231
+ public _brokerBindHooks: ((modeInstance: any, emitter: any) => void)[] = [];
232
+
233
+ public _boundEvents = {};
234
+
235
+ /**
236
+ * Constructor
237
+ */
238
+ constructor() {
239
+ this._prepareServices();
240
+ }
241
+
242
+ /**
243
+ * Sets the REST API host
244
+ */
245
+ setRestHost(host: string) {
246
+ if (typeof host === "string") {
247
+ this._rest.host = host;
248
+ } else {
249
+ throw new Error("[Crisp] setRestHost: parameter host should be a string");
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Sets the RTM API host
255
+ */
256
+ setRtmHost(host: string) {
257
+ if (typeof host === "string") {
258
+ this._rtm.host = host;
259
+ } else {
260
+ throw new Error("[Crisp] setRtmHost: parameter host should be a string");
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Sets the RTM channel mode (ie. WebSockets or Web Hooks)
266
+ */
267
+ setRtmMode(mode: "websockets" | "webhooks") {
268
+ if (AVAILABLE_RTM_MODES.indexOf(mode) !== -1) {
269
+ this._rtm.mode = mode;
270
+ } else {
271
+ throw new Error(
272
+ "[Crisp] setRtmMode: parameter mode value should be one of: " +
273
+ AVAILABLE_RTM_MODES.join(", ")
274
+ );
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Sets the authentication tier
280
+ */
281
+ setTier(tier: "user" | "plugin") {
282
+ this.auth.tier = (tier || "user");
283
+ }
284
+
285
+ /**
286
+ * Authenticates
287
+ */
288
+ authenticate(identifier: string, key: string) {
289
+ // Store credentials
290
+ this.auth.identifier = identifier;
291
+ this.auth.key = key;
292
+
293
+ // Assign pre-computed authentication token
294
+ this.auth.token = Buffer.from(identifier + ":" + key).toString("base64");
295
+ }
296
+
297
+ /**
298
+ * Authenticates (with tier)
299
+ */
300
+ authenticateTier(tier: CrispTier, identifier: string, key: string) {
301
+ this.setTier(tier);
302
+ this.authenticate(identifier, key);
303
+ }
304
+
305
+ /* eslint-disable @typescript-eslint/no-explicit-any */
306
+
307
+ /**
308
+ * Method wrapper to HEAD a resource
309
+ */
310
+ head(resource: string, query?: object | null): Promise<any> {
311
+ return new Promise((resolve, reject) => {
312
+ this.request(
313
+ resource, "head", (query || {}), null, resolve, reject
314
+ );
315
+ });
316
+ }
317
+
318
+ /**
319
+ * Method wrapper to GET a resource
320
+ */
321
+ get(resource: string, query?: object): Promise<any> {
322
+ return new Promise((resolve, reject) => {
323
+ this.request(
324
+ resource, "get", (query || {}), null, resolve, reject
325
+ );
326
+ });
327
+ }
328
+
329
+ /**
330
+ * Method wrapper to POST a resource
331
+ */
332
+ post(resource: string, query: object | null, body: object | null): Promise<any> {
333
+ return new Promise((resolve, reject) => {
334
+ this.request(
335
+ resource, "post", (query || {}), (body || {}), resolve, reject
336
+ );
337
+ });
338
+ }
339
+
340
+ /**
341
+ * Method wrapper to PATCH a resource
342
+ */
343
+ patch(resource: string, query: object | null, body: object | null): Promise<any> {
344
+ return new Promise((resolve, reject) => {
345
+ this.request(
346
+ resource, "patch", (query || {}), (body || {}), resolve, reject
347
+ );
348
+ });
349
+ }
350
+
351
+ /**
352
+ * Method wrapper to PUT a resource
353
+ */
354
+ put(resource: string, query: object | null, body: object | null): Promise<any> {
355
+ return new Promise((resolve, reject) => {
356
+ this.request(
357
+ resource, "put", (query || {}), (body || {}), resolve, reject
358
+ );
359
+ });
360
+ }
361
+
362
+ /**
363
+ * Method wrapper to DELETE a resource
364
+ */
365
+ delete(resource: string, query?: object | null, body?: object | null): Promise<any> {
366
+ return new Promise((resolve, reject) => {
367
+ this.request(
368
+ resource, "delete", (query || {}), (body || null), resolve, reject
369
+ );
370
+ });
371
+ }
372
+
373
+ /* eslint-enable @typescript-eslint/no-explicit-any */
374
+
375
+ /**
376
+ * Binds RTM event
377
+ */
378
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, no-unused-vars
379
+ on(event: string, callback: (data: any) => void) {
380
+ // Ensure all input arguments are set
381
+ if (typeof event !== "string") {
382
+ throw new Error("[Crisp] on: parameter event should be a string");
383
+ }
384
+
385
+ if (typeof callback !== "function") {
386
+ throw new Error("[Crisp] on: parameter callback should be a function");
387
+ }
388
+
389
+ // Disallow unrecognized event names
390
+ if (DEFAULT_RTM_EVENTS.indexOf(event) === -1) {
391
+ throw new Error(
392
+ "[Crisp] on: parameter event value is not recognized: '" + event + "'"
393
+ );
394
+ }
395
+
396
+ // Important: we do not allow .on() to be called once socket is connected, \
397
+ // or loopback is bound as we consider event listeners must be bound \
398
+ // once all together. This prevents bogous integrations from sending \
399
+ // flood of 'socket:bind'` to the RTM API, if using WebSockets. Web \
400
+ // Hooks follows the same scheme for consistency's sake.
401
+ if (this._socket || this._loopback) {
402
+ throw new Error(
403
+ "[Crisp] on: connector is already bound, please listen to event " +
404
+ "earlier on: '" + event + "'"
405
+ );
406
+ }
407
+
408
+ // Add listener to emitter
409
+ this._emitter.on(event, callback);
410
+
411
+ // Subscribe event on the broker
412
+ if (this._boundEvents[event] !== true) {
413
+ let rtmMode = this._rtm.mode;
414
+
415
+ // Mark event as bound
416
+ this._boundEvents[event] = true;
417
+
418
+ // Broker not connected? Connect now.
419
+ return this.prepareBroker(
420
+ (instance, emitter) => {
421
+ // Listen for event? (once instance is bound)
422
+ switch (rtmMode) {
423
+ case "websockets": {
424
+ // Listen on socket event
425
+ instance.on(event, (data) => {
426
+ emitter.emit(event, data);
427
+ });
428
+
429
+ break;
430
+ }
431
+ }
432
+ }
433
+ );
434
+ }
435
+
436
+ return Promise.resolve();
437
+ }
438
+
439
+ /**
440
+ * Receives a raw event and dispatches it to the listener (used for Web Hooks)
441
+ */
442
+ receiveHook(body: Record<string, unknown>) {
443
+ if (this._loopback) {
444
+ // Ensure payload is readable
445
+ if (!body || typeof body !== "object") {
446
+ return new Error("[Crisp] receiveHook: empty hook payload");
447
+ }
448
+
449
+ // Ensure payload is properly formatted
450
+ if (!body.event || !body.data ||
451
+ typeof body.event !== "string" || typeof body.data !== "object") {
452
+ return new Error("[Crisp] receiveHook: malformatted hook payload");
453
+ }
454
+
455
+ // Check if event is subscribed to? (in routing table)
456
+ // Notice: if not in routing table, then silently discard the event w/o \
457
+ // any error, as we do not want an HTTP failure status to be sent in \
458
+ // response by the implementor.
459
+ if (this._boundEvents[body.event] !== true) {
460
+ return null;
461
+ }
462
+
463
+ // Dispatch event to event bus
464
+ // Notice: go asynchronous, so that the event is processed ASAP and \
465
+ // dispatched on the event bus later, as the hook might be received \
466
+ // synchronously over HTTP.
467
+ process.nextTick(() => {
468
+ this._loopback.emit(body.event as string, body.data);
469
+ });
470
+
471
+ return null;
472
+ }
473
+
474
+ return new Error("[Crisp] receiveHook: hook loopback not bound");
475
+ }
476
+
477
+ /**
478
+ * Verifies an event string and checks that signatures match (used for Web \
479
+ * Hooks)
480
+ */
481
+ verifyHook(secret: string, body: object, timestamp: number, signature: string) {
482
+ if (this._loopback) {
483
+ return this.verifySignature(secret, body, timestamp, signature);
484
+ }
485
+
486
+ // Default: not verified (loopback not /yet?/ bound)
487
+ return false;
488
+ }
489
+
490
+ /**
491
+ * Verifies an event string and checks that signatures match (used for \
492
+ * Widgets)
493
+ */
494
+ verifyWidget(secret: string, body: object, timestamp: number, signature: string) {
495
+ return this.verifySignature(secret, body, timestamp, signature);
496
+ }
497
+
498
+ /**
499
+ * Rebinds socket events (used for WebSockets)
500
+ */
501
+ rebindSocket() {
502
+ if (!this._socket) {
503
+ throw new Error(
504
+ "[Crisp] rebindSocket: cannot rebind a socket that is not yet bound"
505
+ );
506
+ }
507
+
508
+ // Make sure that the library user is not rebinding too frequently (which \
509
+ // is illegal)
510
+ const nowTime = Date.now();
511
+
512
+ if (this._lastEventRebind !== null &&
513
+ ((nowTime - this._lastEventRebind) <
514
+ DEFAULT_EVENT_REBIND_INTERVAL_MIN)) {
515
+ throw new Error(
516
+ "[Crisp] rebindSocket: cannot rebind, last rebind was requested too " +
517
+ "recently"
518
+ );
519
+ }
520
+
521
+ return Promise.resolve()
522
+ .then(() => {
523
+ // Rebind to socket events (eg. newly bound websites)
524
+ this._lastEventRebind = nowTime;
525
+
526
+ this._socket.emit("socket:bind", {});
527
+
528
+ return Promise.resolve();
529
+ });
530
+ }
531
+
532
+ /**
533
+ * Prepares a URI based from path segments
534
+ */
535
+ prepareRestUrl(paths: string[]) {
536
+ if (Array.isArray(paths) === true) {
537
+ let output = this._rest.host + this._rest.basePath;
538
+
539
+ output += paths.join("/");
540
+
541
+ return output;
542
+ }
543
+
544
+ throw new Error(
545
+ "[Crisp] prepareRestUrl: parameter host should be an Array"
546
+ );
547
+ }
548
+
549
+ /**
550
+ * Binds services to the main object
551
+ */
552
+ _prepareServices() {
553
+ // Bind services
554
+ for (const name in services) {
555
+ const serviceInstance = new services[name]();
556
+
557
+ // Acquire service map
558
+ const serviceMap = this[(name[0].toLowerCase() + name.substring(1))];
559
+
560
+ // No resources defined in service?
561
+ if (!serviceInstance.__resources ||
562
+ serviceInstance.__resources.length === 0) {
563
+ throw new Error(
564
+ "[Crisp] prepareServices: service '" + name + "' has no resources " +
565
+ "defined"
566
+ );
567
+ }
568
+
569
+ // Prepare all resources (for service)
570
+ this.prepareResources(
571
+ serviceMap, serviceInstance.__resources
572
+ );
573
+ }
574
+ }
575
+
576
+ /**
577
+ * Binds resources to the service object
578
+ */
579
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
580
+ private prepareResources(serviceMap: any, resources: any) {
581
+ for (let i = 0; i < resources.length; i++) {
582
+ const resourceConstructor = resources[i];
583
+
584
+ const resourceInstance = new resourceConstructor(this);
585
+
586
+ // Bind each method of the resource instance to the service map
587
+ for (const methodName of Object.getOwnPropertyNames(Object.getPrototypeOf(resourceInstance))) {
588
+ if (methodName !== "constructor") {
589
+ serviceMap[methodName] = resourceInstance[methodName].bind(resourceInstance);
590
+ }
591
+ }
592
+ }
593
+ }
594
+
595
+ /**
596
+ * Binds broker to the main object
597
+ */
598
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, no-unused-vars
599
+ private prepareBroker(fnBindHook: (modeInstance: any, emitter: any) => void) {
600
+ return new Promise((resolve, reject) => {
601
+ const rtmMode = this._rtm.mode;
602
+ const rtmHostOverride = this._rtm.host;
603
+
604
+ // Append bind hook to pending stack
605
+ this._brokerBindHooks.push(fnBindHook);
606
+
607
+ // Make sure to prepare broker once? (defer broker binding, waiting that \
608
+ // all listeners have been bound, that way we submit the list of \
609
+ // filtered events to the RTM API once, and never again in the future)
610
+ if (this._brokerScheduler === null) {
611
+ // Socket or loopback already set? We should not even have entered \
612
+ // there.
613
+ if (this._socket || this._loopback) {
614
+ throw new Error(
615
+ "[Crisp] prepareBroker: illegal call to prepare broker (tie break)"
616
+ );
617
+ }
618
+
619
+ // @ts-ignore
620
+ this._brokerScheduler = setTimeout(() => {
621
+ switch (rtmMode) {
622
+ case "websockets": {
623
+ // Connect to socket now
624
+ // Notice: will unstack broker bind hooks once ready
625
+ this.connectSocket(rtmHostOverride)
626
+ .then(resolve)
627
+ .catch(reject);
628
+
629
+ break;
630
+ }
631
+
632
+ case "webhooks": {
633
+ // Connect to loopback now
634
+ this.connectLoopback()
635
+ .then(resolve)
636
+ .catch(reject);
637
+
638
+ break;
639
+ }
640
+
641
+ default: {
642
+ const unsupportedError = new Error(
643
+ "[Crisp] prepareBroker: mode of RTM broker unsupported " +
644
+ "('" + rtmMode + "')"
645
+ );
646
+
647
+ reject(unsupportedError);
648
+ }
649
+ }
650
+ }, DEFAULT_BROKER_SCHEDULE);
651
+ } else {
652
+ // Pass-through
653
+ resolve(true);
654
+ }
655
+ });
656
+ }
657
+
658
+ /**
659
+ * Connects loopback (used for Web Hooks)
660
+ */
661
+ private connectLoopback() {
662
+ return Promise.resolve()
663
+ .then(() => {
664
+ // Assign emitter to loopback
665
+ this._loopback = this._emitter;
666
+
667
+ // Unstack broker bind hooks immediately
668
+ this.unstackBrokerBindHooks(this._loopback);
669
+
670
+ return Promise.resolve();
671
+ });
672
+ }
673
+
674
+ /**
675
+ * Connects socket, using preferred RTM API host (used for WebSockets)
676
+ */
677
+ private connectSocket(rtmHostOverride: string) {
678
+ return Promise.resolve()
679
+ .then(() => {
680
+ // Any override RTM API host?
681
+ if (rtmHostOverride) {
682
+ return Promise.resolve({
683
+ socket: {
684
+ app: rtmHostOverride
685
+ }
686
+ });
687
+ }
688
+
689
+ // Acquire RTM API URL from remote
690
+ let restUrlSegments;
691
+
692
+ switch (this.auth.tier) {
693
+ case "plugin": {
694
+ restUrlSegments = ["plugin", "connect", "endpoints"];
695
+
696
+ break;
697
+ }
698
+
699
+ default: {
700
+ restUrlSegments = ["user", "connect", "endpoints"];
701
+ }
702
+ }
703
+
704
+ return this.get(
705
+ this.prepareRestUrl(restUrlSegments)
706
+ )
707
+ .catch(() => {
708
+ // Void error (consider as empty response)
709
+ return Promise.resolve({});
710
+ });
711
+ })
712
+ .then((endpoints) => {
713
+ // @ts-ignore
714
+ const rtmHostAffinity = (endpoints?.socket?.app || null);
715
+
716
+ // No RTM API host acquired?
717
+ if (rtmHostAffinity === null) {
718
+ throw new Error(
719
+ "[Crisp] connectSocket: could not acquire target host to " +
720
+ "connect to, is your session valid for tier?"
721
+ );
722
+ }
723
+
724
+ // Parse target RTM API host as an URL object
725
+ const rtmHostUrl = new URL(rtmHostAffinity);
726
+
727
+ // Connect to socket
728
+ // @ts-ignore
729
+ this._socket = socketio(rtmHostUrl.origin, {
730
+ path: (rtmHostUrl.pathname || "/"),
731
+ transports: ["websocket"],
732
+ timeout: DEFAULT_SOCKET_TIMEOUT,
733
+ reconnection: true,
734
+ reconnectionDelay: DEFAULT_SOCKET_RECONNECT_DELAY,
735
+ reconnectionDelayMax: DEFAULT_SOCKET_RECONNECT_DELAY_MAX,
736
+ randomizationFactor: DEFAULT_SOCKET_RECONNECT_FACTOR
737
+ });
738
+
739
+ this.emitAuthenticateSocket();
740
+
741
+ // Setup base socket event listeners
742
+ this._socket?.io.on("reconnect", () => {
743
+ this.emitAuthenticateSocket();
744
+ });
745
+
746
+ this._socket?.on("unauthorized", () => {
747
+ throw new Error(
748
+ "[Crisp] connectSocket: cannot listen for events as " +
749
+ "authentication is invalid"
750
+ );
751
+ });
752
+
753
+ // Setup user socket event listeners
754
+ this.unstackBrokerBindHooks(this._socket);
755
+
756
+ return Promise.resolve();
757
+ });
758
+ }
759
+
760
+ /**
761
+ * Authenticates client (used for WebSockets)
762
+ */
763
+ private emitAuthenticateSocket() {
764
+ const auth = this.auth;
765
+ const boundEvents = Object.keys(this._boundEvents);
766
+
767
+ if (!this._socket) {
768
+ throw new Error(
769
+ "[Crisp] emitAuthenticateSocket: cannot listen for events as socket " +
770
+ "is not yet bound"
771
+ );
772
+ }
773
+
774
+ if (!auth.identifier || !auth.key) {
775
+ throw new Error(
776
+ "[Crisp] emitAuthenticateSocket: cannot listen for events as you " +
777
+ "did not authenticate"
778
+ );
779
+ }
780
+
781
+ if (boundEvents.length === 0) {
782
+ throw new Error(
783
+ "[Crisp] emitAuthenticateSocket: cannot listen for events as no " +
784
+ "event is being listened to"
785
+ );
786
+ }
787
+
788
+ this._socket.emit("authentication", {
789
+ username: auth.identifier,
790
+ password: auth.key,
791
+ tier: auth.tier,
792
+ events: boundEvents
793
+ });
794
+ }
795
+
796
+ /**
797
+ * Unstacks pending broker bind hooks
798
+ */
799
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
800
+ private unstackBrokerBindHooks(modeInstance: any) {
801
+ // Setup user socket event listeners
802
+ while (this._brokerBindHooks.length > 0) {
803
+ this._brokerBindHooks.shift()?.(
804
+ modeInstance, this._emitter
805
+ );
806
+ }
807
+ }
808
+
809
+ /**
810
+ * Performs a request to REST API
811
+ */
812
+ private request(
813
+ resource: string,
814
+ method: string,
815
+ query: object,
816
+ body: object | null,
817
+ // eslint-disable-next-line no-unused-vars
818
+ resolve: (value: unknown) => void,
819
+ // eslint-disable-next-line no-unused-vars
820
+ reject: (reason?: unknown) => void
821
+ ) {
822
+ let requestParameters = {
823
+ responseType: "json",
824
+ timeout: DEFAULT_REQUEST_TIMEOUT,
825
+
826
+ headers: {
827
+ "User-Agent": this._useragent,
828
+ "X-Crisp-Tier": this.auth.tier
829
+ },
830
+
831
+ throwHttpErrors: false
832
+ };
833
+
834
+ // Add authorization?
835
+ if (this.auth.token) {
836
+ // @ts-ignore
837
+ requestParameters.headers.Authorization = ("Basic " + this.auth.token);
838
+ }
839
+
840
+ // Add body?
841
+ if (body) {
842
+ // @ts-ignore
843
+ requestParameters.json = body;
844
+ }
845
+
846
+ // Add query?
847
+ if (query) {
848
+ // @ts-ignore
849
+ requestParameters.searchParams = query;
850
+ }
851
+
852
+ // Proceed request
853
+ got[method](resource, requestParameters)
854
+ .catch((error) => {
855
+ return Promise.resolve(error);
856
+ })
857
+ .then((response) => {
858
+ // Request error?
859
+ if (!response.statusCode) {
860
+ return reject({
861
+ reason: "error",
862
+ message: "internal_error",
863
+ code: 500,
864
+
865
+ data: {
866
+ namespace: "request",
867
+
868
+ message: (
869
+ "Got request error: " + (response.name || "Unknown")
870
+ )
871
+ }
872
+ });
873
+ }
874
+
875
+ // Response error?
876
+ if (response.statusCode >= 400) {
877
+ let reasonMessage = this.readErrorResponseReason(
878
+ method, response.statusCode, response
879
+ );
880
+
881
+ const dataMessage = (response?.body?.data?.message || "");
882
+
883
+ return reject({
884
+ reason: "error",
885
+ message: reasonMessage,
886
+ code: response.statusCode,
887
+
888
+ data: {
889
+ namespace: "response",
890
+
891
+ message: (
892
+ "Got response error: " + (dataMessage || reasonMessage)
893
+ )
894
+ }
895
+ });
896
+ }
897
+
898
+ // Regular response
899
+ return resolve(
900
+ (response?.body?.data || {})
901
+ );
902
+ });
903
+ }
904
+
905
+ /**
906
+ * Reads reason for error response
907
+ */
908
+ private readErrorResponseReason(method: string, statusCode: number, response: object) {
909
+ // HEAD method? As HEAD requests do not expect any response body, then we \
910
+ // cannot map a reason from the response.
911
+ if (method === "head") {
912
+ // 5xx errors?
913
+ if (statusCode >= 500) {
914
+ return "server_error";
915
+ }
916
+
917
+ // 4xx errors?
918
+ if (statusCode >= 400) {
919
+ return "route_error";
920
+ }
921
+ }
922
+
923
+ // Other methods must hold a response body, therefore we can fallback on \
924
+ // an HTTP error if we fail to acquire any reason at all.
925
+ // @ts-ignore
926
+ return ((response?.body?.reason || "http_error"));
927
+ }
928
+
929
+ /**
930
+ * Verifies an event string and checks that signatures match
931
+ */
932
+ private verifySignature(secret: string, body: object, timestamp: number, signature: string) {
933
+ // Ensure all provided data is valid
934
+ if (!secret || !signature || !body || typeof body !== "object" ||
935
+ !timestamp || isNaN(timestamp) === true) {
936
+ return false;
937
+ }
938
+
939
+ // Compute local trace
940
+ let localTrace = ("[" + timestamp + ";" + JSON.stringify(body) + "]");
941
+
942
+ // Create local HMAC
943
+ let localMac = Crypto.createHmac("sha256", secret);
944
+
945
+ localMac.update(localTrace);
946
+
947
+ // Compute local signature, and compare
948
+ let localSignature = localMac.digest("hex");
949
+
950
+ return (
951
+ (signature === localSignature) ? true : false
952
+ );
953
+ }
954
+ };
955
+
956
+ export default Crisp;
957
+