@webex/plugin-meetings 3.12.0-next.5 → 3.12.0-next.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/AGENTS.md +9 -0
  2. package/dist/aiEnableRequest/index.js +15 -2
  3. package/dist/aiEnableRequest/index.js.map +1 -1
  4. package/dist/breakouts/breakout.js +6 -2
  5. package/dist/breakouts/breakout.js.map +1 -1
  6. package/dist/breakouts/index.js +1 -1
  7. package/dist/config.js +1 -0
  8. package/dist/config.js.map +1 -1
  9. package/dist/constants.js +6 -3
  10. package/dist/constants.js.map +1 -1
  11. package/dist/controls-options-manager/constants.js +11 -1
  12. package/dist/controls-options-manager/constants.js.map +1 -1
  13. package/dist/controls-options-manager/index.js +38 -24
  14. package/dist/controls-options-manager/index.js.map +1 -1
  15. package/dist/controls-options-manager/util.js +91 -0
  16. package/dist/controls-options-manager/util.js.map +1 -1
  17. package/dist/hashTree/constants.js +10 -1
  18. package/dist/hashTree/constants.js.map +1 -1
  19. package/dist/hashTree/hashTreeParser.js +593 -358
  20. package/dist/hashTree/hashTreeParser.js.map +1 -1
  21. package/dist/hashTree/utils.js +22 -0
  22. package/dist/hashTree/utils.js.map +1 -1
  23. package/dist/index.js +7 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/interceptors/locusRetry.js +23 -8
  26. package/dist/interceptors/locusRetry.js.map +1 -1
  27. package/dist/interpretation/index.js +10 -1
  28. package/dist/interpretation/index.js.map +1 -1
  29. package/dist/interpretation/siLanguage.js +1 -1
  30. package/dist/locus-info/controlsUtils.js +4 -1
  31. package/dist/locus-info/controlsUtils.js.map +1 -1
  32. package/dist/locus-info/index.js +277 -86
  33. package/dist/locus-info/index.js.map +1 -1
  34. package/dist/locus-info/types.js +16 -0
  35. package/dist/locus-info/types.js.map +1 -1
  36. package/dist/media/properties.js +1 -0
  37. package/dist/media/properties.js.map +1 -1
  38. package/dist/meeting/in-meeting-actions.js +3 -1
  39. package/dist/meeting/in-meeting-actions.js.map +1 -1
  40. package/dist/meeting/index.js +842 -521
  41. package/dist/meeting/index.js.map +1 -1
  42. package/dist/meeting/util.js +19 -2
  43. package/dist/meeting/util.js.map +1 -1
  44. package/dist/meetings/index.js +199 -77
  45. package/dist/meetings/index.js.map +1 -1
  46. package/dist/meetings/meetings.types.js +6 -1
  47. package/dist/meetings/meetings.types.js.map +1 -1
  48. package/dist/meetings/request.js +39 -0
  49. package/dist/meetings/request.js.map +1 -1
  50. package/dist/meetings/util.js +67 -5
  51. package/dist/meetings/util.js.map +1 -1
  52. package/dist/member/index.js +10 -0
  53. package/dist/member/index.js.map +1 -1
  54. package/dist/member/types.js.map +1 -1
  55. package/dist/member/util.js +3 -0
  56. package/dist/member/util.js.map +1 -1
  57. package/dist/metrics/constants.js +2 -1
  58. package/dist/metrics/constants.js.map +1 -1
  59. package/dist/recording-controller/index.js +1 -3
  60. package/dist/recording-controller/index.js.map +1 -1
  61. package/dist/types/config.d.ts +1 -0
  62. package/dist/types/constants.d.ts +2 -0
  63. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  64. package/dist/types/controls-options-manager/index.d.ts +10 -0
  65. package/dist/types/hashTree/constants.d.ts +1 -0
  66. package/dist/types/hashTree/hashTreeParser.d.ts +61 -15
  67. package/dist/types/hashTree/utils.d.ts +11 -0
  68. package/dist/types/index.d.ts +2 -0
  69. package/dist/types/interceptors/locusRetry.d.ts +4 -4
  70. package/dist/types/locus-info/index.d.ts +46 -6
  71. package/dist/types/locus-info/types.d.ts +17 -1
  72. package/dist/types/media/properties.d.ts +1 -0
  73. package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
  74. package/dist/types/meeting/index.d.ts +70 -1
  75. package/dist/types/meeting/util.d.ts +8 -0
  76. package/dist/types/meetings/index.d.ts +18 -1
  77. package/dist/types/meetings/meetings.types.d.ts +15 -0
  78. package/dist/types/meetings/request.d.ts +14 -0
  79. package/dist/types/member/index.d.ts +1 -0
  80. package/dist/types/member/types.d.ts +1 -0
  81. package/dist/types/member/util.d.ts +1 -0
  82. package/dist/types/metrics/constants.d.ts +1 -0
  83. package/dist/webinar/index.js +361 -235
  84. package/dist/webinar/index.js.map +1 -1
  85. package/package.json +22 -22
  86. package/src/aiEnableRequest/index.ts +16 -0
  87. package/src/breakouts/breakout.ts +2 -1
  88. package/src/config.ts +1 -0
  89. package/src/constants.ts +5 -1
  90. package/src/controls-options-manager/constants.ts +14 -1
  91. package/src/controls-options-manager/index.ts +47 -24
  92. package/src/controls-options-manager/util.ts +81 -1
  93. package/src/hashTree/constants.ts +9 -0
  94. package/src/hashTree/hashTreeParser.ts +306 -160
  95. package/src/hashTree/utils.ts +17 -0
  96. package/src/index.ts +5 -0
  97. package/src/interceptors/locusRetry.ts +25 -4
  98. package/src/interpretation/index.ts +25 -8
  99. package/src/locus-info/controlsUtils.ts +3 -1
  100. package/src/locus-info/index.ts +276 -93
  101. package/src/locus-info/types.ts +19 -1
  102. package/src/media/properties.ts +1 -0
  103. package/src/meeting/in-meeting-actions.ts +4 -0
  104. package/src/meeting/index.ts +315 -26
  105. package/src/meeting/util.ts +20 -2
  106. package/src/meetings/index.ts +104 -43
  107. package/src/meetings/meetings.types.ts +19 -0
  108. package/src/meetings/request.ts +43 -0
  109. package/src/meetings/util.ts +80 -1
  110. package/src/member/index.ts +10 -0
  111. package/src/member/types.ts +1 -0
  112. package/src/member/util.ts +3 -0
  113. package/src/metrics/constants.ts +1 -0
  114. package/src/recording-controller/index.ts +1 -2
  115. package/src/webinar/index.ts +162 -21
  116. package/test/unit/spec/aiEnableRequest/index.ts +86 -0
  117. package/test/unit/spec/breakouts/breakout.ts +7 -3
  118. package/test/unit/spec/controls-options-manager/index.js +140 -29
  119. package/test/unit/spec/controls-options-manager/util.js +165 -0
  120. package/test/unit/spec/hashTree/hashTreeParser.ts +1294 -191
  121. package/test/unit/spec/hashTree/utils.ts +88 -1
  122. package/test/unit/spec/interceptors/locusRetry.ts +205 -4
  123. package/test/unit/spec/interpretation/index.ts +26 -4
  124. package/test/unit/spec/locus-info/controlsUtils.js +172 -57
  125. package/test/unit/spec/locus-info/index.js +443 -81
  126. package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
  127. package/test/unit/spec/meeting/index.js +836 -41
  128. package/test/unit/spec/meeting/muteState.js +3 -0
  129. package/test/unit/spec/meeting/utils.js +33 -0
  130. package/test/unit/spec/meetings/index.js +275 -10
  131. package/test/unit/spec/meetings/request.js +141 -0
  132. package/test/unit/spec/meetings/utils.js +161 -0
  133. package/test/unit/spec/member/index.js +7 -0
  134. package/test/unit/spec/member/util.js +24 -0
  135. package/test/unit/spec/recording-controller/index.js +9 -8
  136. package/test/unit/spec/webinar/index.ts +141 -16
