@webex/internal-plugin-calendar 3.0.0-beta.31 → 3.0.0-beta.310

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
@@ -245,15 +315,26 @@ const Calendar = WebexPlugin.extend({
245
315
  },
246
316
 
247
317
  /**
248
- * Retrieves an array of meeting participants for the meeting id
249
- * @param {String} id
318
+ * Retrieves an array of meeting participants for the meeting participantsUrl
319
+ * @param {String} participantsUrl
250
320
  * @returns {Promise} Resolves with an object of meeting participants
251
321
  */
252
- getParticipants(id) {
322
+ getParticipants(participantsUrl) {
253
323
  return this.request({
254
324
  method: 'GET',
255
- service: 'calendar',
256
- resource: `calendarEvents/${btoa(id)}/participants`,
325
+ uri: participantsUrl,
326
+ });
327
+ },
328
+
329
+ /**
330
+ * get meeting notes using notesUrl from meeting object.
331
+ * @param {String} notesUrl
332
+ * @returns {Promise} Resolves with an object of meeting notes
333
+ */
334
+ getNotesByUrl(notesUrl) {
335
+ return this.request({
336
+ method: 'GET',
337
+ uri: notesUrl,
257
338
  });
258
339
  },
259
340
 
@@ -266,7 +347,7 @@ const Calendar = WebexPlugin.extend({
266
347
  return this.request({
267
348
  method: 'GET',
268
349
  service: 'calendar',
269
- resource: `calendarEvents/${btoa(id)}/notes`,
350
+ resource: `calendarEvents/${base64.encode(id)}/notes`,
270
351
  });
271
352
  },
272
353
 
@@ -292,17 +373,9 @@ const Calendar = WebexPlugin.extend({
292
373
  const promises = [];
293
374
 
294
375
  meetingObjects.forEach((meeting) => {
295
- if (!meeting.encryptedNotes) {
296
- promises.push(
297
- this.getNotes(meeting.id).then((notesResponse) => {
298
- meeting.encryptedNotes = notesResponse.body && notesResponse.body.encryptedNotes;
299
- })
300
- );
301
- }
302
-
303
376
  if (!meeting.encryptedParticipants) {
304
377
  promises.push(
305
- this.getParticipants(meeting.id).then((notesResponse) => {
378
+ this.getParticipants(meeting.participantsUrl).then((notesResponse) => {
306
379
  meeting.encryptedParticipants = notesResponse.body.encryptedParticipants;
307
380
  })
308
381
  );
@@ -312,6 +385,117 @@ const Calendar = WebexPlugin.extend({
312
385
  return Promise.all(promises).then(() => meetingObjects);
313
386
  });
314
387
  },
388
+
389
+ /**
390
+ * Create calendar event
391
+ * @param {object} [data] meeting payload data
392
+ * @param {object} [query] the query parameters for specific usage
393
+ * @returns {Promise} Resolves with creating calendar event response
394
+ * */
395
+ createCalendarEvent(data, query) {
396
+ return EncryptHelper.encryptCalendarEventRequest(this, data).then(() =>
397
+ this.request({
398
+ method: 'POST',
399
+ service: 'calendar',
400
+ body: data,
401
+ resource: 'calendarEvents/sync',
402
+ qs: query || {},
403
+ })
404
+ );
405
+ },
406
+
407
+ /**
408
+ * Update calendar event
409
+ * @param {string} [id] calendar event id
410
+ * @param {object} [data] meeting payload data
411
+ * @param {object} [query] the query parameters for specific usage
412
+ * @returns {Promise} Resolves with updating calendar event response
413
+ * */
414
+ updateCalendarEvent(id, data, query) {
415
+ return EncryptHelper.encryptCalendarEventRequest(this, data).then(() =>
416
+ this.request({
417
+ method: 'PATCH',
418
+ service: 'calendar',
419
+ body: data,
420
+ resource: `calendarEvents/${base64.encode(id)}/sync`,
421
+ qs: query || {},
422
+ })
423
+ );
424
+ },
425
+
426
+ /**
427
+ * Delete calendar event
428
+ * @param {string} [id] calendar event id
429
+ * @param {object} [query] the query parameters for specific usage
430
+ * @returns {Promise} Resolves with deleting calendar event response
431
+ * */
432
+ deleteCalendarEvent(id, query) {
433
+ return this.request({
434
+ method: 'DELETE',
435
+ service: 'calendar',
436
+ resource: `calendarEvents/${base64.encode(id)}/sync`,
437
+ qs: query || {},
438
+ });
439
+ },
440
+
441
+ /**
442
+ * @typedef QuerySchedulerDataOptions
443
+ * @param {string} [siteName] it is site full url, must have. Example: ccctest.dmz.webex.com
444
+ * @param {string} [id] it is seriesOrOccurrenceId. If present, the series/occurrence meeting ID to fetch data for.
445
+ * Example: 040000008200E00074C5B7101A82E008000000004A99F11A0841D9010000000000000000100000009EE499D4A71C1A46B51494C70EC7BFE5
446
+ * @param {string} [clientMeetingId] If present, the client meeting UUID to fetch data for.
447
+ * Example: 7f318aa9-887c-6e94-802a-8dc8e6eb1a0a
448
+ * @param {string} [scheduleTemplateId] it template id.
449
+ * @param {string} [sessionTypeId] it session type id.
450
+ * @param {string} [organizerCIUserId] required in schedule-on-behalf case. It is the organizer's CI UUID.
451
+ * @param {boolean} [usmPreference]
452
+ * @param {string} [webexMeetingId] webex side meeting UUID
453
+ * @param {string} [eventId] event ID.
454
+ * @param {string} [icalUid] icalendar UUID.
455
+ * @param {string} [thirdPartyType] third part type, such as: Microsoft
456
+ */
457
+ /**
458
+ * Get scheduler data from calendar service
459
+ * @param {QuerySchedulerDataOptions} [query] the command parameters for fetching scheduler data.
460
+ * @returns {Promise} Resolves with a decrypted scheduler data
461
+ * */
462
+ getSchedulerData(query) {
463
+ return this.request({
464
+ method: 'GET',
465
+ service: 'calendar',
466
+ resource: 'schedulerData',
467
+ qs: query || {},
468
+ }).then((response) => {
469
+ return DecryptHelper.decryptSchedulerDataResponse(this, response.body).then(() => response);
470
+ });
471
+ },
472
+
473
+ /**
474
+ * Get free busy status from calendar service
475
+ * @param {Object} [data] the command parameters for fetching free busy status.
476
+ * @param {object} [query] the query parameters for specific usage
477
+ * @returns {Promise} Resolves with a decrypted response
478
+ * */
479
+ getFreeBusy(data, query) {
480
+ return EncryptHelper.encryptFreeBusyRequest(this, data)
481
+ .then(() => {
482
+ return this.request({
483
+ method: 'POST',
484
+ service: 'calendar',
485
+ body: data,
486
+ resource: 'freebusy',
487
+ qs: query || {},
488
+ });
489
+ })
490
+ .then(() => {
491
+ return new Promise((resolve, reject) => {
492
+ this.rpcEventRequests[data.requestId] = {resolve, reject};
493
+ });
494
+ })
495
+ .catch((error) => {
496
+ throw error;
497
+ });
498
+ },
315
499
  });
316
500
 
317
501
  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);
@@ -0,0 +1,145 @@
1
+ import sinon from "sinon";
2
+ import { expect } from "@webex/test-helper-chai";
3
+ import DecryptHelper from "@webex/internal-plugin-calendar/src/calendar.decrypt.helper";
4
+
5
+ describe("internal-plugin-calendar", () => {
6
+ describe("DecryptHelper", () => {
7
+ let ctx;
8
+ let encryptedSchedulerData;
9
+ let encryptedFreeBusyData;
10
+
11
+ beforeEach(() => {
12
+ ctx = {
13
+ webex: {
14
+ internal: {
15
+ encryption: {
16
+ decryptText: sinon.stub()
17
+ }
18
+ }
19
+ }
20
+ };
21
+
22
+ encryptedSchedulerData = {
23
+ encryptionKeyUrl: "http://example.com/encryption-key",
24
+ encryptedSubject: "some encrypted subject",
25
+ encryptedLocation: "some encrypted location",
26
+ encryptedNotes: "some encrypted notes",
27
+ encryptedParticipants: [
28
+ {
29
+ encryptedEmailAddress: "some encrypted email address",
30
+ encryptedName: "some encrypted name"
31
+ },
32
+ {
33
+ encryptedEmailAddress: "another encrypted email address",
34
+ encryptedName: "another encrypted name"
35
+ }
36
+ ],
37
+ encryptedScheduleFor: {
38
+ "user1@example.com": {
39
+ encryptedEmail: "some encrypted email address",
40
+ encryptedDisplayName: "some encrypted display name"
41
+ },
42
+ "user2@example.com": {
43
+ encryptedEmail: "another encrypted email address",
44
+ encryptedDisplayName: "another encrypted display name"
45
+ }
46
+ },
47
+ meetingJoinInfo: {
48
+ meetingJoinURI: "some encrypted meeting join URI",
49
+ meetingJoinURL: "some encrypted meeting join URL"
50
+ },
51
+ encryptedOrganizer: {
52
+ encryptedEmailAddress: "some encrypted email address",
53
+ encryptedName: "some encrypted name"
54
+ },
55
+ webexURI: "some encrypted webex URI",
56
+ webexURL: "some encrypted webex URL",
57
+ spaceMeetURL: "some encrypted space meet URL",
58
+ spaceURI: "some encrypted space URI",
59
+ spaceURL: "some encrypted space URL"
60
+ };
61
+
62
+ encryptedFreeBusyData = {
63
+ calendarFreeBusyScheduleResponse: {
64
+ encryptionKeyUrl: "https://encryption.key/url",
65
+ calendarFreeBusyItems: [
66
+ {
67
+ email: "encrypted-email"
68
+ }
69
+ ]
70
+ }
71
+ };
72
+ });
73
+
74
+ afterEach(() => {
75
+ sinon.restore();
76
+ });
77
+
78
+ it("#decryptSchedulerDataResponse - should resolve with undefined if data is undefined", async () => {
79
+ const decryptedData = await DecryptHelper.decryptSchedulerDataResponse(ctx, undefined);
80
+ expect(decryptedData).to.be.undefined;
81
+ });
82
+
83
+ it("#decryptSchedulerDataResponse - should resolve with undefined if data.encryptionKeyUrl is undefined", async () => {
84
+ encryptedSchedulerData.encryptionKeyUrl = undefined;
85
+ const decryptedData = await DecryptHelper.decryptSchedulerDataResponse(ctx, encryptedSchedulerData);
86
+ expect(decryptedData).to.be.undefined;
87
+ });
88
+
89
+ describe("#decryptSchedulerDataResponse - should replace encrypted data with decrypted data in response", () => {
90
+ it("should decrypt scheduler data response correctly", async () => {
91
+ // Stub the decryption method to return the plaintext value.
92
+ const expectedCiphertext = "some decrypted text for testing";
93
+
94
+ ctx.webex.internal.encryption.decryptText.callsFake((key, ciphertext) => Promise.resolve(expectedCiphertext));
95
+
96
+ // Decrypt the data.
97
+ await DecryptHelper.decryptSchedulerDataResponse(ctx, encryptedSchedulerData);
98
+
99
+ // Check that all encrypted properties were decrypted correctly.
100
+ expect(encryptedSchedulerData.encryptedSubject).to.equal(expectedCiphertext);
101
+ expect(encryptedSchedulerData.encryptedLocation).to.equal(expectedCiphertext);
102
+ expect(encryptedSchedulerData.encryptedNotes).to.equal(expectedCiphertext);
103
+ expect(encryptedSchedulerData.encryptedParticipants[0].encryptedEmailAddress).to.equal(expectedCiphertext);
104
+ expect(encryptedSchedulerData.encryptedParticipants[0].encryptedName).to.equal(expectedCiphertext);
105
+ expect(encryptedSchedulerData.encryptedScheduleFor["user1@example.com"].encryptedEmail).to.equal(expectedCiphertext);
106
+ expect(encryptedSchedulerData.encryptedScheduleFor["user1@example.com"].encryptedDisplayName).to.equal(expectedCiphertext);
107
+ expect(encryptedSchedulerData.meetingJoinInfo.meetingJoinURI).to.equal(expectedCiphertext);
108
+ expect(encryptedSchedulerData.meetingJoinInfo.meetingJoinURL).to.equal(expectedCiphertext);
109
+
110
+ expect(encryptedSchedulerData.encryptedOrganizer.encryptedEmailAddress).to.equal(expectedCiphertext);
111
+ expect(encryptedSchedulerData.encryptedOrganizer.encryptedName).to.equal(expectedCiphertext);
112
+ expect(encryptedSchedulerData.webexURI).to.equal(expectedCiphertext);
113
+ expect(encryptedSchedulerData.webexURL).to.equal(expectedCiphertext);
114
+ expect(encryptedSchedulerData.spaceMeetURL).to.equal(expectedCiphertext);
115
+ expect(encryptedSchedulerData.spaceURI).to.equal(expectedCiphertext);
116
+ expect(encryptedSchedulerData.spaceURL).to.equal(expectedCiphertext);
117
+ });
118
+ });
119
+
120
+ it("#decryptFreeBusyResponse - should resolve with undefined if data is undefined", async () => {
121
+ const decryptedData = await DecryptHelper.decryptFreeBusyResponse(ctx, undefined);
122
+ expect(decryptedData).to.be.undefined;
123
+ });
124
+
125
+ it("#decryptFreeBusyResponse - should resolve with undefined if data.calendarFreeBusyScheduleResponse is undefined", async () => {
126
+ const decryptedData = await DecryptHelper.decryptFreeBusyResponse(ctx, {});
127
+ expect(decryptedData).to.be.undefined;
128
+ });
129
+
130
+ it("#decryptFreeBusyResponse - should resolve with undefined if data.calendarFreeBusyScheduleResponse.encryptionKeyUrl is undefined", async () => {
131
+ encryptedFreeBusyData.calendarFreeBusyScheduleResponse.encryptionKeyUrl = undefined;
132
+ const decryptedData = await DecryptHelper.decryptFreeBusyResponse(ctx, encryptedFreeBusyData);
133
+ expect(decryptedData).to.be.undefined;
134
+ });
135
+
136
+ it("#decryptFreeBusyResponse - should replace encrypted email with decrypted email in calendarFreeBusyItems", async () => {
137
+ const decryptTextStub = ctx.webex.internal.encryption.decryptText;
138
+ decryptTextStub.resolves("decrypted-email");
139
+
140
+ await DecryptHelper.decryptFreeBusyResponse(ctx, encryptedFreeBusyData);
141
+
142
+ expect(encryptedFreeBusyData.calendarFreeBusyScheduleResponse.calendarFreeBusyItems[0].email).to.equal("decrypted-email");
143
+ });
144
+ });
145
+ });
@@ -0,0 +1,52 @@
1
+ import sinon from 'sinon';
2
+ import {expect} from '@webex/test-helper-chai';
3
+ import EncryptHelper from '@webex/internal-plugin-calendar/src/calendar.encrypt.helper';
4
+ describe('internal-plugin-calendar', () => {
5
+ describe('encryptHelper', () => {
6
+ let ctx;
7
+ beforeEach(() => {
8
+ ctx = {
9
+ encryptionKeyUrl: 'http://example.com/encryption-key',
10
+ webex: {
11
+ internal: {
12
+ encryption: {
13
+ encryptText: sinon.stub(),
14
+ },
15
+ },
16
+ },
17
+ };
18
+ });
19
+
20
+ afterEach(() => {
21
+ sinon.restore();
22
+ });
23
+
24
+ it('#encryptFreebusyRequestData with emails should ', async () => {
25
+ const freeBusyRequest = {
26
+ start: '20230712T10:20:00Z',
27
+ end: '20230712T20:20:00Z',
28
+ emails: ['test@webex.com'],
29
+ };
30
+ const expectedCiphertext = 'some encrpty data';
31
+ ctx.webex.internal.encryption.encryptText.callsFake((key, ciphertext) =>
32
+ Promise.resolve(expectedCiphertext)
33
+ );
34
+ await EncryptHelper.encryptFreeBusyRequest(ctx, freeBusyRequest);
35
+ expect(freeBusyRequest.emails[0]).to.be.equal(expectedCiphertext);
36
+ });
37
+
38
+ it('#encryptFreebusyRequestData not include emails, but include ids- should b', async () => {
39
+ const freeBusyRequest = {
40
+ start: '20230712T10:20:00Z',
41
+ end: '20230712T20:20:00Z',
42
+ userIds: ['91aee1231'],
43
+ };
44
+ const expectedCiphertext = '91aee1231';
45
+ ctx.webex.internal.encryption.encryptText.callsFake((key, ciphertext) =>
46
+ Promise.resolve(expectedCiphertext)
47
+ );
48
+ await EncryptHelper.encryptFreeBusyRequest(ctx, freeBusyRequest);
49
+ expect(freeBusyRequest.userIds[0]).to.equal(expectedCiphertext);
50
+ });
51
+ });
52
+ });