@webex/internal-plugin-calendar 2.48.0 → 2.50.0

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/calendar.js CHANGED
@@ -41,8 +41,8 @@
41
41
  * @instance
42
42
  * @memberof Calendar
43
43
  */
44
-
45
- import btoa from 'btoa';
44
+ import {isArray} from 'lodash';
45
+ import {base64} from '@webex/common';
46
46
  import {WebexPlugin} from '@webex/webex-core';
47
47
 
48
48
  import CalendarCollection from './collection';
@@ -54,6 +54,9 @@ import {
54
54
  CALENDAR_UPDATED,
55
55
  } from './constants';
56
56
 
57
+ import EncryptHelper from './calendar.encrypt.helper';
58
+ import DecryptHelper from './calendar.decrypt.helper';
59
+
57
60
  const Calendar = WebexPlugin.extend({
58
61
  namespace: 'Calendar',
59
62
 
@@ -65,6 +68,43 @@ const Calendar = WebexPlugin.extend({
65
68
  */
66
69
  registered: false,
67
70
 
71
+ /**
72
+ * Cache all rpc event request locally
73
+ * */
74
+ rpcEventRequests: [],
75
+
76
+ /**
77
+ * Cache KMS encryptionKeyUrl
78
+ * */
79
+ encryptionKeyUrl: null,
80
+
81
+ /**
82
+ * WebexPlugin initialize method. This triggers once Webex has completed its
83
+ * initialization workflow.
84
+ *
85
+ * If the plugin is meant to perform startup actions, place them in this
86
+ * `initialize()` method instead of the `constructor()` method.
87
+ * @returns {void}
88
+ */
89
+ initialize() {
90
+ // Used to perform actions after webex is fully qualified and ready for
91
+ // operation.
92
+ this.listenToOnce(this.webex, 'ready', () => {
93
+ // Pre-fetch a KMS encryption key url to improve performance
94
+ this.webex.internal.encryption.kms.createUnboundKeys({count: 1}).then((keys) => {
95
+ const key = isArray(keys) ? keys[0] : keys;
96
+ this.encryptionKeyUrl = key ? key.uri : null;
97
+ this.logger.info('calendar->bind a KMS encryption key url');
98
+ this.webex.internal.encryption
99
+ .getKey(this.encryptionKeyUrl, {onBehalfOf: null})
100
+ .then((retrievedKey) => {
101
+ this.encryptionKeyUrl = retrievedKey ? retrievedKey.uri : null;
102
+ this.logger.info('calendar->retrieve the KMS encryption key url and cache it');
103
+ });
104
+ });
105
+ });
106
+ },
107
+
68
108
  /**
69
109
  * Explicitly sets up the calendar plugin by registering
70
110
  * the device, connecting to mercury, and listening for calendar events.
@@ -148,6 +188,9 @@ const Calendar = WebexPlugin.extend({
148
188
  this.webex.internal.mercury.on('event:calendar.meeting.delete', (envelope) => {
149
189
  this._handleDelete(envelope.data);
150
190
  });
191
+ this.webex.internal.mercury.on('event:calendar.free_busy', (envelope) => {
192
+ this._handleFreeBusy(envelope.data);
193
+ });
151
194
  },
152
195
 
153
196
  /**
@@ -161,6 +204,7 @@ const Calendar = WebexPlugin.extend({
161
204
  this.webex.internal.mercury.off('event:calendar.meeting.update');
162
205
  this.webex.internal.mercury.off('event:calendar.meeting.update.minimal');
163
206
  this.webex.internal.mercury.off('event:calendar.meeting.delete');
207
+ this.webex.internal.mercury.off('event:calendar.free_busy');
164
208
  },
165
209
 
166
210
  /**
@@ -199,6 +243,32 @@ const Calendar = WebexPlugin.extend({
199
243
  this.trigger(CALENDAR_DELETE, item);
200
244
  },
201
245
 
246
+ /**
247
+ * handles free_busy events
248
+ * @param {Object} data
249
+ * @returns {undefined}
250
+ * @private
251
+ */
252
+ _handleFreeBusy(data) {
253
+ DecryptHelper.decryptFreeBusyResponse(this, data).then(() => {
254
+ let response = {};
255
+ if (data && data.calendarFreeBusyScheduleResponse) {
256
+ response = data.calendarFreeBusyScheduleResponse;
257
+ }
258
+ if (response && response.requestId && response.requestId in this.rpcEventRequests) {
259
+ this.logger.log(
260
+ `webex.internal.calendar - receive requests, requestId: ${response.requestId}`
261
+ );
262
+ delete response.encryptionKeyUrl;
263
+ const {resolve} = this.rpcEventRequests[response.requestId];
264
+ resolve(response);
265
+ delete this.rpcEventRequests[response.requestId];
266
+ } else {
267
+ this.logger.log('webex.internal.calendar - receive other requests.');
268
+ }
269
+ });
270
+ },
271
+
202
272
  /**
203
273
  * Retrieves a collection of calendars based on the request parameters
204
274
  * Defaults to 1 day before and 7 days ahead
@@ -253,7 +323,7 @@ const Calendar = WebexPlugin.extend({
253
323
  return this.request({
254
324
  method: 'GET',
255
325
  service: 'calendar',
256
- resource: `calendarEvents/${btoa(id)}/participants`,
326
+ resource: `calendarEvents/${base64.encode(id)}/participants`,
257
327
  });
258
328
  },
259
329
 
@@ -266,7 +336,7 @@ const Calendar = WebexPlugin.extend({
266
336
  return this.request({
267
337
  method: 'GET',
268
338
  service: 'calendar',
269
- resource: `calendarEvents/${btoa(id)}/notes`,
339
+ resource: `calendarEvents/${base64.encode(id)}/notes`,
270
340
  });
271
341
  },
272
342
 
@@ -312,6 +382,114 @@ const Calendar = WebexPlugin.extend({
312
382
  return Promise.all(promises).then(() => meetingObjects);
313
383
  });
314
384
  },
385
+
386
+ /**
387
+ * Create calendar event
388
+ * @param {object} [data] meeting payload data
389
+ * @returns {Promise} Resolves with creating calendar event response
390
+ * */
391
+ createCalendarEvent(data) {
392
+ return EncryptHelper.encryptCalendarEventRequest(this, data).then(() =>
393
+ this.request({
394
+ method: 'POST',
395
+ service: 'calendar',
396
+ body: data,
397
+ resource: 'calendarEvents/sync',
398
+ })
399
+ );
400
+ },
401
+
402
+ /**
403
+ * Update calendar event
404
+ * @param {string} [id] calendar event id
405
+ * @param {object} [data] meeting payload data
406
+ * @param {object} [query] the query parameters for specific usage
407
+ * @returns {Promise} Resolves with updating calendar event response
408
+ * */
409
+ updateCalendarEvent(id, data, query) {
410
+ return EncryptHelper.encryptCalendarEventRequest(this, data).then(() =>
411
+ this.request({
412
+ method: 'PATCH',
413
+ service: 'calendar',
414
+ body: data,
415
+ resource: `calendarEvents/${base64.encode(id)}/sync`,
416
+ qs: query || {},
417
+ })
418
+ );
419
+ },
420
+
421
+ /**
422
+ * Delete calendar event
423
+ * @param {string} [id] calendar event id
424
+ * @returns {Promise} Resolves with deleting calendar event response
425
+ * */
426
+ deleteCalendarEvent(id) {
427
+ return this.request({
428
+ method: 'DELETE',
429
+ service: 'calendar',
430
+ resource: `calendarEvents/${base64.encode(id)}/sync`,
431
+ });
432
+ },
433
+
434
+ /**
435
+ * @typedef QuerySchedulerDataOptions
436
+ * @param {string} [siteName] it is site full url, must have. Example: ccctest.dmz.webex.com
437
+ * @param {string} [id] it is seriesOrOccurrenceId. If present, the series/occurrence meeting ID to fetch data for. It should be base64 encoded.
438
+ * Example: 040000008200E00074C5B7101A82E008000000004A99F11A0841D9010000000000000000100000009EE499D4A71C1A46B51494C70EC7BFE5
439
+ * @param {string} [clientMeetingId] If present, the client meeting UUID to fetch data for. It should be base64 encoded.
440
+ * Example: 7f318aa9-887c-6e94-802a-8dc8e6eb1a0a
441
+ * @param {string} [scheduleTemplateId] it template id.
442
+ * @param {string} [sessionTypeId] it session type id.
443
+ * @param {string} [organizerCIUserId] required in schedule-on-behalf case. It is the organizer's CI UUID.
444
+ * @param {boolean} [usmPreference]
445
+ * @param {string} [webexMeetingId] webex side meeting UUID
446
+ * @param {string} [eventId] event ID.
447
+ * @param {string} [icalUid] icalendar UUID.
448
+ * @param {string} [thirdPartyType] third part type, such as: Microsoft
449
+ */
450
+ /**
451
+ * Get scheduler data from calendar service
452
+ * @param {QuerySchedulerDataOptions} [query] the command parameters for fetching scheduler data.
453
+ * @returns {Promise} Resolves with a decrypted scheduler data
454
+ * */
455
+ getSchedulerData(query) {
456
+ return this.request({
457
+ method: 'GET',
458
+ service: 'calendar',
459
+ resource: 'schedulerData',
460
+ qs: query || {},
461
+ }).then((response) => {
462
+ return DecryptHelper.decryptSchedulerDataResponse(this, response.body).then(() => response);
463
+ });
464
+ },
465
+
466
+ /**
467
+ * Get free busy status from calendar service
468
+ * @param {Object} [data] the command parameters for fetching free busy status.
469
+ * @returns {Promise} Resolves with a decrypted response
470
+ * */
471
+ getFreeBusy(data) {
472
+ return new Promise((resolve, reject) => {
473
+ EncryptHelper.encryptFreeBusyRequest(this, data)
474
+ .then(() => {
475
+ this.request({
476
+ method: 'POST',
477
+ service: 'calendar',
478
+ body: data,
479
+ resource: 'freebusy',
480
+ })
481
+ .then(() => {
482
+ this.rpcEventRequests[data.requestId] = {resolve, reject};
483
+ })
484
+ .catch((error) => {
485
+ reject(error);
486
+ });
487
+ })
488
+ .catch((error) => {
489
+ reject(error);
490
+ });
491
+ });
492
+ },
315
493
  });
316
494
 
317
495
  export default Calendar;
package/src/index.js CHANGED
@@ -20,7 +20,15 @@ registerInternalPlugin('calendar', Calendar, {
20
20
  name: 'transformMeetingNotes',
21
21
  direction: 'inbound',
22
22
  test(ctx, response) {
23
- return Promise.resolve(has(response, 'body.encryptedNotes'));
23
+ return Promise.resolve(
24
+ has(response, 'body.encryptedNotes') &&
25
+ !(
26
+ response.options &&
27
+ response.options.service === 'calendar' &&
28
+ response.options.method === 'GET' &&
29
+ response.options.resource === 'schedulerData'
30
+ )
31
+ );
24
32
  },
25
33
  extract(response) {
26
34
  return Promise.resolve(response.body);
@@ -30,7 +38,15 @@ registerInternalPlugin('calendar', Calendar, {
30
38
  name: 'transformMeetingParticipants',
31
39
  direction: 'inbound',
32
40
  test(ctx, response) {
33
- return Promise.resolve(has(response, 'body.encryptedParticipants'));
41
+ return Promise.resolve(
42
+ has(response, 'body.encryptedParticipants') &&
43
+ !(
44
+ response.options &&
45
+ response.options.service === 'calendar' &&
46
+ response.options.method === 'GET' &&
47
+ response.options.resource === 'schedulerData'
48
+ )
49
+ );
34
50
  },
35
51
  extract(response) {
36
52
  return Promise.resolve(response.body);
@@ -50,7 +66,15 @@ registerInternalPlugin('calendar', Calendar, {
50
66
  name: 'transformMeeting',
51
67
  direction: 'inbound',
52
68
  test(ctx, response) {
53
- return Promise.resolve(has(response, 'body.seriesId'));
69
+ return Promise.resolve(
70
+ has(response, 'body.seriesId') &&
71
+ !(
72
+ response.options &&
73
+ response.options.service === 'calendar' &&
74
+ response.options.method === 'GET' &&
75
+ response.options.resource === 'schedulerData'
76
+ )
77
+ );
54
78
  },
55
79
  extract(response) {
56
80
  return Promise.resolve(response.body);
@@ -2,10 +2,10 @@
2
2
  * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
3
  */
4
4
 
5
- import {assert} from '@webex/test-helper-chai';
5
+ import { assert, expect } from "@webex/test-helper-chai";
6
6
  import Calendar from '@webex/internal-plugin-calendar';
7
7
  import MockWebex from '@webex/test-helper-mock-webex';
8
- import btoa from 'btoa';
8
+ import { base64 } from "@webex/common";
9
9
  import sinon from 'sinon';
10
10
 
11
11
  import {
@@ -48,6 +48,14 @@ describe('internal-plugin-calendar', () => {
48
48
  }),
49
49
  off: sinon.spy(),
50
50
  };
51
+ webex.internal.encryption = {
52
+ kms: {
53
+ createUnboundKeys: sinon.stub().resolves([{
54
+ uri: "kms://kms-us-int.wbx2.com/keys/xxxx-xxxx-xxxx-xxxx"
55
+ }])
56
+ },
57
+ encryptText: sinon.stub().resolves("encryptedText")
58
+ };
51
59
  });
52
60
 
53
61
  describe('Public Api Contract', () => {
@@ -55,7 +63,7 @@ describe('internal-plugin-calendar', () => {
55
63
  it('on calendar register call mercury registration', async () => {
56
64
  await webex.internal.calendar.register();
57
65
  assert.calledOnce(webex.internal.device.register);
58
- assert.callCount(webex.internal.mercury.on, 5);
66
+ assert.callCount(webex.internal.mercury.on, 6);
59
67
  assert.equal(webex.internal.calendar.registered, true);
60
68
  });
61
69
  it('should trigger `calendar:register` event', async () => {
@@ -97,7 +105,7 @@ describe('internal-plugin-calendar', () => {
97
105
  it('should call `mercury.unregister` and `device.unregister`', async () => {
98
106
  await webex.internal.calendar.register();
99
107
  await webex.internal.calendar.unregister();
100
- assert.callCount(webex.internal.mercury.off, 5);
108
+ assert.callCount(webex.internal.mercury.off, 6);
101
109
  assert.calledOnce(webex.internal.mercury.disconnect);
102
110
  assert.calledOnce(webex.internal.device.unregister);
103
111
  });
@@ -208,7 +216,7 @@ describe('internal-plugin-calendar', () => {
208
216
  .withArgs({
209
217
  method: 'GET',
210
218
  service: 'calendar',
211
- resource: `calendarEvents/${btoa('calendar1')}/notes`,
219
+ resource: `calendarEvents/${base64.encode('calendar1')}/notes`,
212
220
  })
213
221
  .returns(
214
222
  Promise.resolve({
@@ -235,7 +243,7 @@ describe('internal-plugin-calendar', () => {
235
243
  assert.calledWith(webex.request, {
236
244
  method: 'GET',
237
245
  service: 'calendar',
238
- resource: `calendarEvents/${btoa('calendar1')}/notes`,
246
+ resource: `calendarEvents/${base64.encode('calendar1')}/notes`,
239
247
  });
240
248
  });
241
249
 
@@ -269,7 +277,7 @@ describe('internal-plugin-calendar', () => {
269
277
  .withArgs({
270
278
  method: 'GET',
271
279
  service: 'calendar',
272
- resource: `calendarEvents/${btoa('calendar1')}/notes`,
280
+ resource: `calendarEvents/${base64.encode('calendar1')}/notes`,
273
281
  })
274
282
  .returns(
275
283
  Promise.resolve({
@@ -298,7 +306,7 @@ describe('internal-plugin-calendar', () => {
298
306
  assert.calledWith(webex.request, {
299
307
  method: 'GET',
300
308
  service: 'calendar',
301
- resource: `calendarEvents/${btoa('calendar1')}/notes`,
309
+ resource: `calendarEvents/${base64.encode('calendar1')}/notes`,
302
310
  });
303
311
  });
304
312
  });
@@ -321,7 +329,7 @@ describe('internal-plugin-calendar', () => {
321
329
  assert.calledWith(webex.request, {
322
330
  method: 'GET',
323
331
  service: 'calendar',
324
- resource: `calendarEvents/${btoa(id)}/notes`,
332
+ resource: `calendarEvents/${base64.encode(id)}/notes`,
325
333
  });
326
334
  });
327
335
  });
@@ -344,7 +352,124 @@ describe('internal-plugin-calendar', () => {
344
352
  assert.calledWith(webex.request, {
345
353
  method: 'GET',
346
354
  service: 'calendar',
347
- resource: `calendarEvents/${btoa(id)}/participants`,
355
+ resource: `calendarEvents/${base64.encode(id)}/participants`,
356
+ });
357
+ });
358
+ });
359
+
360
+ describe("#getSchedulerData()", () => {
361
+ it("should fetch meeting calendar data", async () => {
362
+ const query = {
363
+ siteName: "scheduler01.dmz.webex.com",
364
+ clientMeetingId: "YWJjZGFiY2QtYWJjZC1hYmNkLWFiY2QtMDAwMDAwMDA"
365
+ };
366
+
367
+ webex.request = sinon.stub().resolves({
368
+ body: {
369
+ encryptedSubject: "My Meeting 1",
370
+ schedulerPreferences: {
371
+ uiControlAttributes: {
372
+ displayHostSaveMeetingTemplate: true
373
+ },
374
+ webexOptions: {
375
+ sessionTypeId: 3
376
+ }
377
+ }
378
+ }
379
+ });
380
+
381
+ const res = await webex.internal.calendar.getSchedulerData(query);
382
+
383
+ expect(res.body.encryptedSubject).to.equal("My Meeting 1");
384
+ expect(res.body.schedulerPreferences.uiControlAttributes.displayHostSaveMeetingTemplate).to.be.true;
385
+ expect(res.body.schedulerPreferences.webexOptions.sessionTypeId).to.equal(3);
386
+ assert.calledWith(webex.request, {
387
+ method: "GET",
388
+ service: "calendar",
389
+ resource: "schedulerData",
390
+ qs: {
391
+ siteName: query.siteName,
392
+ clientMeetingId: query.clientMeetingId
393
+ }
394
+ });
395
+ });
396
+ });
397
+
398
+ describe("#createCalendarEvent()", () => {
399
+ it("should create an calendar event", async () => {
400
+ const data = {
401
+ encryptionKeyUrl: "kms://kms-us-int.wbx2.com/keys/d1c14fc5-be10-4389-ae83-9521f92fbfd3",
402
+ notes: "This is Agenda",
403
+ subject: "My Meeting 1",
404
+ webexOptions: "{}"
405
+ };
406
+
407
+ webex.request = sinon.stub().resolves({
408
+ body: {
409
+ meetingId: "abcdabcd-abcd-abcd-abcd-00000000",
410
+ globalMeetingId: "xxxx-xxxx-xxxx-xxxx"
411
+ }
412
+ });
413
+
414
+ const res = await webex.internal.calendar.createCalendarEvent(data);
415
+
416
+ expect(res.body.meetingId).to.equal("abcdabcd-abcd-abcd-abcd-00000000");
417
+ expect(res.body.globalMeetingId).to.equal("xxxx-xxxx-xxxx-xxxx");
418
+ assert.calledWith(webex.request, {
419
+ method: "POST",
420
+ service: "calendar",
421
+ body: data,
422
+ resource: "calendarEvents/sync"
423
+ });
424
+ });
425
+ });
426
+
427
+ describe("#updateCalendarEvent()", () => {
428
+ it("should update a calendar event", async () => {
429
+ const id = "abcdabcd-abcd-abcd-abcd-00000000";
430
+ const data = {
431
+ encryptionKeyUrl: "kms://kms-us-int.wbx2.com/keys/d1c14fc5-be10-4389-ae83-9521f92fbfd3",
432
+ notes: "This is Agenda",
433
+ subject: "My Meeting 1",
434
+ webexOptions: "{}"
435
+ };
436
+ const query = {};
437
+
438
+ webex.request = sinon.stub().resolves({
439
+ body: {
440
+ meetingId: "abcdabcd-abcd-abcd-abcd-00000000",
441
+ globalMeetingId: "xxxx-xxxx-xxxx-xxxx"
442
+ }
443
+ });
444
+
445
+ const res = await webex.internal.calendar.updateCalendarEvent(id, data);
446
+
447
+ expect(res.body.meetingId).to.equal("abcdabcd-abcd-abcd-abcd-00000000");
448
+ expect(res.body.globalMeetingId).to.equal("xxxx-xxxx-xxxx-xxxx");
449
+ assert.calledWith(webex.request, {
450
+ method: "PATCH",
451
+ service: "calendar",
452
+ body: data,
453
+ resource: `calendarEvents/${base64.encode(id)}/sync`,
454
+ qs: query
455
+ });
456
+ });
457
+ });
458
+
459
+ describe("#deleteCalendarEvent()", () => {
460
+ it("should delete a calendar event", async () => {
461
+ const id = "abcdabcd-abcd-abcd-abcd-00000000";
462
+
463
+ webex.request = sinon.stub().resolves({
464
+ body: {}
465
+ });
466
+
467
+ await webex.internal.calendar.deleteCalendarEvent(id);
468
+
469
+ assert.calledWith(webex.request, {
470
+ method: "DELETE",
471
+ service: "calendar",
472
+ resource: `calendarEvents/${base64.encode(id)}/sync`
348
473
  });
349
474
  });
350
475
  });