@webex/plugin-meetings 3.12.0-next.86 → 3.12.0-next.88

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/package.json CHANGED
@@ -44,7 +44,7 @@
44
44
  "@webex/event-dictionary-ts": "^1.0.2181",
45
45
  "@webex/jest-config-legacy": "0.0.0",
46
46
  "@webex/legacy-tools": "0.0.0",
47
- "@webex/plugin-rooms": "3.12.0-next.28",
47
+ "@webex/plugin-rooms": "3.12.0-next.29",
48
48
  "@webex/test-helper-chai": "3.12.0-next.5",
49
49
  "@webex/test-helper-mocha": "3.12.0-next.5",
50
50
  "@webex/test-helper-mock-webex": "3.12.0-next.5",
@@ -63,20 +63,20 @@
63
63
  "dependencies": {
64
64
  "@webex/common": "3.12.0-next.5",
65
65
  "@webex/internal-media-core": "2.26.1",
66
- "@webex/internal-plugin-conversation": "3.12.0-next.28",
67
- "@webex/internal-plugin-device": "3.12.0-next.26",
68
- "@webex/internal-plugin-llm": "3.12.0-next.30",
69
- "@webex/internal-plugin-mercury": "3.12.0-next.27",
70
- "@webex/internal-plugin-metrics": "3.12.0-next.26",
71
- "@webex/internal-plugin-support": "3.12.0-next.28",
72
- "@webex/internal-plugin-user": "3.12.0-next.27",
73
- "@webex/internal-plugin-voicea": "3.12.0-next.30",
66
+ "@webex/internal-plugin-conversation": "3.12.0-next.29",
67
+ "@webex/internal-plugin-device": "3.12.0-next.27",
68
+ "@webex/internal-plugin-llm": "3.12.0-next.31",
69
+ "@webex/internal-plugin-mercury": "3.12.0-next.28",
70
+ "@webex/internal-plugin-metrics": "3.12.0-next.27",
71
+ "@webex/internal-plugin-support": "3.12.0-next.29",
72
+ "@webex/internal-plugin-user": "3.12.0-next.28",
73
+ "@webex/internal-plugin-voicea": "3.12.0-next.31",
74
74
  "@webex/media-helpers": "3.12.0-next.9",
75
- "@webex/plugin-people": "3.12.0-next.27",
76
- "@webex/plugin-rooms": "3.12.0-next.28",
75
+ "@webex/plugin-people": "3.12.0-next.28",
76
+ "@webex/plugin-rooms": "3.12.0-next.29",
77
77
  "@webex/ts-sdp": "^1.8.1",
78
78
  "@webex/web-capabilities": "^1.10.0",
79
- "@webex/webex-core": "3.12.0-next.26",
79
+ "@webex/webex-core": "3.12.0-next.27",
80
80
  "ampersand-collection": "^2.0.2",
81
81
  "bowser": "^2.11.0",
82
82
  "btoa": "^1.2.1",
@@ -94,5 +94,5 @@
94
94
  "//": [
95
95
  "TODO: upgrade jwt-decode when moving to node 18"
96
96
  ],
97
- "version": "3.12.0-next.86"
97
+ "version": "3.12.0-next.88"
98
98
  }
@@ -4202,7 +4202,12 @@ export default class Meeting extends StatelessWebexPlugin {
4202
4202
  }
4203
4203
 
4204
4204
  /**
4205
- * Transfer the moderator role to another eligible member
4205
+ * Transfer the moderator role to another eligible member.
4206
+ *
4207
+ * If the target member is currently joined in the current session, the request is
4208
+ * sent against the current locus. Otherwise, we search the breakout sessions and,
4209
+ * when the member is found in one, we send the request against that breakout
4210
+ * session's locus url instead.
4206
4211
  * @param {String} memberId
4207
4212
  * @param {Boolean} moderator
4208
4213
  * @returns {Promise} see #members.transferHostToMember
@@ -4210,7 +4215,21 @@ export default class Meeting extends StatelessWebexPlugin {
4210
4215
  * @memberof Meeting
4211
4216
  */