@@ -55,10 +55,12 @@ import PasswordError from '../common/errors/password-error';
55
55
  import CaptchaError from '../common/errors/captcha-error';
56
56
  import MeetingCollection from './collection';
57
57
  import {
58
+ FetchSitePreferencesMeViaSiteOptions,
58
59
  MEETING_KEY,
59
60
  INoiseReductionEffect,
60
61
  IVirtualBackgroundEffect,
61
62
  MeetingRegistrationStatus,
63
+ SitePreferencesResponse,
62
64
  } from './meetings.types';
63
65
  import MeetingsUtil from './util';
64
66
  import PermissionError from '../common/errors/permission';
@@ -69,7 +71,9 @@ import JoinForbiddenError from '../common/errors/join-forbidden-error';
69
71
  import {HashTreeMessage} from '../hashTree/hashTreeParser';
70
72
  import {HashTreeObject} from '../hashTree/types';
71
73
  import {isSelf} from '../hashTree/utils';
74
+
72
75
  import {createLocusFromHashTreeMessage, findMeetingForHashTreeMessage} from '../locus-info';
76
+ import {LocusDTO} from '../locus-info/types';
73
77
 
74
78
  let mediaLogger;
75
79
 
@@ -313,7 +317,7 @@ export default class Meetings extends WebexPlugin {
313
317
  const breakoutLocus = this.meetingCollection.getActiveBreakoutLocus(breakoutUrl);
314
318
 
315
319
  const isSelfJoined = newLocus?.self?.state === _JOINED_;
316
- const isSelfMoved = newLocus?.self?.state === _LEFT_ && newLocus?.self?.reason === _MOVED_;
320
+ const isSelfMoved = MeetingsUtil.isSelfMovedOrBreakoutEnded(newLocus);
317
321
  // @ts-ignore
318
322
  const deviceFromNewLocus = MeetingsUtil.getThisDevice(newLocus, this.webex.internal.device.url);
319
323
  const isResourceMovedOnThisDevice =
@@ -390,7 +394,7 @@ export default class Meetings extends WebexPlugin {
390
394
  private isNeedHandleLocusDTO(meeting: any, newLocus: any) {
391
395
  if (newLocus) {
392
396
  const isNewLocusAsBreakout = MeetingsUtil.isBreakoutLocusDTO(newLocus);
393
- const isSelfMoved = newLocus?.self?.state === _LEFT_ && newLocus?.self?.reason === _MOVED_;
397
+ const isSelfMoved = MeetingsUtil.isSelfMovedOrBreakoutEnded(newLocus);
394
398
  const isSelfMovedToLobby =
395
399
  newLocus?.self?.devices[0]?.intent?.reason === _ON_HOLD_LOBBY_ &&
396
400
  newLocus?.self?.devices[0]?.intent?.type === _WAIT_;
@@ -435,14 +439,11 @@ export default class Meetings extends WebexPlugin {
435
439
  if (existingMeeting) {
436
440
  return existingMeeting;
437
441
  }
438
-
439
442
  if (data.eventType === LOCUSEVENT.HASH_TREE_DATA_UPDATED) {
440
443
  // need to check if maybe this event indicates a move to/from breakout
441
444
  const meetingForHashTreeMessage = findMeetingForHashTreeMessage(
442
- data.stateElementsMessage,
443
- this.meetingCollection,
444
- // @ts-ignore
445
- this.webex.internal.device.url
445
+ data?.stateElementsMessage,
446
+ this.meetingCollection
446
447
  );
447
448
 
448
449
  if (meetingForHashTreeMessage) {
@@ -492,7 +493,6 @@ export default class Meetings extends WebexPlugin {
492
493
  */
493
494
  private handleLocusEvent(data: LocusEvent, useRandomDelayForInfo = false) {
494
495
  let meeting = this.getCorrespondingMeetingByLocus(data);
495
-
496
496
  // @ts-ignore
497
497
  if (this.config.experimental.storeLocusHashTreeEventsForDebugging) {
498
498
  storeEventForDebugging('mercury', data);
@@ -586,17 +586,21 @@ export default class Meetings extends WebexPlugin {
586
586
  this.create(data.locus, DESTINATION_TYPE.LOCUS_ID, useRandomDelayForInfo)
587
587
  .then(async (newMeeting) => {
588
588
  meeting = newMeeting;
589
-
590
589
  try {
591
590
  // It's a new meeting so initialize the locus data
592
- await meeting.locusInfo.initialSetup({
593
- trigger:
594
- data.eventType === LOCUSEVENT.SDK_LOCUS_FROM_SYNC_MEETINGS
595
- ? 'get-loci-response'
596
- : 'locus-message',
597
- locus: data.locus,
598
- hashTreeMessage: data.stateElementsMessage,
599
- });
591
+ await meeting.locusInfo.initialSetup(
592
+ {
593
+ trigger:
594
+ data.eventType === LOCUSEVENT.SDK_LOCUS_FROM_SYNC_MEETINGS
595
+ ? 'get-loci-response'
596
+ : 'locus-message',
597
+ locus: data.locus,
598
+ hashTreeMessage: data.stateElementsMessage,
599
+ },
600
+ (locus: LocusDTO) => {
601
+ meeting.finalizeMeetingAfterInitialLocusSetup(locus);
602
+ }
603
+ );
600
604
  } catch (error) {
601
605
  LoggerProxy.logger.warn(
602
606
  `Meetings:index#handleLocusEvent --> Error initializing locus data: ${error.message}`
@@ -1403,6 +1407,31 @@ export default class Meetings extends WebexPlugin {
1403
1407
  return this.personalMeetingRoom;
1404
1408
  }
1405
1409
 
1410
+ /**
1411
+ * Fetches site preferences for the provided Webex site, or the preferred Webex site.
1412
+ * This is used to determine capabilities of the site, such as whether scheduling a webinar is supported.
1413
+ *
1414
+ * @param {object} [options]
1415
+ * @param {string} [options.siteUrl] - Webex site URL. Defaults to preferredWebexSite, for example "cisco.webex.com".
1416
+ * @param {string} [options.siteName] - Site name query override. Defaults to the site name derived from siteUrl, for example "cisco" for "cisco.webex.com".
1417
+ * @param {SitePreferenceSelectOption[]} [options.selectOptions] - Preference sections to fetch. Defaults to 'scheduling'.
1418
+ * @returns {Promise<SitePreferencesResponse>} site preferences response body
1419
+ * @throws {ParameterError}
1420
+ * @public
1421
+ * @memberof Meetings
1422
+ * @example
1423
+ * const preferences = await webex.meetings.fetchSitePreferencesMeViaSite();
1424
+ * const supportScheduleWebinar = preferences?.scheduling?.supportScheduleWebinar;
1425
+ */
1426
+ public fetchSitePreferencesMeViaSite(
1427
+ options: FetchSitePreferencesMeViaSiteOptions = {}
1428
+ ): Promise<SitePreferencesResponse> {
1429
+ return this.request.fetchSitePreferencesMeViaSite({
1430
+ ...options,
1431
+ siteUrl: options.siteUrl || this.preferredWebexSite,
1432
+ });
1433
+ }
1434
+
1406
1435
  /**
1407
1436
  * Returns basic information about a meeting that exists or
1408
1437
  * used to exist in the MeetingCollection
@@ -1765,6 +1794,7 @@ export default class Meetings extends WebexPlugin {
1765
1794
  extraParams: infoExtraParams,
1766
1795
  sendCAevents: !!callStateForMetrics?.correlationId, // if client sends correlation id as argument of public create(), then it means that this meeting creation is part of a pre-join intent from user
1767
1796
  };
1797
+ const shouldDeferMeetingInfoFetch = type === DESTINATION_TYPE.LOCUS_ID && !destination?.info;
1768
1798
 
1769
1799
  if (meetingInfo) {
1770
1800
  meeting.injectMeetingInfo(meetingInfo, meetingInfoOptions, meetingLookupUrl);
@@ -1776,8 +1806,12 @@ export default class Meetings extends WebexPlugin {
1776
1806
  waitingTime
1777
1807
  );
1778
1808
  meeting.parseMeetingInfo(undefined, destination);
1779
- } else {
1809
+ } else if (!shouldDeferMeetingInfoFetch) {
1780
1810
  await meeting.fetchMeetingInfo(meetingInfoOptions);
1811
+ } else {
1812
+ LoggerProxy.logger.info(
1813
+ 'Meetings:index#createMeeting --> defer fetchMeetingInfo for incomplete locus, will do it after locus initialSetup'
1814
+ );
1781
1815
  }
1782
1816
  }
1783
1817
  } catch (err) {
@@ -1811,7 +1845,11 @@ export default class Meetings extends WebexPlugin {
1811
1845
  // For type LOCUS_ID we need to parse the locus object to get the information
1812
1846
  // about the caller and callee
1813
1847
  // Meeting Added event will be created in `handleLocusEvent`
1814
- if (type !== DESTINATION_TYPE.LOCUS_ID) {
1848
+ // Only emit MEETING_ADDED if the meeting still exists in the collection.
1849
+ // If fetchMeetingInfo failed and the meeting was destroyed in the catch block,
1850
+ // skip emitting to prevent orphaned meeting references on the consumer side.
1851
+ // @ts-ignore - getMeetingByType types value as object but accepts strings (same as handleLocusEvent)
1852
+ if (type !== DESTINATION_TYPE.LOCUS_ID && this.getMeetingByType(_ID_, meeting.id)) {
1815
1853
  if (!meeting.sipUri) {
1816
1854
  meeting.setSipUri(destination);
1817
1855
  }
@@ -1886,23 +1924,20 @@ export default class Meetings extends WebexPlugin {
1886
1924
  * @public
1887
1925
  * @memberof Meetings
1888
1926
  */
1889
- public syncMeetings({keepOnlyLocusMeetings = true} = {}): Promise<void> {
1927
+ public async syncMeetings({keepOnlyLocusMeetings = true} = {}): Promise<void> {
1890
1928
  // @ts-ignore
1891
1929
  if (this.webex.credentials.isUnverifiedGuest) {
1892
1930
  LoggerProxy.logger.info(
1893
- 'Meetings:index#syncMeetings --> skipping meeting sync as unverified guest'
1931
+ 'Meetings:index#syncMeetings --> user is unverified guest, skipping calling Locus for meeting sync'
1894
1932
  );
1895
-
1896
- return Promise.resolve();
1897
- }
1898
-
1899
- return this.request
1900
- .getActiveMeetings()
1901
- .then((locusArray) => {
1902
- const activeLocusUrl = [];
1933
+ } else {
1934
+ try {
1935
+ const locusArray = await this.request.getActiveMeetings();
1936
+ const activeLocusUrl: string[] = [];
1903
1937
 
1904
1938
  if (locusArray?.loci && locusArray.loci.length > 0) {
1905
1939
  const lociToUpdate = this.sortLocusArrayToUpdate(locusArray.loci);
1940
+
1906
1941
  lociToUpdate.forEach((locus) => {
1907
1942
  activeLocusUrl.push(locus.url);
1908
1943
  this.handleLocusEvent({
@@ -1920,21 +1955,48 @@ export default class Meetings extends WebexPlugin {
1920
1955
  // (they had a locusUrl previously but are no longer active) in the sync
1921
1956
  for (const meeting of Object.values(meetingsCollection)) {
1922
1957
  // @ts-ignore
1923
- const {locusUrl} = meeting;
1958
+ const {locusUrl, locusInfo} = meeting;
1924
1959
  if ((keepOnlyLocusMeetings || locusUrl) && !activeLocusUrl.includes(locusUrl)) {
1925
- // destroy function also uploads logs
1926
- // @ts-ignore
1927
- this.destroy(meeting, MEETING_REMOVED_REASON.NO_MEETINGS_TO_SYNC);
1960
+ const globalMeetingId = locusInfo?.info?.globalMeetingId;
1961
+
1962
+ if (
1963
+ globalMeetingId &&
1964
+ locusArray?.loci?.some(
1965
+ (locus: LocusDTO) => locus.info?.globalMeetingId === globalMeetingId
1966
+ )
1967
+ ) {
1968
+ // don't destroy the meeting as Locus API still returned some Locus that shares
1969
+ // the same globalMeetingId - that happens for example if a webinar user (who hasn't scheduled it)
1970
+ // is in a breakout and gets moved to a different breakout while we were offline
1971
+ } else {
1972
+ // destroy function also uploads logs
1973
+ // @ts-ignore
1974
+ this.destroy(meeting, MEETING_REMOVED_REASON.NO_MEETINGS_TO_SYNC);
1975
+ }
1928
1976
  }
1929
1977
  }
1930
1978
  }
1931
- })
1932
- .catch((error) => {
1979
+ } catch (error) {
1933
1980
  LoggerProxy.logger.error(
1934
1981
  `Meetings:index#syncMeetings --> failed to sync meetings, ${error}`
1935
1982
  );
1936
- throw new Error(error);
1937
- });
1983
+ throw error;
1984
+ }
1985
+ }
1986
+
1987
+ // Trigger hash tree syncs for all remaining meetings
1988
+ const remainingMeetings = this.meetingCollection.getAll();
1989
+ const syncPromises = [];
1990
+
1991
+ for (const meeting of Object.values(remainingMeetings) as any[]) {
1992
+ if (meeting.locusInfo) {
1993
+ syncPromises.push(meeting.locusInfo.syncAllHashTreeDatasets());
1994
+ }
1995
+ }
1996
+
1997
+ if (syncPromises.length > 0) {
1998
+ await Promise.all(syncPromises);
1999
+ }
1938
2000
  }
1939
2001
 
1940
2002
  /**
@@ -1950,8 +2012,8 @@ export default class Meetings extends WebexPlugin {
1950
2012
  this.breakoutLocusForHandleLater = [];
1951
2013
  const lociToUpdate = [...mainLoci];
1952
2014
  breakoutLoci.forEach((breakoutLocus) => {
1953
- const associateMainLocus = mainLoci.find(
1954
- (mainLocus) => mainLocus.controls?.breakout?.url === breakoutLocus.controls?.breakout?.url
2015
+ const associateMainLocus = mainLoci.find((mainLocus) =>
2016
+ MeetingsUtil.isMainAssociatedWithBreakout(mainLocus, breakoutLocus)
1955
2017
  );
1956
2018
  const existCorrespondingMeeting = this.getCorrespondingMeetingByLocus({
1957
2019
  eventType: LOCUSEVENT.SDK_NO_EVENT,
@@ -1979,7 +2041,7 @@ export default class Meetings extends WebexPlugin {
1979
2041
  * @public
1980
2042
  * @memberof Meetings
1981
2043
  */
1982
- checkHandleBreakoutLocus(newCreatedLocus) {
2044
+ checkHandleBreakoutLocus(newCreatedLocus: any) {
1983
2045
  if (
1984
2046
  !newCreatedLocus ||
1985
2047
  !this.breakoutLocusForHandleLater ||
@@ -1990,9 +2052,8 @@ export default class Meetings extends WebexPlugin {
1990
2052
  if (MeetingsUtil.isBreakoutLocusDTO(newCreatedLocus)) {
1991
2053
  return;
1992
2054
  }
1993
- const existIndex = this.breakoutLocusForHandleLater.findIndex(
1994
- (breakoutLocus) =>
1995
- breakoutLocus.controls?.breakout?.url === newCreatedLocus.controls?.breakout?.url
2055
+ const existIndex = this.breakoutLocusForHandleLater.findIndex((breakoutLocus: any) =>
2056
+ MeetingsUtil.isMainAssociatedWithBreakout(newCreatedLocus, breakoutLocus)
1996
2057
  );
1997
2058
 
1998
2059
  if (existIndex < 0) {
@@ -31,3 +31,22 @@ export type MeetingRegistrationStatus = {
31
31
  mercuryConnect: boolean;
32
32
  checkH264Support: boolean;
33
33
  };
34
+
35
+ export enum SitePreferenceSelectOption {
36
+ SCHEDULING = 'scheduling',
37
+ }
38
+
39
+ export type FetchSitePreferencesMeViaSiteOptions = {
40
+ siteUrl?: string;
41
+ siteName?: string;
42
+ selectOptions?: SitePreferenceSelectOption[];
43
+ };
44
+
45
+ export const DEFAULT_SITE_PREFERENCE_SELECT_OPTIONS = [SitePreferenceSelectOption.SCHEDULING];
46
+
47
+ export type SitePreferencesResponse = {
48
+ scheduling?: {
49
+ supportScheduleWebinar?: boolean;
50
+ webinarWebLink?: string;
51
+ };
52
+ };
@@ -2,7 +2,14 @@
2
2
  import {StatelessWebexPlugin} from '@webex/webex-core';
3
3
 
4
4
  import LoggerProxy from '../common/logs/logger-proxy';
5
+ import ParameterError from '../common/errors/parameter';
5
6
  import {HTTP_VERBS, API, RESOURCE} from '../constants';
7
+ import {
8
+ DEFAULT_SITE_PREFERENCE_SELECT_OPTIONS,
9
+ type FetchSitePreferencesMeViaSiteOptions,
10
+ type SitePreferencesResponse,
11
+ } from './meetings.types';
12
+ import MeetingsUtil from './util';
6
13
 
7
14
  /**
8
15
  * @class MeetingRequest
@@ -45,6 +52,42 @@ export default class MeetingRequest extends StatelessWebexPlugin {
45
52
  return this.webex.internal.services.getMeetingPreferences();
46
53
  }
47
54
 
55
+ /**
56
+ * Fetches site preferences from a given site given a select option and a siteUrl with an optional siteName. If siteName is not provided, it will be derived from the siteUrl. If siteUrl is not provided, it will throw an error. If selectOptions is not provided, it will default to scheduling.
57
+ *
58
+ * @param {object} [options]
59
+ * @param {string} [options.siteUrl] - Webex site URL, for example "cisco.webex.com".
60
+ * @param {string} [options.siteName] - Site name query override. Defaults to the site name derived from options.siteUrl, e.g., "cisco".
61
+ * @param {SitePreferenceSelectOption[]} [options.selectOptions] - Preference sections to fetch. Defaults to 'scheduling'.
62
+ * @returns {Promise<SitePreferencesResponse>} site preferences response body
63
+ * @throws {ParameterError}
64
+ * @public
65
+ * @memberof MeetingRequest
66
+ */
67
+ fetchSitePreferencesMeViaSite(
68
+ options: FetchSitePreferencesMeViaSiteOptions = {}
69
+ ): Promise<SitePreferencesResponse> {
70
+ const {siteUrl, selectOptions = DEFAULT_SITE_PREFERENCE_SELECT_OPTIONS} = options;
71
+
72
+ if (!siteUrl) {
73
+ throw new ParameterError(
74
+ 'No siteUrl available. Call register() before fetching site preferences or provide options.siteUrl.'
75
+ );
76
+ }
77
+
78
+ // @ts-ignore - config comes from registerPlugin
79
+ const multipartSitePrefixList = this.config.meetings.multipartSitePrefixList || [];
80
+ const siteName = options.siteName || MeetingsUtil.getSiteName(siteUrl, multipartSitePrefixList);
81
+
82
+ // @ts-ignore
83
+ return this.request({
84
+ method: HTTP_VERBS.GET,
85
+ uri: `https://${siteUrl}/wbxappapi/v1/users/me/preference?select=${encodeURIComponent(
86
+ selectOptions.join(',')
87
+ )}&siteurl=${encodeURIComponent(siteName)}`,
88
+ }).then((res: any) => res.body);
89
+ }
90
+
48
91
  // locus federation, determines and populate locus if the responseBody has remote URLs to fetch locus details
49
92
 
50
93
  /**
@@ -18,6 +18,7 @@ import Trigger from '../common/events/trigger-proxy';
18
18
  import BEHAVIORAL_METRICS from '../metrics/constants';
19
19
  import Metrics from '../metrics';
20
20
  import {MEETING_KEY} from './meetings.types';
21
+ import {EndMeetingReason, LocusFullState} from '../locus-info/types';
21
22
 
22
23
  /**
23
24
  * Meetings Media Codec Missing Event
@@ -152,6 +153,30 @@ MeetingsUtil.parseDefaultSiteFromMeetingPreferences = (userPreferences) => {
152
153
  return result;
153
154
  };
154
155
 
156
+ MeetingsUtil.getSiteName = (site: string, multipartSitePrefixList: string[] = []) => {
157
+ if (!site) {
158
+ return null;
159
+ }
160
+
161
+ let siteName: string | undefined;
162
+
163
+ multipartSitePrefixList.forEach((multipartSitePrefix) => {
164
+ if (!siteName && site.includes(multipartSitePrefix)) {
165
+ const secondDot = site.indexOf('.', site.indexOf('.') + 1);
166
+
167
+ siteName = site.substring(0, secondDot);
168
+ }
169
+ });
170
+
171
+ if (siteName) {
172
+ return siteName;
173
+ }
174
+
175
+ siteName = site.substring(0, site.indexOf('.'));
176
+
177
+ return siteName;
178
+ };
179
+
155
180
  /**
156
181
  * Will check to see if the H.264 media codec is supported.
157
182
  * @async
@@ -266,6 +291,35 @@ MeetingsUtil.getThisDevice = (newLocus: any, deviceUrl: string) => {
266
291
  return null;
267
292
  };
268
293
 
294
+ /**
295
+ * Checks if the fullState indicates the meeting has fully ended (not just a breakout move).
296
+ * @param {Object} fullState locus fullState data
297
+ * @returns {boolean}
298
+ */
299
+ MeetingsUtil.isWholeMeetingEnded = (fullState: LocusFullState): boolean => {
300
+ return (
301
+ fullState.state === LOCUS.STATE.INACTIVE &&
302
+ fullState.endMeetingReason !== EndMeetingReason.breakoutEnded
303
+ );
304
+ };
305
+
306
+ /**
307
+ * Checks if the self state in a locus indicates a breakout move or breakout end.
308
+ * Returns true when:
309
+ * - self state is LEFT with reason MOVED (regular breakout move), OR
310
+ * - fullState is INACTIVE with endMeetingReason BREAKOUT_ENDED (breakout session ended)
311
+ * @param {Object} locus locus data
312
+ * @returns {boolean}
313
+ */
314
+ MeetingsUtil.isSelfMovedOrBreakoutEnded = (locus: any): boolean => {
315
+ const isSelfLeftMoved = locus?.self?.state === _LEFT_ && locus?.self?.reason === _MOVED_;
316
+ const isBreakoutEnded =
317
+ locus?.fullState?.state === LOCUS.STATE.INACTIVE &&
318
+ locus?.fullState?.endMeetingReason === EndMeetingReason.breakoutEnded;
319
+
320
+ return isSelfLeftMoved || isBreakoutEnded;
321
+ };
322
+
269
323
  /**
270
324
  * get self device joined status from locus data
271
325
  * @param {Object} meeting current meeting data
@@ -294,7 +348,10 @@ MeetingsUtil.joinedOnThisDevice = (meeting: any, newLocus: any, deviceUrl: strin
294
348
  * @private
295
349
  */
296
350
  MeetingsUtil.isBreakoutLocusDTO = (newLocus: any) => {
297
- return newLocus?.controls?.breakout?.sessionType === BREAKOUTS.SESSION_TYPES.BREAKOUT;
351
+ return (
352
+ newLocus?.controls?.breakout?.sessionType === BREAKOUTS.SESSION_TYPES.BREAKOUT ||
353
+ !!newLocus?.info?.isBreakout
354
+ );
298
355
  };
299
356
 
300
357
  /**
@@ -310,4 +367,26 @@ MeetingsUtil.isValidBreakoutLocus = (locus: any) => {
310
367
 
311
368
  return isLocusAsBreakout && !inActiveStatus && selfJoined;
312
369
  };
370
+ /**
371
+ * check if the breakout locus is associated with the main locus by comparing the breakout control url or the replaces info in self device
372
+ * @param {Object} mainLocus main locus data
373
+ * @param {Object} breakoutLocus breakout locus data
374
+ * @returns {boolean}
375
+ * @private
376
+ */
377
+ MeetingsUtil.isMainAssociatedWithBreakout = (mainLocus: any, breakoutLocus: any) => {
378
+ if (
379
+ mainLocus.controls?.breakout?.url &&
380
+ mainLocus.controls?.breakout?.url === breakoutLocus.controls?.breakout?.url
381
+ ) {
382
+ return true;
383
+ }
384
+ const deviceUrl = breakoutLocus?.self?.deviceUrl;
385
+ const replaceInfo = MeetingsUtil.getThisDevice(breakoutLocus, deviceUrl)?.replaces?.[0];
386
+ if (replaceInfo?.locusUrl && replaceInfo.locusUrl === mainLocus.url) {
387
+ return true;
388
+ }
389
+
390
+ return false;
391
+ };
313
392
  export default MeetingsUtil;
@@ -27,6 +27,7 @@ export default class Member {
27
27
  isModerator: any;
28
28
  isModeratorAssignmentProhibited: any;
29
29
  isPresenterAssignmentProhibited: any;
30
+ isAttendeeAssignmentProhibited: any;
30
31
  isMutable: any;
31
32
  isNotAdmitted: any;
32
33
  isRecording: any;
@@ -292,6 +293,14 @@ export default class Member {
292
293
  */
293
294
  this.isPresenterAssignmentProhibited = null;
294
295
 
296
+ /**
297
+ * @instance
298
+ * @type {Boolean}
299
+ * @public
300
+ * @memberof Member
301
+ */
302
+ this.isAttendeeAssignmentProhibited = null;
303
+
295
304
  /**
296
305
  * @instance
297
306
  * @type {Boolean}
@@ -369,6 +378,7 @@ export default class Member {
369
378
  MemberUtil.isModeratorAssignmentProhibited(participant);
370
379
  this.isPresenterAssignmentProhibited =
371
380
  MemberUtil.isPresenterAssignmentProhibited(participant);
381
+ this.isAttendeeAssignmentProhibited = MemberUtil.isAttendeeAssignmentProhibited(participant);
372
382
  this.canApproveAIEnablement = MemberUtil.canApproveAIEnablement(participant);
373
383
  this.processStatus(participant);
374
384
  this.processRoles(participant);
@@ -103,6 +103,7 @@ export interface Participant {
103
103
  moderator: boolean; // Locus docs say this is deprecated and role control should be used instead
104
104
  moderatorAssignmentNotAllowed: boolean;
105
105
  presenterAssignmentNotAllowed: boolean;
106
+ attendeeAssignmentNotAllowed?: boolean;
106
107
  person: ParticipantPerson;
107
108
  resourceGuest: boolean;
108
109
  state: string; // probably one of MEETING_STATE.STATES
@@ -140,6 +140,9 @@ const MemberUtil = {
140
140
  isPresenterAssignmentProhibited: (participant: Participant) =>
141
141
  participant && participant.presenterAssignmentNotAllowed,
142
142
 
143
+ isAttendeeAssignmentProhibited: (participant: Participant) =>
144
+ !!(participant && participant.attendeeAssignmentNotAllowed),
145
+
143
146
  /**
144
147
  * checks to see if the participant id is the same as the passed id
145
148
  * there are multiple ids that can be used
@@ -96,6 +96,7 @@ const BEHAVIORAL_METRICS = {
96
96
  SET_CUSTOM_CODEC_PARAMETERS_USED: 'js_sdk_set_custom_codec_parameters_used',
97
97
  MARK_CUSTOM_CODEC_PARAMETERS_FOR_DELETION_USED:
98
98
  'js_sdk_mark_custom_codec_parameters_for_deletion_used',
99
+ HASH_TREE_SYNC_FAILURE: 'js_sdk_hash_tree_sync_failure',
99
100
  };
100
101
 
101
102
  export {BEHAVIORAL_METRICS as default};
@@ -261,8 +261,7 @@ export default class RecordingController {
261
261
 
262
262
  LoggerProxy.logger.log(`RecordingController:index#recordingControls --> ${record}`);
263
263
 
264
- // @ts-ignore
265
- return this.request.request({
264
+ return this.request.locusDeltaRequest({
266
265
  uri: `${this.locusUrl}/${CONTROLS}`,
267
266
  body: {
268
267
  record,