@webex/internal-plugin-board 2.59.1 → 2.59.3-next.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/src/board.js CHANGED
@@ -1,764 +1,764 @@
1
- /*!
2
- * Copyright (c) 2015-2022 Cisco Systems, Inc. See LICENSE file.
3
- */
4
-
5
- import querystring from 'querystring';
6
-
7
- import {WebexPlugin, Page} from '@webex/webex-core';
8
- import promiseSeries from 'es6-promise-series';
9
- import {assign, defaults, chunk, pick} from 'lodash';
10
-
11
- import Realtime from './realtime';
12
-
13
- const Board = WebexPlugin.extend({
14
- namespace: 'Board',
15
-
16
- children: {
17
- realtime: Realtime,
18
- },
19
-
20
- /**
21
- * Adds Content to a Channel
22
- * If contents length is greater than config.board.numberContentsPerPageForAdd, this method
23
- * will break contents into chunks and make multiple GET request to the
24
- * board service
25
- * @memberof Board.BoardService
26
- * @param {Board~Channel} channel
27
- * @param {Array} contents - Array of {@link Board~Content} objects
28
- * @returns {Promise<Board~Content>}
29
- */
30
- addContent(channel, contents) {
31
- let chunks = [];
32
-
33
- chunks = chunk(contents, this.config.numberContentsPerPageForAdd);
34
-
35
- // we want the first promise to resolve before continuing with the next
36
- // chunk or else we'll have race conditions among patches
37
- return promiseSeries(chunks.map((part) => this._addContentChunk.bind(this, channel, part)));
38
- },
39
-
40
- /**
41
- * Adds Image to a Channel
42
- * Uploads image to webex files and adds SCR + downloadUrl to the persistence
43
- * service
44
- * @memberof Board.BoardService
45
- * @param {Board~Channel} channel
46
- * @param {File} image - image to be uploaded
47
- * @param {Object} metadata - metadata such as displayName
48
- * @returns {Promise<Board~Content>}
49
- */
50
- addImage(channel, image, metadata) {
51
- return this.webex.internal.board._uploadImage(channel, image).then((scr) =>
52
- this.webex.internal.board.addContent(channel, [
53
- {
54
- type: 'FILE',
55
- metadata,
56
- file: {
57
- mimeType: image.type,
58
- scr,
59
- size: image.size,
60
- url: scr.loc,
61
- },
62
- },
63
- ])
64
- );
65
- },
66
-
67
- /**
68
- * Set a snapshot image for a board
69
- *
70
- * @param {Board~Channel} channel
71
- * @param {File} image
72
- * @returns {Promise<Board~Channel>}
73
- */
74
- setSnapshotImage(channel, image) {
75
- let imageScr;
76
-
77
- return this.webex.internal.board
78
- ._uploadImage(channel, image, {hiddenSpace: true})
79
- .then((scr) => {
80
- imageScr = scr;
81
-
82
- return this.webex.internal.encryption.encryptScr(channel.defaultEncryptionKeyUrl, imageScr);
83
- })
84
- .then((encryptedScr) => {
85
- imageScr.encryptedScr = encryptedScr;
86
-
87
- return encryptedScr;
88
- })
89
- .then(() => {
90
- const imageBody = {
91
- image: {
92
- url: imageScr.loc,
93
- height: image.height || 900,
94
- width: image.width || 1600,
95
- mimeType: image.type || 'image/png',
96
- scr: imageScr.encryptedScr,
97
- encryptionKeyUrl: channel.defaultEncryptionKeyUrl,
98
- fileSize: image.size,
99
- },
100
- };
101
-
102
- return this.webex.request({
103
- method: 'PATCH',
104
- uri: channel.channelUrl,
105
- body: imageBody,
106
- });
107
- })
108
- .then((res) => res.body);
109
- },
110
-
111
- /**
112
- * Creates a Channel
113
- * @memberof Board.BoardService
114
- * @param {Conversation~ConversationObject} conversation
115
- * @param {Board~Channel} channel
116
- * @returns {Promise<Board~Channel>}
117
- */
118
- createChannel(conversation, channel) {
119
- return this.webex
120
- .request({
121
- method: 'POST',
122
- api: 'board',
123
- resource: '/channels',
124
- body: this._prepareChannel(conversation, channel),
125
- })
126
- .then((res) => res.body);
127
- },
128
-
129
- _prepareChannel(conversation, channel) {
130
- return {
131
- aclUrlLink: conversation.aclUrl,
132
- kmsMessage: {
133
- method: 'create',
134
- uri: '/resources',
135
- userIds: [conversation.kmsResourceObjectUrl],
136
- keyUris: [],
137
- },
138
- ...channel,
139
- };
140
- },
141
-
142
- /**
143
- * Deletes a Channel from a Conversation
144
- * @memberof Board.BoardService
145
- * @param {Conversation~ConversationObject} conversation
146
- * @param {Board~Channel} channel
147
- * @param {Object} options
148
- * @param {Object} options.preventDeleteActiveChannel Returns error if channel is in use
149
- * @returns {Promise}
150
- */
151
- deleteChannel(conversation, channel, options = {}) {
152
- // remove the ACL link between conversation and board
153
- // remove conversation auth from board KRO in kms message
154
- const body = {
155
- aclLinkType: 'INCOMING',
156
- linkedAcl: conversation.aclUrl,
157
- kmsMessage: {
158
- method: 'delete',
159
- uri: `${channel.kmsResourceUrl}/authorizations?${querystring.stringify({
160
- authId: conversation.kmsResourceObjectUrl,
161
- })}`,
162
- },
163
- aclLinkOperation: 'DELETE',
164
- };
165
-
166
- let promise = Promise.resolve();
167
-
168
- if (options.preventDeleteActiveChannel) {
169
- promise = this.lockChannelForDeletion(channel);
170
- }
171
-
172
- return promise
173
- .then(() =>
174
- this.webex.request({
175
- method: 'PUT',
176
- uri: `${channel.aclUrl}/links`,
177
- body,
178
- })
179
- )
180
- .then((res) => res.body);
181
- },
182
-
183
- /**
184
- * Locks and marks a channel for deletion
185
- * If a channel is being used, it will return 409 - Conflict
186
- * @memberof Board.BoardService
187
- * @param {Board~Channel} channel
188
- * @returns {Promise}
189
- */
190
- lockChannelForDeletion(channel) {
191
- return this.webex
192
- .request({
193
- method: 'POST',
194
- uri: `${channel.channelUrl}/lock`,
195
- qs: {
196
- intent: 'delete',
197
- },
198
- })
199
- .then((res) => res.body);
200
- },
201
-
202
- /**
203
- * Keeps a channel as 'active' to prevent other people from deleting it
204
- * @param {Board~Channel} channel
205
- * @returns {Promise}
206
- */
207
- keepActive(channel) {
208
- return this.webex.request({
209
- method: 'POST',
210
- uri: `${channel.channelUrl}/keepAlive`,
211
- });
212
- },
213
-
214
- /**
215
- * Decrypts a collection of content objects
216
- *
217
- * @memberof Board.BoardService
218
- * @param {Array} contents curves, text, and images
219
- * @returns {Promise<Array>} Resolves with an array of {@link Board~Content} objects.
220
- */
221
- decryptContents(contents) {
222
- return Promise.all(
223
- contents.items.map((content) => {
224
- let decryptPromise;
225
-
226
- if (content.type === 'FILE') {
227
- decryptPromise = this.decryptSingleFileContent(content.encryptionKeyUrl, content);
228
- } else {
229
- decryptPromise = this.decryptSingleContent(content.encryptionKeyUrl, content.payload);
230
- }
231
-
232
- return decryptPromise.then((res) => {
233
- Reflect.deleteProperty(content, 'payload');
234
- Reflect.deleteProperty(content, 'encryptionKeyUrl');
235
-
236
- return defaults(res, content);
237
- });
238
- })
239
- );
240
- },
241
-
242
- /**
243
- * Decryts a single STRING content object
244
- * @memberof Board.BoardService
245
- * @param {string} encryptionKeyUrl
246
- * @param {string} encryptedData
247
- * @returns {Promise<Board~Content>}
248
- */
249
- decryptSingleContent(encryptionKeyUrl, encryptedData) {
250
- return this.webex.internal.encryption
251
- .decryptText(encryptionKeyUrl, encryptedData)
252
- .then((res) => JSON.parse(res));
253
- },
254
-
255
- /**
256
- * Decryts a single FILE content object
257
- * @memberof Board.BoardService
258
- * @param {string} encryptionKeyUrl
259
- * @param {object} encryptedContent {file, payload}
260
- * @returns {Promise<Board~Content>}
261
- */
262
- decryptSingleFileContent(encryptionKeyUrl, encryptedContent) {
263
- let metadata;
264
-
265
- if (encryptedContent.payload) {
266
- metadata = encryptedContent.payload;
267
- }
268
-
269
- return this.webex.internal.encryption
270
- .decryptScr(encryptionKeyUrl, encryptedContent.file.scr)
271
- .then((scr) => {
272
- encryptedContent.file.scr = scr;
273
- if (metadata) {
274
- return this.webex.internal.encryption.decryptText(encryptionKeyUrl, metadata);
275
- }
276
-
277
- return '';
278
- })
279
- .then((decryptedMetadata) => {
280
- try {
281
- encryptedContent.metadata = JSON.parse(decryptedMetadata);
282
- if (encryptedContent.metadata.displayName) {
283
- encryptedContent.displayName = encryptedContent.metadata.displayName;
284
- }
285
- } catch (error) {
286
- encryptedContent.metadata = {};
287
- }
288
-
289
- return encryptedContent;
290
- });
291
- },
292
-
293
- /**
294
- * Deletes all Content from a Channel
295
- * @memberof Board.BoardService
296
- * @param {Board~Channel} channel
297
- * @returns {Promise} Resolves with an content response
298
- */
299
- deleteAllContent(channel) {
300
- return this.webex
301
- .request({
302
- method: 'DELETE',
303
- uri: `${channel.channelUrl}/contents`,
304
- })
305
- .then((res) => res.body);
306
- },
307
-
308
- /**
309
- * Deletes Contents from a Channel except the ones listed in contentsToKeep
310
- * @memberof Board.BoardService
311
- * @param {Board~Channel} channel
312
- * @param {Array<Board~Content>} contentsToKeep Array of board objects (curves, text, and images) with valid contentId (received from server)
313
- * @returns {Promise} Resolves with an content response
314
- */
315
- deletePartialContent(channel, contentsToKeep) {
316
- const body = contentsToKeep.map((content) => pick(content, 'contentId'));
317
-
318
- return this.webex
319
- .request({
320
- method: 'POST',
321
- uri: `${channel.channelUrl}/contents`,
322
- body,
323
- qs: {
324
- clearBoard: true,
325
- },
326
- })
327
- .then((res) => res.body);
328
- },
329
-
330
- /**
331
- * Encrypts a collection of content
332
- * @memberof Board.BoardService
333
- * @param {string} encryptionKeyUrl channel.defaultEncryptionKeyUrl
334
- * @param {Array} contents Array of {@link Board~Content} objects. (curves, text, and images)
335
- * @returns {Promise<Array>} Resolves with an array of encrypted {@link Board~Content} objects.
336
- */
337
- encryptContents(encryptionKeyUrl, contents) {
338
- return Promise.all(
339
- contents.map((content) => {
340
- let encryptionPromise;
341
- let contentType = 'STRING';
342
-
343
- // the existence of an scr will determine if the content is a FILE.
344
- if (content.file) {
345
- contentType = 'FILE';
346
- encryptionPromise = this.encryptSingleFileContent(encryptionKeyUrl, content);
347
- } else {
348
- encryptionPromise = this.encryptSingleContent(encryptionKeyUrl, content);
349
- }
350
-
351
- return encryptionPromise.then((res) =>
352
- assign(
353
- {
354
- device: this.webex.internal.device.deviceType,
355
- type: contentType,
356
- encryptionKeyUrl,
357
- },
358
- pick(res, 'file', 'payload')
359
- )
360
- );
361
- })
362
- );
363
- },
364
-
365
- /**
366
- * Encrypts a single STRING content object
367
- * @memberof Board.BoardService
368
- * @param {string} encryptionKeyUrl
369
- * @param {Board~Content} content
370
- * @returns {Promise<Board~Content>}
371
- */
372
- encryptSingleContent(encryptionKeyUrl, content) {
373
- return this.webex.internal.encryption
374
- .encryptText(encryptionKeyUrl, JSON.stringify(content))
375
- .then((res) => ({
376
- payload: res,
377
- encryptionKeyUrl,
378
- }));
379
- },
380
-
381
- /**
382
- * Encrypts a single FILE content object
383
- * @memberof Board.BoardService
384
- * @param {string} encryptionKeyUrl
385
- * @param {Board~Content} content
386
- * @returns {Promise<Board~Content>}
387
- */
388
- encryptSingleFileContent(encryptionKeyUrl, content) {
389
- return this.webex.internal.encryption
390
- .encryptScr(encryptionKeyUrl, content.file.scr)
391
- .then((encryptedScr) => {
392
- content.file.scr = encryptedScr;
393
- if (content.displayName) {
394
- content.metadata = assign(content.metadata, {displayName: content.displayName});
395
- }
396
- if (content.metadata) {
397
- return this.webex.internal.encryption
398
- .encryptText(encryptionKeyUrl, JSON.stringify(content.metadata))
399
- .then((encryptedMetadata) => {
400
- content.metadata = encryptedMetadata;
401
- });
402
- }
403
-
404
- return content;
405
- })
406
- .then(() => ({
407
- file: content.file,
408
- payload: content.metadata,
409
- encryptionKeyUrl,
410
- }));
411
- },
412
-
413
- /**
414
- * Retrieves contents from a specified channel
415
- * @memberof Board.BoardService
416
- * @param {Board~Channel} channel
417
- * @param {Object} options
418
- * @param {Object} options.qs
419
- * @returns {Promise<Page<Board~Channel>>} Resolves with an array of Content items
420
- */
421
- getContents(channel, options) {
422
- options = options || {};
423
-
424
- const params = {
425
- uri: `${channel.channelUrl}/contents`,
426
- qs: {
427
- contentsLimit: this.config.numberContentsPerPageForGet,
428
- },
429
- };
430
-
431
- assign(params.qs, pick(options, 'contentsLimit'));
432
-
433
- return this.request(params).then((res) => new Page(res, this.webex));
434
- },
435
-
436
- /**
437
- * Gets a Channel
438
- * @memberof Board.BoardService
439
- * @param {Board~Channel} channel
440
- * @returns {Promise<Board~Channel>}
441
- */
442
- getChannel(channel) {
443
- return this.webex
444
- .request({
445
- method: 'GET',
446
- uri: channel.channelUrl,
447
- })
448
- .then((res) => res.body);
449
- },
450
-
451
- /**
452
- * Gets Channels
453
- * @memberof Board.BoardService
454
- * @param {Conversation~ConversationObject} conversation
455
- * @param {Object} options
456
- * @param {number} options.channelsLimit number of boards to return per page
457
- * @param {number} options.type type of whiteboard: whiteboard or annotated
458
- * @returns {Promise<Page<Board~Channel>>} Resolves with an array of Channel items
459
- */
460
- getChannels(conversation, options) {
461
- options = options || {};
462
-
463
- if (!conversation) {
464
- return Promise.reject(new Error('`conversation` is required'));
465
- }
466
-
467
- const params = {
468
- api: 'board',
469
- resource: '/channels',
470
- qs: {
471
- aclUrlLink: conversation.aclUrl,
472
- },
473
- };
474
-
475
- assign(params.qs, pick(options, 'channelsLimit', 'type'));
476
-
477
- return this.request(params).then((res) => new Page(res, this.webex));
478
- },
479
-
480
- /**
481
- * Pings persistence
482
- * @memberof Board.BoardService
483
- * @returns {Promise<Object>} ping response body
484
- */
485
- ping() {
486
- return this.webex
487
- .request({
488
- method: 'GET',
489
- api: 'board',
490
- resource: '/ping',
491
- })
492
- .then((res) => res.body);
493
- },
494
-
495
- processActivityEvent(message) {
496
- let decryptionPromise;
497
-
498
- if (message.contentType === 'FILE') {
499
- decryptionPromise = this.decryptSingleFileContent(
500
- message.envelope.encryptionKeyUrl,
501
- message.payload
502
- );
503
- } else {
504
- decryptionPromise = this.decryptSingleContent(
505
- message.envelope.encryptionKeyUrl,
506
- message.payload
507
- );
508
- }
509
-
510
- return decryptionPromise.then((decryptedData) => {
511
- // call the event handlers
512
- message.payload = decryptedData;
513
-
514
- return message;
515
- });
516
- },
517
-
518
- /**
519
- * Registers with Mercury
520
- * @memberof Board.BoardService
521
- * @param {Object} data - Mercury bindings
522
- * @returns {Promise<Board~Registration>}
523
- */
524
- register(data) {
525
- return this.webex
526
- .request({
527
- method: 'POST',
528
- api: 'board',
529
- resource: '/registrations',
530
- body: data,
531
- })
532
- .then((res) => res.body);
533
- },
534
-
535
- /**
536
- * Registers with Mercury for sharing web socket
537
- * @memberof Board.BoardService
538
- * @param {Board~Channel} channel
539
- * @returns {Promise<Board~Registration>}
540
- */
541
- registerToShareMercury(channel) {
542
- return this.webex.internal.feature
543
- .getFeature('developer', 'web-shared-mercury')
544
- .then((isSharingMercuryFeatureEnabled) => {
545
- if (!this.webex.internal.mercury.localClusterServiceUrls) {
546
- return Promise.reject(
547
- new Error('`localClusterServiceUrls` is not defined, make sure mercury is connected')
548
- );
549
- }
550
- if (!isSharingMercuryFeatureEnabled) {
551
- return Promise.reject(new Error('`web-shared-mercury` is not enabled'));
552
- }
553
-
554
- const {webSocketUrl} = this.webex.internal.device;
555
- const {mercuryConnectionServiceClusterUrl} =
556
- this.webex.internal.mercury.localClusterServiceUrls;
557
-
558
- const data = {
559
- mercuryConnectionServiceClusterUrl,
560
- webSocketUrl,
561
- action: 'ADD',
562
- };
563
-
564
- return this.webex.request({
565
- method: 'POST',
566
- uri: `${channel.channelUrl}/register`,
567
- body: data,
568
- });
569
- })
570
- .then((res) => res.body);
571
- },
572
-
573
- /**
574
- * Remove board binding from existing mercury connection
575
- * @memberof Board.BoardService
576
- * @param {Board~Channel} channel
577
- * @param {String} binding - the binding as provided in board registration
578
- * @returns {Promise<Board~Registration>}
579
- */
580
- unregisterFromSharedMercury(channel, binding) {
581
- const {webSocketUrl} = this.webex.internal.device;
582
- const data = {
583
- binding,
584
- webSocketUrl,
585
- action: 'REMOVE',
586
- };
587
-
588
- return this.webex
589
- .request({
590
- method: 'POST',
591
- uri: `${channel.channelUrl}/register`,
592
- body: data,
593
- })
594
- .then((res) => res.body);
595
- },
596
-
597
- _addContentChunk(channel, contentChunk) {
598
- return this.webex.internal.board
599
- .encryptContents(channel.defaultEncryptionKeyUrl, contentChunk)
600
- .then((res) =>
601
- this.webex.request({
602
- method: 'POST',
603
- uri: `${channel.channelUrl}/contents`,
604
- body: res,
605
- })
606
- )
607
- .then((res) => res.body);
608
- },
609
-
610
- /**
611
- * Encrypts and uploads image to WebexFiles
612
- * @memberof Board.BoardService
613
- * @param {Board~Channel} channel
614
- * @param {File} file - File to be uploaded
615
- * @param {Object} options
616
- * @param {Object} options.hiddenSpace - true for hidden, false for open space
617
- * @private
618
- * @returns {Object} Encrypted Scr and KeyUrl
619
- */
620
- _uploadImage(channel, file, options) {
621
- options = options || {};
622
-
623
- return this.webex.internal.encryption
624
- .encryptBinary(file)
625
- .then(({scr, cdata}) =>
626
- Promise.all([scr, this._uploadImageToWebexFiles(channel, cdata, options.hiddenSpace)])
627
- )
628
- .then(([scr, res]) => assign(scr, {loc: res.downloadUrl}));
629
- },
630
-
631
- _getSpaceUrl(channel, hiddenSpace) {
632
- let requestUri = `${channel.channelUrl}/spaces/open`;
633
-
634
- if (hiddenSpace) {
635
- requestUri = `${channel.channelUrl}/spaces/hidden`;
636
- }
637
-
638
- return this.webex
639
- .request({
640
- method: 'PUT',
641
- uri: requestUri,
642
- })
643
- .then((res) => res.body.spaceUrl);
644
- },
645
-
646
- _uploadImageToWebexFiles(channel, file, hiddenSpace) {
647
- const fileSize = file.length || file.size || file.byteLength;
648
-
649
- return this._getSpaceUrl(channel, hiddenSpace).then((spaceUrl) =>
650
- this.webex.upload({
651
- uri: `${spaceUrl}/upload_sessions`,
652
- file,
653
- qs: {
654
- transcode: true,
655
- },
656
- phases: {
657
- initialize: {fileSize},
658
- upload: {
659
- $url(session) {
660
- return session.uploadUrl;
661
- },
662
- },
663
- finalize: {
664
- $uri(session) {
665
- return session.finishUploadUrl;
666
- },
667
- body: {fileSize},
668
- },
669
- },
670
- })
671
- );
672
- },
673
-
674
- /** Authorize transcoder (for sharing whiteboard to mobile)
675
- *
676
- * @param {Board~Channel} board
677
- * @memberof Board.BoardService
678
- * @returns {String} authorization
679
- */
680
- authorizeMediaInjector(board) {
681
- if (!board) {
682
- Promise.reject(
683
- new Error('#authorizeMediaInjector --> cannot authorize transcoder without board')
684
- );
685
- }
686
-
687
- return this.webex.internal.encryption.kms
688
- .prepareRequest({
689
- method: 'create',
690
- uri: '/authorizations',
691
- resourceUri: board.kmsResourceUrl,
692
- anonymous: 1,
693
- })
694
- .then((request) =>
695
- this.webex.request({
696
- uri: `${board.channelUrl}/sharePolicies/transcoder`,
697
- method: 'PUT',
698
- body: {kmsMessage: request.wrapped},
699
- })
700
- )
701
- .then((res) => this.webex.internal.encryption.kms.decryptKmsMessage(res.body.kmsResponse))
702
- .then((decryptedKmsMessage) => {
703
- if (decryptedKmsMessage?.authorizations.length > 0) {
704
- return decryptedKmsMessage.authorizations[0].bearer;
705
- }
706
-
707
- return undefined;
708
- })
709
- .catch((err) =>
710
- /* We want to resolve any errors so that whiteboard share will still work
711
- * except mobile being able to receive the share
712
- */
713
- Promise.resolve(err)
714
- );
715
- },
716
-
717
- /** Unauthorize transcoder (for stopping whiteboard share to mobile)
718
- *
719
- * @param {Board~Channel} board
720
- * @memberof Board.BoardService
721
- * @returns {Array} list of authIds removed
722
- */
723
- unauthorizeMediaInjector(board) {
724
- if (!board) {
725
- Promise.reject(
726
- new Error('#unauthorizeMediaInjector --> cannot unauthorize transcoder without board')
727
- );
728
- }
729
-
730
- return this.webex.internal.encryption.kms
731
- .listAuthorizations({
732
- kroUri: board.kmsResourceUrl,
733
- })
734
- .then((authorizations) => {
735
- /* Attempt to remove the authorization made from starting whiteboard share
736
- * Also removing any previous authorizations that were not cleared
737
- */
738
- const promises = authorizations.map((auth) => {
739
- const {authId} = auth;
740
-
741
- return this.webex.internal.encryption.kms
742
- .removeAuthorization({
743
- authId,
744
- kroUri: board.kmsResourceUrl,
745
- })
746
- .then(() => Promise.resolve(authId))
747
- .catch((err) =>
748
- /* We don't want this to error out, otherwise the
749
- * Promise.all will not process the rest of the request
750
- */
751
- Promise.resolve(err)
752
- );
753
- });
754
-
755
- if (promises.length > 0) {
756
- return Promise.all(promises).then((responses) => responses);
757
- }
758
-
759
- return Promise.resolve([]);
760
- });
761
- },
762
- });
763
-
764
- export default Board;
1
+ /*!
2
+ * Copyright (c) 2015-2022 Cisco Systems, Inc. See LICENSE file.
3
+ */
4
+
5
+ import querystring from 'querystring';
6
+
7
+ import {WebexPlugin, Page} from '@webex/webex-core';
8
+ import promiseSeries from 'es6-promise-series';
9
+ import {assign, defaults, chunk, pick} from 'lodash';
10
+
11
+ import Realtime from './realtime';
12
+
13
+ const Board = WebexPlugin.extend({
14
+ namespace: 'Board',
15
+
16
+ children: {
17
+ realtime: Realtime,
18
+ },
19
+
20
+ /**
21
+ * Adds Content to a Channel
22
+ * If contents length is greater than config.board.numberContentsPerPageForAdd, this method
23
+ * will break contents into chunks and make multiple GET request to the
24
+ * board service
25
+ * @memberof Board.BoardService
26
+ * @param {Board~Channel} channel
27
+ * @param {Array} contents - Array of {@link Board~Content} objects
28
+ * @returns {Promise<Board~Content>}
29
+ */
30
+ addContent(channel, contents) {
31
+ let chunks = [];
32
+
33
+ chunks = chunk(contents, this.config.numberContentsPerPageForAdd);
34
+
35
+ // we want the first promise to resolve before continuing with the next
36
+ // chunk or else we'll have race conditions among patches
37
+ return promiseSeries(chunks.map((part) => this._addContentChunk.bind(this, channel, part)));
38
+ },
39
+
40
+ /**
41
+ * Adds Image to a Channel
42
+ * Uploads image to webex files and adds SCR + downloadUrl to the persistence
43
+ * service
44
+ * @memberof Board.BoardService
45
+ * @param {Board~Channel} channel
46
+ * @param {File} image - image to be uploaded
47
+ * @param {Object} metadata - metadata such as displayName
48
+ * @returns {Promise<Board~Content>}
49
+ */
50
+ addImage(channel, image, metadata) {
51
+ return this.webex.internal.board._uploadImage(channel, image).then((scr) =>
52
+ this.webex.internal.board.addContent(channel, [
53
+ {
54
+ type: 'FILE',
55
+ metadata,
56
+ file: {
57
+ mimeType: image.type,
58
+ scr,
59
+ size: image.size,
60
+ url: scr.loc,
61
+ },
62
+ },
63
+ ])
64
+ );
65
+ },
66
+
67
+ /**
68
+ * Set a snapshot image for a board
69
+ *
70
+ * @param {Board~Channel} channel
71
+ * @param {File} image
72
+ * @returns {Promise<Board~Channel>}
73
+ */
74
+ setSnapshotImage(channel, image) {
75
+ let imageScr;
76
+
77
+ return this.webex.internal.board
78
+ ._uploadImage(channel, image, {hiddenSpace: true})
79
+ .then((scr) => {
80
+ imageScr = scr;
81
+
82
+ return this.webex.internal.encryption.encryptScr(channel.defaultEncryptionKeyUrl, imageScr);
83
+ })
84
+ .then((encryptedScr) => {
85
+ imageScr.encryptedScr = encryptedScr;
86
+
87
+ return encryptedScr;
88
+ })
89
+ .then(() => {
90
+ const imageBody = {
91
+ image: {
92
+ url: imageScr.loc,
93
+ height: image.height || 900,
94
+ width: image.width || 1600,
95
+ mimeType: image.type || 'image/png',
96
+ scr: imageScr.encryptedScr,
97
+ encryptionKeyUrl: channel.defaultEncryptionKeyUrl,
98
+ fileSize: image.size,
99
+ },
100
+ };
101
+
102
+ return this.webex.request({
103
+ method: 'PATCH',
104
+ uri: channel.channelUrl,
105
+ body: imageBody,
106
+ });
107
+ })
108
+ .then((res) => res.body);
109
+ },
110
+
111
+ /**
112
+ * Creates a Channel
113
+ * @memberof Board.BoardService
114
+ * @param {Conversation~ConversationObject} conversation
115
+ * @param {Board~Channel} channel
116
+ * @returns {Promise<Board~Channel>}
117
+ */
118
+ createChannel(conversation, channel) {
119
+ return this.webex
120
+ .request({
121
+ method: 'POST',
122
+ api: 'board',
123
+ resource: '/channels',
124
+ body: this._prepareChannel(conversation, channel),
125
+ })
126
+ .then((res) => res.body);
127
+ },
128
+
129
+ _prepareChannel(conversation, channel) {
130
+ return {
131
+ aclUrlLink: conversation.aclUrl,
132
+ kmsMessage: {
133
+ method: 'create',
134
+ uri: '/resources',
135
+ userIds: [conversation.kmsResourceObjectUrl],
136
+ keyUris: [],
137
+ },
138
+ ...channel,
139
+ };
140
+ },
141
+
142
+ /**
143
+ * Deletes a Channel from a Conversation
144
+ * @memberof Board.BoardService
145
+ * @param {Conversation~ConversationObject} conversation
146
+ * @param {Board~Channel} channel
147
+ * @param {Object} options
148
+ * @param {Object} options.preventDeleteActiveChannel Returns error if channel is in use
149
+ * @returns {Promise}
150
+ */
151
+ deleteChannel(conversation, channel, options = {}) {
152
+ // remove the ACL link between conversation and board
153
+ // remove conversation auth from board KRO in kms message
154
+ const body = {
155
+ aclLinkType: 'INCOMING',
156
+ linkedAcl: conversation.aclUrl,
157
+ kmsMessage: {
158
+ method: 'delete',
159
+ uri: `${channel.kmsResourceUrl}/authorizations?${querystring.stringify({
160
+ authId: conversation.kmsResourceObjectUrl,
161
+ })}`,
162
+ },
163
+ aclLinkOperation: 'DELETE',
164
+ };
165
+
166
+ let promise = Promise.resolve();
167
+
168
+ if (options.preventDeleteActiveChannel) {
169
+ promise = this.lockChannelForDeletion(channel);
170
+ }
171
+
172
+ return promise
173
+ .then(() =>
174
+ this.webex.request({
175
+ method: 'PUT',
176
+ uri: `${channel.aclUrl}/links`,
177
+ body,
178
+ })
179
+ )
180
+ .then((res) => res.body);
181
+ },
182
+
183
+ /**
184
+ * Locks and marks a channel for deletion
185
+ * If a channel is being used, it will return 409 - Conflict
186
+ * @memberof Board.BoardService
187
+ * @param {Board~Channel} channel
188
+ * @returns {Promise}
189
+ */
190
+ lockChannelForDeletion(channel) {
191
+ return this.webex
192
+ .request({
193
+ method: 'POST',
194
+ uri: `${channel.channelUrl}/lock`,
195
+ qs: {
196
+ intent: 'delete',
197
+ },
198
+ })
199
+ .then((res) => res.body);
200
+ },
201
+
202
+ /**
203
+ * Keeps a channel as 'active' to prevent other people from deleting it
204
+ * @param {Board~Channel} channel
205
+ * @returns {Promise}
206
+ */
207
+ keepActive(channel) {
208
+ return this.webex.request({
209
+ method: 'POST',
210
+ uri: `${channel.channelUrl}/keepAlive`,
211
+ });
212
+ },
213
+
214
+ /**
215
+ * Decrypts a collection of content objects
216
+ *
217
+ * @memberof Board.BoardService
218
+ * @param {Array} contents curves, text, and images
219
+ * @returns {Promise<Array>} Resolves with an array of {@link Board~Content} objects.
220
+ */
221
+ decryptContents(contents) {
222
+ return Promise.all(
223
+ contents.items.map((content) => {
224
+ let decryptPromise;
225
+
226
+ if (content.type === 'FILE') {
227
+ decryptPromise = this.decryptSingleFileContent(content.encryptionKeyUrl, content);
228
+ } else {
229
+ decryptPromise = this.decryptSingleContent(content.encryptionKeyUrl, content.payload);
230
+ }
231
+
232
+ return decryptPromise.then((res) => {
233
+ Reflect.deleteProperty(content, 'payload');
234
+ Reflect.deleteProperty(content, 'encryptionKeyUrl');
235
+
236
+ return defaults(res, content);
237
+ });
238
+ })
239
+ );
240
+ },
241
+
242
+ /**
243
+ * Decryts a single STRING content object
244
+ * @memberof Board.BoardService
245
+ * @param {string} encryptionKeyUrl
246
+ * @param {string} encryptedData
247
+ * @returns {Promise<Board~Content>}
248
+ */
249
+ decryptSingleContent(encryptionKeyUrl, encryptedData) {
250
+ return this.webex.internal.encryption
251
+ .decryptText(encryptionKeyUrl, encryptedData)
252
+ .then((res) => JSON.parse(res));
253
+ },
254
+
255
+ /**
256
+ * Decryts a single FILE content object
257
+ * @memberof Board.BoardService
258
+ * @param {string} encryptionKeyUrl
259
+ * @param {object} encryptedContent {file, payload}
260
+ * @returns {Promise<Board~Content>}
261
+ */
262
+ decryptSingleFileContent(encryptionKeyUrl, encryptedContent) {
263
+ let metadata;
264
+
265
+ if (encryptedContent.payload) {
266
+ metadata = encryptedContent.payload;
267
+ }
268
+
269
+ return this.webex.internal.encryption
270
+ .decryptScr(encryptionKeyUrl, encryptedContent.file.scr)
271
+ .then((scr) => {
272
+ encryptedContent.file.scr = scr;
273
+ if (metadata) {
274
+ return this.webex.internal.encryption.decryptText(encryptionKeyUrl, metadata);
275
+ }
276
+
277
+ return '';
278
+ })
279
+ .then((decryptedMetadata) => {
280
+ try {
281
+ encryptedContent.metadata = JSON.parse(decryptedMetadata);
282
+ if (encryptedContent.metadata.displayName) {
283
+ encryptedContent.displayName = encryptedContent.metadata.displayName;
284
+ }
285
+ } catch (error) {
286
+ encryptedContent.metadata = {};
287
+ }
288
+
289
+ return encryptedContent;
290
+ });
291
+ },
292
+
293
+ /**
294
+ * Deletes all Content from a Channel
295
+ * @memberof Board.BoardService
296
+ * @param {Board~Channel} channel
297
+ * @returns {Promise} Resolves with an content response
298
+ */
299
+ deleteAllContent(channel) {
300
+ return this.webex
301
+ .request({
302
+ method: 'DELETE',
303
+ uri: `${channel.channelUrl}/contents`,
304
+ })
305
+ .then((res) => res.body);
306
+ },
307
+
308
+ /**
309
+ * Deletes Contents from a Channel except the ones listed in contentsToKeep
310
+ * @memberof Board.BoardService
311
+ * @param {Board~Channel} channel
312
+ * @param {Array<Board~Content>} contentsToKeep Array of board objects (curves, text, and images) with valid contentId (received from server)
313
+ * @returns {Promise} Resolves with an content response
314
+ */
315
+ deletePartialContent(channel, contentsToKeep) {
316
+ const body = contentsToKeep.map((content) => pick(content, 'contentId'));
317
+
318
+ return this.webex
319
+ .request({
320
+ method: 'POST',
321
+ uri: `${channel.channelUrl}/contents`,
322
+ body,
323
+ qs: {
324
+ clearBoard: true,
325
+ },
326
+ })
327
+ .then((res) => res.body);
328
+ },
329
+
330
+ /**
331
+ * Encrypts a collection of content
332
+ * @memberof Board.BoardService
333
+ * @param {string} encryptionKeyUrl channel.defaultEncryptionKeyUrl
334
+ * @param {Array} contents Array of {@link Board~Content} objects. (curves, text, and images)
335
+ * @returns {Promise<Array>} Resolves with an array of encrypted {@link Board~Content} objects.
336
+ */
337
+ encryptContents(encryptionKeyUrl, contents) {
338
+ return Promise.all(
339
+ contents.map((content) => {
340
+ let encryptionPromise;
341
+ let contentType = 'STRING';
342
+
343
+ // the existence of an scr will determine if the content is a FILE.
344
+ if (content.file) {
345
+ contentType = 'FILE';
346
+ encryptionPromise = this.encryptSingleFileContent(encryptionKeyUrl, content);
347
+ } else {
348
+ encryptionPromise = this.encryptSingleContent(encryptionKeyUrl, content);
349
+ }
350
+
351
+ return encryptionPromise.then((res) =>
352
+ assign(
353
+ {
354
+ device: this.webex.internal.device.deviceType,
355
+ type: contentType,
356
+ encryptionKeyUrl,
357
+ },
358
+ pick(res, 'file', 'payload')
359
+ )
360
+ );
361
+ })
362
+ );
363
+ },
364
+
365
+ /**
366
+ * Encrypts a single STRING content object
367
+ * @memberof Board.BoardService
368
+ * @param {string} encryptionKeyUrl
369
+ * @param {Board~Content} content
370
+ * @returns {Promise<Board~Content>}
371
+ */
372
+ encryptSingleContent(encryptionKeyUrl, content) {
373
+ return this.webex.internal.encryption
374
+ .encryptText(encryptionKeyUrl, JSON.stringify(content))
375
+ .then((res) => ({
376
+ payload: res,
377
+ encryptionKeyUrl,
378
+ }));
379
+ },
380
+
381
+ /**
382
+ * Encrypts a single FILE content object
383
+ * @memberof Board.BoardService
384
+ * @param {string} encryptionKeyUrl
385
+ * @param {Board~Content} content
386
+ * @returns {Promise<Board~Content>}
387
+ */
388
+ encryptSingleFileContent(encryptionKeyUrl, content) {
389
+ return this.webex.internal.encryption
390
+ .encryptScr(encryptionKeyUrl, content.file.scr)
391
+ .then((encryptedScr) => {
392
+ content.file.scr = encryptedScr;
393
+ if (content.displayName) {
394
+ content.metadata = assign(content.metadata, {displayName: content.displayName});
395
+ }
396
+ if (content.metadata) {
397
+ return this.webex.internal.encryption
398
+ .encryptText(encryptionKeyUrl, JSON.stringify(content.metadata))
399
+ .then((encryptedMetadata) => {
400
+ content.metadata = encryptedMetadata;
401
+ });
402
+ }
403
+
404
+ return content;
405
+ })
406
+ .then(() => ({
407
+ file: content.file,
408
+ payload: content.metadata,
409
+ encryptionKeyUrl,
410
+ }));
411
+ },
412
+
413
+ /**
414
+ * Retrieves contents from a specified channel
415
+ * @memberof Board.BoardService
416
+ * @param {Board~Channel} channel
417
+ * @param {Object} options
418
+ * @param {Object} options.qs
419
+ * @returns {Promise<Page<Board~Channel>>} Resolves with an array of Content items
420
+ */
421
+ getContents(channel, options) {
422
+ options = options || {};
423
+
424
+ const params = {
425
+ uri: `${channel.channelUrl}/contents`,
426
+ qs: {
427
+ contentsLimit: this.config.numberContentsPerPageForGet,
428
+ },
429
+ };
430
+
431
+ assign(params.qs, pick(options, 'contentsLimit'));
432
+
433
+ return this.request(params).then((res) => new Page(res, this.webex));
434
+ },
435
+
436
+ /**
437
+ * Gets a Channel
438
+ * @memberof Board.BoardService
439
+ * @param {Board~Channel} channel
440
+ * @returns {Promise<Board~Channel>}
441
+ */
442
+ getChannel(channel) {
443
+ return this.webex
444
+ .request({
445
+ method: 'GET',
446
+ uri: channel.channelUrl,
447
+ })
448
+ .then((res) => res.body);
449
+ },
450
+
451
+ /**
452
+ * Gets Channels
453
+ * @memberof Board.BoardService
454
+ * @param {Conversation~ConversationObject} conversation
455
+ * @param {Object} options
456
+ * @param {number} options.channelsLimit number of boards to return per page
457
+ * @param {number} options.type type of whiteboard: whiteboard or annotated
458
+ * @returns {Promise<Page<Board~Channel>>} Resolves with an array of Channel items
459
+ */
460
+ getChannels(conversation, options) {
461
+ options = options || {};
462
+
463
+ if (!conversation) {
464
+ return Promise.reject(new Error('`conversation` is required'));
465
+ }
466
+
467
+ const params = {
468
+ api: 'board',
469
+ resource: '/channels',
470
+ qs: {
471
+ aclUrlLink: conversation.aclUrl,
472
+ },
473
+ };
474
+
475
+ assign(params.qs, pick(options, 'channelsLimit', 'type'));
476
+
477
+ return this.request(params).then((res) => new Page(res, this.webex));
478
+ },
479
+
480
+ /**
481
+ * Pings persistence
482
+ * @memberof Board.BoardService
483
+ * @returns {Promise<Object>} ping response body
484
+ */
485
+ ping() {
486
+ return this.webex
487
+ .request({
488
+ method: 'GET',
489
+ api: 'board',
490
+ resource: '/ping',
491
+ })
492
+ .then((res) => res.body);
493
+ },
494
+
495
+ processActivityEvent(message) {
496
+ let decryptionPromise;
497
+
498
+ if (message.contentType === 'FILE') {
499
+ decryptionPromise = this.decryptSingleFileContent(
500
+ message.envelope.encryptionKeyUrl,
501
+ message.payload
502
+ );
503
+ } else {
504
+ decryptionPromise = this.decryptSingleContent(
505
+ message.envelope.encryptionKeyUrl,
506
+ message.payload
507
+ );
508
+ }
509
+
510
+ return decryptionPromise.then((decryptedData) => {
511
+ // call the event handlers
512
+ message.payload = decryptedData;
513
+
514
+ return message;
515
+ });
516
+ },
517
+
518
+ /**
519
+ * Registers with Mercury
520
+ * @memberof Board.BoardService
521
+ * @param {Object} data - Mercury bindings
522
+ * @returns {Promise<Board~Registration>}
523
+ */
524
+ register(data) {
525
+ return this.webex
526
+ .request({
527
+ method: 'POST',
528
+ api: 'board',
529
+ resource: '/registrations',
530
+ body: data,
531
+ })
532
+ .then((res) => res.body);
533
+ },
534
+
535
+ /**
536
+ * Registers with Mercury for sharing web socket
537
+ * @memberof Board.BoardService
538
+ * @param {Board~Channel} channel
539
+ * @returns {Promise<Board~Registration>}
540
+ */
541
+ registerToShareMercury(channel) {
542
+ return this.webex.internal.feature
543
+ .getFeature('developer', 'web-shared-mercury')
544
+ .then((isSharingMercuryFeatureEnabled) => {
545
+ if (!this.webex.internal.mercury.localClusterServiceUrls) {
546
+ return Promise.reject(
547
+ new Error('`localClusterServiceUrls` is not defined, make sure mercury is connected')
548
+ );
549
+ }
550
+ if (!isSharingMercuryFeatureEnabled) {
551
+ return Promise.reject(new Error('`web-shared-mercury` is not enabled'));
552
+ }
553
+
554
+ const {webSocketUrl} = this.webex.internal.device;
555
+ const {mercuryConnectionServiceClusterUrl} =
556
+ this.webex.internal.mercury.localClusterServiceUrls;
557
+
558
+ const data = {
559
+ mercuryConnectionServiceClusterUrl,
560
+ webSocketUrl,
561
+ action: 'ADD',
562
+ };
563
+
564
+ return this.webex.request({
565
+ method: 'POST',
566
+ uri: `${channel.channelUrl}/register`,
567
+ body: data,
568
+ });
569
+ })
570
+ .then((res) => res.body);
571
+ },
572
+
573
+ /**
574
+ * Remove board binding from existing mercury connection
575
+ * @memberof Board.BoardService
576
+ * @param {Board~Channel} channel
577
+ * @param {String} binding - the binding as provided in board registration
578
+ * @returns {Promise<Board~Registration>}
579
+ */
580
+ unregisterFromSharedMercury(channel, binding) {
581
+ const {webSocketUrl} = this.webex.internal.device;
582
+ const data = {
583
+ binding,
584
+ webSocketUrl,
585
+ action: 'REMOVE',
586
+ };
587
+
588
+ return this.webex
589
+ .request({
590
+ method: 'POST',
591
+ uri: `${channel.channelUrl}/register`,
592
+ body: data,
593
+ })
594
+ .then((res) => res.body);
595
+ },
596
+
597
+ _addContentChunk(channel, contentChunk) {
598
+ return this.webex.internal.board
599
+ .encryptContents(channel.defaultEncryptionKeyUrl, contentChunk)
600
+ .then((res) =>
601
+ this.webex.request({
602
+ method: 'POST',
603
+ uri: `${channel.channelUrl}/contents`,
604
+ body: res,
605
+ })
606
+ )
607
+ .then((res) => res.body);
608
+ },
609
+
610
+ /**
611
+ * Encrypts and uploads image to WebexFiles
612
+ * @memberof Board.BoardService
613
+ * @param {Board~Channel} channel
614
+ * @param {File} file - File to be uploaded
615
+ * @param {Object} options
616
+ * @param {Object} options.hiddenSpace - true for hidden, false for open space
617
+ * @private
618
+ * @returns {Object} Encrypted Scr and KeyUrl
619
+ */
620
+ _uploadImage(channel, file, options) {
621
+ options = options || {};
622
+
623
+ return this.webex.internal.encryption
624
+ .encryptBinary(file)
625
+ .then(({scr, cdata}) =>
626
+ Promise.all([scr, this._uploadImageToWebexFiles(channel, cdata, options.hiddenSpace)])
627
+ )
628
+ .then(([scr, res]) => assign(scr, {loc: res.downloadUrl}));
629
+ },
630
+
631
+ _getSpaceUrl(channel, hiddenSpace) {
632
+ let requestUri = `${channel.channelUrl}/spaces/open`;
633
+
634
+ if (hiddenSpace) {
635
+ requestUri = `${channel.channelUrl}/spaces/hidden`;
636
+ }
637
+
638
+ return this.webex
639
+ .request({
640
+ method: 'PUT',
641
+ uri: requestUri,
642
+ })
643
+ .then((res) => res.body.spaceUrl);
644
+ },
645
+
646
+ _uploadImageToWebexFiles(channel, file, hiddenSpace) {
647
+ const fileSize = file.length || file.size || file.byteLength;
648
+
649
+ return this._getSpaceUrl(channel, hiddenSpace).then((spaceUrl) =>
650
+ this.webex.upload({
651
+ uri: `${spaceUrl}/upload_sessions`,
652
+ file,
653
+ qs: {
654
+ transcode: true,
655
+ },
656
+ phases: {
657
+ initialize: {fileSize},
658
+ upload: {
659
+ $url(session) {
660
+ return session.uploadUrl;
661
+ },
662
+ },
663
+ finalize: {
664
+ $uri(session) {
665
+ return session.finishUploadUrl;
666
+ },
667
+ body: {fileSize},
668
+ },
669
+ },
670
+ })
671
+ );
672
+ },
673
+
674
+ /** Authorize transcoder (for sharing whiteboard to mobile)
675
+ *
676
+ * @param {Board~Channel} board
677
+ * @memberof Board.BoardService
678
+ * @returns {String} authorization
679
+ */
680
+ authorizeMediaInjector(board) {
681
+ if (!board) {
682
+ Promise.reject(
683
+ new Error('#authorizeMediaInjector --> cannot authorize transcoder without board')
684
+ );
685
+ }
686
+
687
+ return this.webex.internal.encryption.kms
688
+ .prepareRequest({
689
+ method: 'create',
690
+ uri: '/authorizations',
691
+ resourceUri: board.kmsResourceUrl,
692
+ anonymous: 1,
693
+ })
694
+ .then((request) =>
695
+ this.webex.request({
696
+ uri: `${board.channelUrl}/sharePolicies/transcoder`,
697
+ method: 'PUT',
698
+ body: {kmsMessage: request.wrapped},
699
+ })
700
+ )
701
+ .then((res) => this.webex.internal.encryption.kms.decryptKmsMessage(res.body.kmsResponse))
702
+ .then((decryptedKmsMessage) => {
703
+ if (decryptedKmsMessage?.authorizations.length > 0) {
704
+ return decryptedKmsMessage.authorizations[0].bearer;
705
+ }
706
+
707
+ return undefined;
708
+ })
709
+ .catch((err) =>
710
+ /* We want to resolve any errors so that whiteboard share will still work
711
+ * except mobile being able to receive the share
712
+ */
713
+ Promise.resolve(err)
714
+ );
715
+ },
716
+
717
+ /** Unauthorize transcoder (for stopping whiteboard share to mobile)
718
+ *
719
+ * @param {Board~Channel} board
720
+ * @memberof Board.BoardService
721
+ * @returns {Array} list of authIds removed
722
+ */
723
+ unauthorizeMediaInjector(board) {
724
+ if (!board) {
725
+ Promise.reject(
726
+ new Error('#unauthorizeMediaInjector --> cannot unauthorize transcoder without board')
727
+ );
728
+ }
729
+
730
+ return this.webex.internal.encryption.kms
731
+ .listAuthorizations({
732
+ kroUri: board.kmsResourceUrl,
733
+ })
734
+ .then((authorizations) => {
735
+ /* Attempt to remove the authorization made from starting whiteboard share
736
+ * Also removing any previous authorizations that were not cleared
737
+ */
738
+ const promises = authorizations.map((auth) => {
739
+ const {authId} = auth;
740
+
741
+ return this.webex.internal.encryption.kms
742
+ .removeAuthorization({
743
+ authId,
744
+ kroUri: board.kmsResourceUrl,
745
+ })
746
+ .then(() => Promise.resolve(authId))
747
+ .catch((err) =>
748
+ /* We don't want this to error out, otherwise the
749
+ * Promise.all will not process the rest of the request
750
+ */
751
+ Promise.resolve(err)
752
+ );
753
+ });
754
+
755
+ if (promises.length > 0) {
756
+ return Promise.all(promises).then((responses) => responses);
757
+ }
758
+
759
+ return Promise.resolve([]);
760
+ });
761
+ },
762
+ });
763
+
764
+ export default Board;