4212
4217
  public transfer(memberId: string, moderator = true) {
4213
- return this.members.transferHostToMember(memberId, moderator);
4218
+ const targetMember = this.members.membersCollection.get(memberId);
4219
+
4220
+ if (targetMember?.isInMeeting) {
4221
+ return this.members.transferHostToMember(memberId, moderator);
4222
+ }
4223
+
4224
+ const breakout = this.breakouts?.breakouts?.models?.find((bo: any) => {
4225
+ const breakoutMember = bo.members?.membersCollection?.get(memberId);
4226
+
4227
+ return breakoutMember?.isInMeeting;
4228
+ });
4229
+
4230
+ const breakoutLocusUrl = breakout?.breakoutRosterLocus?.url;
4231
+
4232
+ return this.members.transferHostToMember(memberId, moderator, breakoutLocusUrl);
4214
4233
  }
4215
4234
 
4216
4235
  /**
@@ -9168,7 +9187,6 @@ export default class Meeting extends StatelessWebexPlugin {
9168
9187
  options: {meetingId: this.id},
9169
9188
  });
9170
9189
  LoggerProxy.logger.log('Meeting:index#leave --> Leaving a meeting');
9171
-
9172
9190
  this.stopListeningForMeetingEvents();
9173
9191
 
9174
9192
  return MeetingUtil.leaveMeeting(this, options)
@@ -920,9 +920,20 @@ const MeetingUtil = {
920
920
 
921
921
  const options = buildLocusDeltaRequestOptions(originalOptions);
922
922
 
923
- return meeting
924
- .request(options)
925
- .then((response) => MeetingUtil.updateLocusFromApiResponse(meeting, response));
923
+ return meeting.request(options).then((response) => {
924
+ const requestUri = options?.uri;
925
+ const meetingLocusUrl = meeting.locusUrl;
926
+
927
+ if (!requestUri || !meetingLocusUrl || !requestUri.startsWith(meetingLocusUrl)) {
928
+ LoggerProxy.logger.info(
929
+ `Meeting:util#generateLocusDeltaRequest --> skipping updateLocusFromApiResponse, request uri "${requestUri}" does not match meeting locusUrl "${meetingLocusUrl}"`
930
+ );
931
+
932
+ return response;
933
+ }
934
+
935
+ return MeetingUtil.updateLocusFromApiResponse(meeting, response);
936
+ });
926
937
  };
927
938
 
928
939
  return locusDeltaRequest;
@@ -1136,12 +1136,16 @@ export default class Members extends StatelessWebexPlugin {
1136
1136
  * Transfers the host to another member
1137
1137
  * @param {String} memberId
1138
1138
  * @param {boolean} [moderator] default true
1139
+ * @param {String} [breakoutLocusUrl] when provided, the request is sent against this locus url
1140
+ * (i.e. the breakout session's locus) instead of the main session's locus url
1139
1141
  * @returns {Promise}
1140
1142
  * @public
1141
1143
  * @memberof Members
1142
1144
  */
