crisp-api 9.13.0 → 10.0.3

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