1143
- public transferHostToMember(memberId: string, moderator = true) {
1144
- if (!this.locusUrl) {
1145
+ public transferHostToMember(memberId: string, moderator = true, breakoutLocusUrl = '') {
1146
+ const locusUrl = breakoutLocusUrl || this.locusUrl;
1147
+
1148
+ if (!locusUrl) {
1145
1149
  return Promise.reject(
1146
1150
  new ParameterError(
1147
1151
  'The associated locus url for this meetings members object must be defined.'
@@ -1153,11 +1157,14 @@ export default class Members extends StatelessWebexPlugin {
1153
1157
  new ParameterError('The member id must be defined to transfer host to the member.')
1154
1158
  );
1155
1159
  }
1156
- const options = MembersUtil.generateTransferHostMemberOptions(
1160
+ const options: Record<string, any> = MembersUtil.generateTransferHostMemberOptions(
1157
1161
  memberId,
1158
1162
  moderator,
1159
- this.locusUrl
1163
+ locusUrl
1160
1164
  );
1165
+ if (breakoutLocusUrl && breakoutLocusUrl !== this.locusUrl) {
1166
+ options.authorizingLocusUrl = this.locusUrl;
1167
+ }
1161
1168
 
1162
1169
  return this.membersRequest.transferHostToMember(options);
1163
1170
  }
@@ -13,7 +13,7 @@ import {
13
13
  ALIAS,
14
14
  } from '../constants';
15
15
 
16
- import {RoleAssignmentOptions, RoleAssignmentRequest, ServerRoleShape} from './types';
16
+ import {RoleAssignmentOptions, RoleAssignmentRequest, ServerRoleShape, ServerRoles} from './types';
17
17
  import {Invitee} from '../meeting/type';
18
18
 
19
19
  const MembersUtil = {
@@ -330,12 +330,20 @@ const MembersUtil = {
330
330
  };
331
331
  },
332
332
 
333
- getTransferHostToMemberRequestParams: (options) => {
334
- const body = {
333
+ getTransferHostToMemberRequestParams: (options: any) => {
334
+ const body: Record<string, any> = {
335
335
  role: {
336
- moderator: options.moderator,
336
+ roles: [
337
+ {
338
+ hasRole: true,
339
+ type: ServerRoles.Moderator,
340
+ },
341
+ ],
337
342
  },
338
343
  };
344
+ if (options.authorizingLocusUrl) {
345
+ body.authorizingLocusUrl = options.authorizingLocusUrl;
346
+ }
339
347
  const uri = `${options.locusUrl}/${PARTICIPANT}/${options.memberId}/${CONTROLS}`;
340
348
 
341
349
  return {
@@ -883,6 +883,67 @@ describe('plugin-meetings', () => {
883
883
  assert.deepEqual(args, [[uuid1], locusUrls]);
884
884
  });
885
885
  });
886
+ describe('#transfer', () => {
887
+ it('should have #transfer', () => {
888
+ assert.exists(meeting.transfer);
889
+ });
890
+
891
+ it('should transfer host using the current locus when the target member is in the current session', async () => {
892
+ sinon.stub(meeting.members.membersCollection, 'get').withArgs(uuid2).returns({isInMeeting: true});
893
+ meeting.members.transferHostToMember = sinon.stub().resolves(test1);
894
+
895
+ await meeting.transfer(uuid2, false);
896
+
897
+ assert.calledOnceWithExactly(meeting.members.transferHostToMember, uuid2, false);
898
+ });
899
+
900
+ it('should transfer host using breakout locus when the target member is joined in a breakout session', async () => {
901
+ sinon.stub(meeting.members.membersCollection, 'get').withArgs(uuid2).returns({isInMeeting: false});
902
+ meeting.members.transferHostToMember = sinon.stub().resolves(test1);
903
+ meeting.breakouts.breakouts = {
904
+ models: [
905
+ {
906
+ members: {
907
+ membersCollection: {
908
+ get: sinon.stub().withArgs(uuid2).returns({isInMeeting: true}),
909
+ },
910
+ },
911
+ breakoutRosterLocus: {url: 'https://example.com/breakout-locus'},
912
+ },
913
+ ],
914
+ };
915
+
916
+ await meeting.transfer(uuid2, false);
917
+
918
+ assert.calledOnceWithExactly(
919
+ meeting.members.transferHostToMember,
920
+ uuid2,
921
+ false,
922
+ 'https://example.com/breakout-locus'
923
+ );
924
+ });
925
+
926
+ it('should not use breakout locus when the breakout member is not in meeting', async () => {
927
+ sinon.stub(meeting.members.membersCollection, 'get').withArgs(uuid2).returns({isInMeeting: false});
928
+ meeting.members.transferHostToMember = sinon.stub().resolves(test1);
929
+ meeting.breakouts.breakouts = {
930
+ models: [
931
+ {
932
+ members: {
933
+ membersCollection: {
934
+ get: sinon.stub().withArgs(uuid2).returns({isInMeeting: false}),
935
+ },
936
+ },
937
+ breakoutRosterLocus: {url: 'https://example.com/breakout-locus'},
938
+ },
939
+ ],
940
+ };
941
+
942
+ await meeting.transfer(uuid2, false);
943
+
944
+ assert.calledOnceWithExactly(meeting.members.transferHostToMember, uuid2, false, undefined);
945
+ });
946
+ });
886
947
  describe('#getMembers', () => {
887
948
  it('should have #getMembers', () => {
888
949
  assert.exists(meeting.getMembers);
@@ -319,6 +319,7 @@ describe('plugin-meetings', () => {
319
319
  const addSequenceSpy = sinon.spy(MeetingUtil, 'addSequence');
320
320
 
321
321
  const meeting = {
322
+ locusUrl: 'https://locus.example.com/loci/abc',
322
323
  request: sinon.stub().returns(Promise.resolve('result')),
323
324
  };
324
325
 
@@ -326,6 +327,7 @@ describe('plugin-meetings', () => {
326
327
 
327
328
  const options = {
328
329
  some: 'option',
330
+ uri: 'https://locus.example.com/loci/abc',
329
331
  body: {},
330
332
  };
331
333
 
@@ -339,7 +341,7 @@ describe('plugin-meetings', () => {
339
341
  addSequenceSpy.resetHistory();
340
342
 
341
343
  // body missing from options
342
- result = await locusDeltaRequest({});
344
+ result = await locusDeltaRequest({uri: 'https://locus.example.com/loci/abc'});
343
345
  assert.equal(result, 'result');
344
346
  assert.calledOnceWithExactly(updateLocusFromApiResponseSpy, meeting, 'result');
345
347
  assert.calledOnceWithExactly(addSequenceSpy, meeting, options.body);
@@ -353,6 +355,78 @@ describe('plugin-meetings', () => {
353
355
  WeakRef.prototype.deref.restore();
354
356
  });
355
357
 
358
+ it('calls updateLocusFromApiResponse when request uri starts with the meeting locusUrl', async () => {
359
+ const updateLocusFromApiResponseSpy = sinon.spy(MeetingUtil, 'updateLocusFromApiResponse');
360
+
361
+ const meeting = {
362
+ locusUrl: 'https://locus.example.com/loci/abc',
363
+ request: sinon.stub().returns(Promise.resolve('result')),
364
+ };
365
+
366
+ const locusDeltaRequest = MeetingUtil.generateLocusDeltaRequest(meeting);
367
+
368
+ const result = await locusDeltaRequest({
369
+ uri: 'https://locus.example.com/loci/abc/participant/123',
370
+ body: {},
371
+ });
372
+
373
+ assert.equal(result, 'result');
374
+ assert.calledOnceWithExactly(updateLocusFromApiResponseSpy, meeting, 'result');
375
+ });
376
+
377
+ it('does not call updateLocusFromApiResponse when request uri does not match the meeting locusUrl', async () => {
378
+ const updateLocusFromApiResponseSpy = sinon.spy(MeetingUtil, 'updateLocusFromApiResponse');
379
+
380
+ const meeting = {
381
+ locusUrl: 'https://locus.example.com/loci/abc',
382
+ request: sinon.stub().returns(Promise.resolve('result')),
383
+ };
384
+
385
+ const locusDeltaRequest = MeetingUtil.generateLocusDeltaRequest(meeting);
386
+
387
+ const result = await locusDeltaRequest({
388
+ uri: 'https://locus.example.com/loci/different',
389
+ body: {},
390
+ });
391
+
392
+ assert.equal(result, 'result');
393
+ assert.notCalled(updateLocusFromApiResponseSpy);
394
+ });
395
+
396
+ it('does not call updateLocusFromApiResponse when request uri is missing', async () => {
397
+ const updateLocusFromApiResponseSpy = sinon.spy(MeetingUtil, 'updateLocusFromApiResponse');
398
+
399
+ const meeting = {
400
+ locusUrl: 'https://locus.example.com/loci/abc',
401
+ request: sinon.stub().returns(Promise.resolve('result')),
402
+ };
403
+
404
+ const locusDeltaRequest = MeetingUtil.generateLocusDeltaRequest(meeting);
405
+
406
+ const result = await locusDeltaRequest({body: {}});
407
+
408
+ assert.equal(result, 'result');
409
+ assert.notCalled(updateLocusFromApiResponseSpy);
410
+ });
411
+
412
+ it('does not call updateLocusFromApiResponse when meeting locusUrl is missing', async () => {
413
+ const updateLocusFromApiResponseSpy = sinon.spy(MeetingUtil, 'updateLocusFromApiResponse');
414
+
415
+ const meeting = {
416
+ request: sinon.stub().returns(Promise.resolve('result')),
417
+ };
418
+
419
+ const locusDeltaRequest = MeetingUtil.generateLocusDeltaRequest(meeting);
420
+
421
+ const result = await locusDeltaRequest({
422
+ uri: 'https://locus.example.com/loci/abc',
423
+ body: {},
424
+ });
425
+
426
+ assert.equal(result, 'result');
427
+ assert.notCalled(updateLocusFromApiResponseSpy);
428
+ });
429
+
356
430
  it('calls generateBuildLocusDeltaRequestOptions as expected', () => {
357
431
  const generateBuildLocusDeltaRequestOptionsSpy = sinon.spy(
358
432
  MeetingUtil,
@@ -303,6 +303,67 @@ describe('plugin-meetings', () => {
303
303
  });
304
304
  });
305
305
 
306
+ describe('#transferHostToMember', () => {
307
+ it('should reject if no locus url is set and no breakoutLocusUrl is provided', async () => {
308
+ const members = createMembers({url: false});
309
+
310
+ await assert.isRejected(members.transferHostToMember('bob'));
311
+ });
312
+
313
+ it('should reject if no memberId is provided', async () => {
314
+ const members = createMembers({url: url1});
315
+
316
+ await assert.isRejected(members.transferHostToMember());
317
+ });
318
+
319
+ it('uses the main locus url when no breakoutLocusUrl is provided', async () => {
320
+ sandbox.spy(MembersUtil, 'generateTransferHostMemberOptions');
321
+
322
+ const members = createMembers({url: url1});
323
+ const {membersRequest} = members;
324
+ sandbox.spy(membersRequest, 'transferHostToMember');
325
+
326
+ await members.transferHostToMember('bob', true);
327
+
328
+ assert.calledOnceWithExactly(
329
+ MembersUtil.generateTransferHostMemberOptions,
330
+ 'bob',
331
+ true,
332
+ url1
333
+ );
334
+ assert.calledOnceWithExactly(membersRequest.transferHostToMember, {
335
+ memberId: 'bob',
336
+ moderator: true,
337
+ locusUrl: url1,
338
+ });
339
+ });
340
+
341
+ it('uses the breakoutLocusUrl when provided', async () => {
342
+ sandbox.spy(MembersUtil, 'generateTransferHostMemberOptions');
343
+
344
+ const members = createMembers({url: url1});
345
+ const {membersRequest} = members;
346
+ sandbox.spy(membersRequest, 'transferHostToMember');
347
+
348
+ const breakoutLocusUrl = 'https://example.com/breakout-locus';
349
+
350
+ await members.transferHostToMember('bob', true, breakoutLocusUrl);
351
+
352
+ assert.calledOnceWithExactly(
353
+ MembersUtil.generateTransferHostMemberOptions,
354
+ 'bob',
355
+ true,
356
+ breakoutLocusUrl
357
+ );
358
+ assert.calledOnceWithExactly(membersRequest.transferHostToMember, {
359
+ memberId: 'bob',
360
+ moderator: true,
361
+ locusUrl: breakoutLocusUrl,
362
+ authorizingLocusUrl: url1,
363
+ });
364
+ });
365
+ });
366
+
306
367
  describe('#clearMembers', () => {
307
368
  it('should send clear event if clear members', () => {
308
369
  const members = createMembers({url: url1});
@@ -194,7 +194,43 @@ describe('plugin-meetings', () => {
194
194
  checkRequest({
195
195
  method: 'PATCH',
196
196
  uri: 'https://example.com/12345/participant/member1/controls',
197
- body: {role: {moderator: true}},
197
+ body: {
198
+ role: {
199
+ roles: [
200
+ {
201
+ hasRole: true,
202
+ type: 'MODERATOR',
203
+ },
204
+ ],
205
+ },
206
+ },
207
+ });
208
+ });
209
+
210
+ it('sends a request with authorizingLocusUrl when provided', async () => {
211
+ const options = {
212
+ locusUrl: url1,
213
+ memberId: 'member1',
214
+ moderator: true,
215
+ authorizingLocusUrl: 'https://example.com/breakout-locus',
216
+ };
217
+
218
+ await membersRequest.transferHostToMember(options);
219
+
220
+ checkRequest({
221
+ method: 'PATCH',
222
+ uri: 'https://example.com/12345/participant/member1/controls',
223
+ body: {
224
+ role: {
225
+ roles: [
226
+ {
227
+ hasRole: true,
228
+ type: 'MODERATOR',
229
+ },
230
+ ],
231
+ },
232
+ authorizingLocusUrl: 'https://example.com/breakout-locus',
233
+ },
198
234
  });
199
235
  });
200
236
  });
@@ -226,6 +226,69 @@ describe('plugin-meetings', () => {
226
226
  });
227
227
  });
228
228
 
229
+ describe('#generateTransferHostMemberOptions', () => {
230
+ it('returns the correct options', () => {
231
+ assert.deepEqual(
232
+ MembersUtil.generateTransferHostMemberOptions('member1', true, 'locusUrl'),
233
+ {
234
+ memberId: 'member1',
235
+ moderator: true,
236
+ locusUrl: 'locusUrl',
237
+ }
238
+ );
239
+ });
240
+ });
241
+
242
+ describe('#getTransferHostToMemberRequestParams', () => {
243
+ it('returns the correct request params without authorizingLocusUrl', () => {
244
+ const options = {
245
+ locusUrl: 'locusUrl',
246
+ memberId: 'member1',
247
+ moderator: true,
248
+ };
249
+
250
+ assert.deepEqual(MembersUtil.getTransferHostToMemberRequestParams(options), {
251
+ method: HTTP_VERBS.PATCH,
252
+ uri: 'locusUrl/participant/member1/controls',
253
+ body: {
254
+ role: {
255
+ roles: [
256
+ {
257
+ hasRole: true,
258
+ type: 'MODERATOR',
259
+ },
260
+ ],
261
+ },
262
+ },
263
+ });
264
+ });
265
+
266
+ it('returns the correct request params with authorizingLocusUrl', () => {
267
+ const options = {
268
+ locusUrl: 'mainLocusUrl',
269
+ memberId: 'member1',
270
+ moderator: true,
271
+ authorizingLocusUrl: 'breakoutLocusUrl',
272
+ };
273
+
274
+ assert.deepEqual(MembersUtil.getTransferHostToMemberRequestParams(options), {
275
+ method: HTTP_VERBS.PATCH,
276
+ uri: 'mainLocusUrl/participant/member1/controls',
277
+ body: {
278
+ role: {
279
+ roles: [
280
+ {
281
+ hasRole: true,
282
+ type: 'MODERATOR',
283
+ },
284
+ ],
285
+ },
286
+ authorizingLocusUrl: 'breakoutLocusUrl',
287
+ },
288
+ });
289
+ });
290
+ });
291
+
229
292
  describe('#generateMuteMemberOptions', () => {
230
293
  const testOptions = (isAudio) => {
231
294
  const memberId = 'bob';