@webex/plugin-meetings 3.11.0 → 3.12.0-next.2

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 (170) hide show
  1. package/dist/aiEnableRequest/index.js +184 -0
  2. package/dist/aiEnableRequest/index.js.map +1 -0
  3. package/dist/aiEnableRequest/utils.js +36 -0
  4. package/dist/aiEnableRequest/utils.js.map +1 -0
  5. package/dist/annotation/index.js +14 -5
  6. package/dist/annotation/index.js.map +1 -1
  7. package/dist/breakouts/breakout.js +1 -1
  8. package/dist/breakouts/index.js +1 -1
  9. package/dist/config.js +7 -2
  10. package/dist/config.js.map +1 -1
  11. package/dist/constants.js +28 -6
  12. package/dist/constants.js.map +1 -1
  13. package/dist/hashTree/constants.js +3 -1
  14. package/dist/hashTree/constants.js.map +1 -1
  15. package/dist/hashTree/hashTree.js +18 -0
  16. package/dist/hashTree/hashTree.js.map +1 -1
  17. package/dist/hashTree/hashTreeParser.js +850 -410
  18. package/dist/hashTree/hashTreeParser.js.map +1 -1
  19. package/dist/hashTree/types.js +4 -2
  20. package/dist/hashTree/types.js.map +1 -1
  21. package/dist/hashTree/utils.js +10 -0
  22. package/dist/hashTree/utils.js.map +1 -1
  23. package/dist/index.js +11 -2
  24. package/dist/index.js.map +1 -1
  25. package/dist/interceptors/constant.js +12 -0
  26. package/dist/interceptors/constant.js.map +1 -0
  27. package/dist/interceptors/dataChannelAuthToken.js +290 -0
  28. package/dist/interceptors/dataChannelAuthToken.js.map +1 -0
  29. package/dist/interceptors/index.js +7 -0
  30. package/dist/interceptors/index.js.map +1 -1
  31. package/dist/interceptors/utils.js +27 -0
  32. package/dist/interceptors/utils.js.map +1 -0
  33. package/dist/interpretation/index.js +2 -2
  34. package/dist/interpretation/index.js.map +1 -1
  35. package/dist/interpretation/siLanguage.js +1 -1
  36. package/dist/locus-info/controlsUtils.js +5 -3
  37. package/dist/locus-info/controlsUtils.js.map +1 -1
  38. package/dist/locus-info/index.js +522 -131
  39. package/dist/locus-info/index.js.map +1 -1
  40. package/dist/locus-info/selfUtils.js +1 -0
  41. package/dist/locus-info/selfUtils.js.map +1 -1
  42. package/dist/locus-info/types.js.map +1 -1
  43. package/dist/media/MediaConnectionAwaiter.js +57 -1
  44. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  45. package/dist/media/properties.js +4 -2
  46. package/dist/media/properties.js.map +1 -1
  47. package/dist/meeting/in-meeting-actions.js +7 -1
  48. package/dist/meeting/in-meeting-actions.js.map +1 -1
  49. package/dist/meeting/index.js +1173 -877
  50. package/dist/meeting/index.js.map +1 -1
  51. package/dist/meeting/request.js +50 -0
  52. package/dist/meeting/request.js.map +1 -1
  53. package/dist/meeting/request.type.js.map +1 -1
  54. package/dist/meeting/util.js +133 -3
  55. package/dist/meeting/util.js.map +1 -1
  56. package/dist/meetings/index.js +117 -48
  57. package/dist/meetings/index.js.map +1 -1
  58. package/dist/member/index.js +10 -0
  59. package/dist/member/index.js.map +1 -1
  60. package/dist/member/util.js +10 -0
  61. package/dist/member/util.js.map +1 -1
  62. package/dist/metrics/constants.js +2 -1
  63. package/dist/metrics/constants.js.map +1 -1
  64. package/dist/multistream/mediaRequestManager.js +9 -60
  65. package/dist/multistream/mediaRequestManager.js.map +1 -1
  66. package/dist/multistream/remoteMediaManager.js +11 -0
  67. package/dist/multistream/remoteMediaManager.js.map +1 -1
  68. package/dist/reachability/index.js +18 -10
  69. package/dist/reachability/index.js.map +1 -1
  70. package/dist/reactions/reactions.type.js.map +1 -1
  71. package/dist/reconnection-manager/index.js +0 -1
  72. package/dist/reconnection-manager/index.js.map +1 -1
  73. package/dist/types/aiEnableRequest/index.d.ts +5 -0
  74. package/dist/types/aiEnableRequest/utils.d.ts +2 -0
  75. package/dist/types/config.d.ts +4 -0
  76. package/dist/types/constants.d.ts +23 -1
  77. package/dist/types/hashTree/constants.d.ts +1 -0
  78. package/dist/types/hashTree/hashTree.d.ts +7 -0
  79. package/dist/types/hashTree/hashTreeParser.d.ts +122 -14
  80. package/dist/types/hashTree/types.d.ts +3 -0
  81. package/dist/types/hashTree/utils.d.ts +6 -0
  82. package/dist/types/index.d.ts +1 -0
  83. package/dist/types/interceptors/constant.d.ts +5 -0
  84. package/dist/types/interceptors/dataChannelAuthToken.d.ts +43 -0
  85. package/dist/types/interceptors/index.d.ts +2 -1
  86. package/dist/types/interceptors/utils.d.ts +1 -0
  87. package/dist/types/locus-info/index.d.ts +60 -8
  88. package/dist/types/locus-info/types.d.ts +7 -0
  89. package/dist/types/media/MediaConnectionAwaiter.d.ts +10 -1
  90. package/dist/types/media/properties.d.ts +2 -1
  91. package/dist/types/meeting/in-meeting-actions.d.ts +6 -0
  92. package/dist/types/meeting/index.d.ts +61 -7
  93. package/dist/types/meeting/request.d.ts +16 -1
  94. package/dist/types/meeting/request.type.d.ts +5 -0
  95. package/dist/types/meeting/util.d.ts +31 -0
  96. package/dist/types/meetings/index.d.ts +4 -2
  97. package/dist/types/member/index.d.ts +1 -0
  98. package/dist/types/member/util.d.ts +5 -0
  99. package/dist/types/metrics/constants.d.ts +1 -0
  100. package/dist/types/multistream/mediaRequestManager.d.ts +0 -23
  101. package/dist/types/reactions/reactions.type.d.ts +1 -0
  102. package/dist/types/webinar/utils.d.ts +6 -0
  103. package/dist/webinar/index.js +291 -91
  104. package/dist/webinar/index.js.map +1 -1
  105. package/dist/webinar/utils.js +25 -0
  106. package/dist/webinar/utils.js.map +1 -0
  107. package/package.json +24 -23
  108. package/src/aiEnableRequest/README.md +84 -0
  109. package/src/aiEnableRequest/index.ts +170 -0
  110. package/src/aiEnableRequest/utils.ts +25 -0
  111. package/src/annotation/index.ts +27 -7
  112. package/src/config.ts +4 -0
  113. package/src/constants.ts +29 -1
  114. package/src/hashTree/constants.ts +1 -0
  115. package/src/hashTree/hashTree.ts +17 -0
  116. package/src/hashTree/hashTreeParser.ts +745 -252
  117. package/src/hashTree/types.ts +4 -0
  118. package/src/hashTree/utils.ts +9 -0
  119. package/src/index.ts +8 -1
  120. package/src/interceptors/constant.ts +6 -0
  121. package/src/interceptors/dataChannelAuthToken.ts +170 -0
  122. package/src/interceptors/index.ts +2 -1
  123. package/src/interceptors/utils.ts +16 -0
  124. package/src/interpretation/index.ts +2 -2
  125. package/src/locus-info/controlsUtils.ts +11 -0
  126. package/src/locus-info/index.ts +579 -113
  127. package/src/locus-info/selfUtils.ts +1 -0
  128. package/src/locus-info/types.ts +8 -0
  129. package/src/media/MediaConnectionAwaiter.ts +41 -1
  130. package/src/media/properties.ts +3 -1
  131. package/src/meeting/in-meeting-actions.ts +12 -0
  132. package/src/meeting/index.ts +291 -76
  133. package/src/meeting/request.ts +42 -0
  134. package/src/meeting/request.type.ts +6 -0
  135. package/src/meeting/util.ts +160 -2
  136. package/src/meetings/index.ts +157 -44
  137. package/src/member/index.ts +10 -0
  138. package/src/member/util.ts +12 -0
  139. package/src/metrics/constants.ts +1 -0
  140. package/src/multistream/mediaRequestManager.ts +4 -54
  141. package/src/multistream/remoteMediaManager.ts +13 -0
  142. package/src/reachability/index.ts +9 -0
  143. package/src/reactions/reactions.type.ts +1 -0
  144. package/src/reconnection-manager/index.ts +0 -1
  145. package/src/webinar/index.ts +191 -6
  146. package/src/webinar/utils.ts +16 -0
  147. package/test/unit/spec/aiEnableRequest/index.ts +981 -0
  148. package/test/unit/spec/aiEnableRequest/utils.ts +130 -0
  149. package/test/unit/spec/annotation/index.ts +69 -7
  150. package/test/unit/spec/hashTree/hashTree.ts +66 -0
  151. package/test/unit/spec/hashTree/hashTreeParser.ts +2225 -189
  152. package/test/unit/spec/interceptors/dataChannelAuthToken.ts +210 -0
  153. package/test/unit/spec/interceptors/utils.ts +75 -0
  154. package/test/unit/spec/locus-info/controlsUtils.js +29 -0
  155. package/test/unit/spec/locus-info/index.js +1134 -55
  156. package/test/unit/spec/media/MediaConnectionAwaiter.ts +41 -1
  157. package/test/unit/spec/media/properties.ts +12 -3
  158. package/test/unit/spec/meeting/in-meeting-actions.ts +8 -2
  159. package/test/unit/spec/meeting/index.js +829 -115
  160. package/test/unit/spec/meeting/request.js +70 -0
  161. package/test/unit/spec/meeting/utils.js +438 -26
  162. package/test/unit/spec/meetings/index.js +653 -32
  163. package/test/unit/spec/member/index.js +28 -4
  164. package/test/unit/spec/member/util.js +65 -27
  165. package/test/unit/spec/multistream/mediaRequestManager.ts +2 -85
  166. package/test/unit/spec/multistream/remoteMediaManager.ts +30 -0
  167. package/test/unit/spec/reachability/index.ts +23 -0
  168. package/test/unit/spec/reconnection-manager/index.js +4 -8
  169. package/test/unit/spec/webinar/index.ts +474 -37
  170. package/test/unit/spec/webinar/utils.ts +39 -0
@@ -33,6 +33,7 @@ import {
33
33
  ToggleReactionsOptions,
34
34
  PostMeetingDataConsentOptions,
35
35
  SynchronizeVideoLayout,
36
+ fetchDataChannelTokenOptions,
36
37
  } from './request.type';
37
38
  import MeetingUtil from './util';
38
39
  import {AnnotationInfo} from '../annotation/annotation.types';
@@ -1126,4 +1127,45 @@ export default class MeetingRequest extends StatelessWebexPlugin {
1126
1127
  throw err;
1127
1128
  }
1128
1129
  }
1130
+
1131
+ /**
1132
+ * Sends a request to retrieve the datachannel authorization token for a participant.
1133
+ *
1134
+ * For regular meeting data channel:
1135
+ * GET /locus/api/v1/loci/{uuid:lid}/participant/{uuid:pid}/datachannel/token
1136
+ *
1137
+ * For practice session data channel:
1138
+ * GET /locus/api/v1/loci/{uuid:lid}/participant/{uuid:pid}/practiceSession/datachannel/token
1139
+ *
1140
+ * @param {string} locusUrl - The locus url.
1141
+ * @param {string} requestingParticipantId - The participant UUID.
1142
+ * @param {boolean} [isPracticeSession=false] - Whether to get the practice session token.
1143
+ * @returns {Promise<{datachannelToken: string}>}
1144
+ */
1145
+ public async fetchDatachannelToken({
1146
+ locusUrl,
1147
+ requestingParticipantId,
1148
+ isPracticeSession = false,
1149
+ }: fetchDataChannelTokenOptions) {
1150
+ if (!locusUrl || !requestingParticipantId) {
1151
+ return Promise.reject(new Error('locusUrl and participantId are required'));
1152
+ }
1153
+ const practicePrefix = isPracticeSession ? '/practiceSession' : '';
1154
+
1155
+ const uri = `${locusUrl}/${PARTICIPANT}/${requestingParticipantId}${practicePrefix}/datachannel/token`;
1156
+
1157
+ // @ts-ignore
1158
+ return this.locusDeltaRequest({
1159
+ method: HTTP_VERBS.GET,
1160
+ uri,
1161
+ }).catch((err) => {
1162
+ LoggerProxy.logger.warn(
1163
+ `Meeting:request#fetchDatachannelToken --> Failed to retrieve ${
1164
+ isPracticeSession ? 'practice session ' : ''
1165
+ }datachannel token: ${err?.message || err}`
1166
+ );
1167
+
1168
+ return null;
1169
+ });
1170
+ }
1129
1171
  }
@@ -88,4 +88,10 @@ export type UnsetStageVideoLayout = {
88
88
  overrideDefault: false;
89
89
  };
90
90
 
91
+ export type fetchDataChannelTokenOptions = {
92
+ locusUrl: string;
93
+ requestingParticipantId: string;
94
+ isPracticeSession: boolean;
95
+ };
96
+
91
97
  export type SynchronizeVideoLayout = SetStageVideoLayout | UnsetStageVideoLayout;
@@ -1,4 +1,5 @@
1
1
  import {LocalCameraStream, LocalMicrophoneStream} from '@webex/media-helpers';
2
+ import url from 'url';
2
3
 
3
4
  import {cloneDeep} from 'lodash';
4
5
  import {MeetingNotActiveError, UserNotJoinedError} from '../common/errors/webex-errors';
@@ -24,6 +25,7 @@ import PermissionError from '../common/errors/permission';
24
25
  import PasswordError from '../common/errors/password-error';
25
26
  import CaptchaError from '../common/errors/captcha-error';
26
27
  import Trigger from '../common/events/trigger-proxy';
28
+ import {ServerRoles} from '../member/types';
27
29
 
28
30
  const MeetingUtil = {
29
31
  parseLocusJoin: (response) => {
@@ -32,6 +34,7 @@ const MeetingUtil = {
32
34
  // First todo: add check for existance
33
35
  parsed.locus = response.body.locus;
34
36
  parsed.dataSets = response.body.dataSets;
37
+ parsed.metadata = response.body.metaData;
35
38
  parsed.mediaConnections = response.body.mediaConnections;
36
39
  parsed.locusUrl = parsed.locus.url;
37
40
  parsed.locusId = parsed.locus.url.split('/').pop();
@@ -47,6 +50,124 @@ const MeetingUtil = {
47
50
  return parsed;
48
51
  },
49
52
 
53
+ /**
54
+ * Sanitizes a WebSocket URL by extracting only protocol, host, and pathname
55
+ * Returns concatenated protocol + host + pathname for safe logging
56
+ * Note: This is used for logging only; URL matching uses partial matching via _urlsPartiallyMatch
57
+ * @param {string} urlString - The URL to sanitize
58
+ * @returns {string} Sanitized URL or empty string if parsing fails
59
+ */
60
+ sanitizeWebSocketUrl: (urlString: string): string => {
61
+ if (!urlString || typeof urlString !== 'string') {
62
+ return '';
63
+ }
64
+
65
+ try {
66
+ const parsedUrl = url.parse(urlString);
67
+ const protocol = parsedUrl.protocol || '';
68
+ const host = parsedUrl.host || '';
69
+
70
+ // If we don't have at least protocol and host, it's not a valid URL
71
+ if (!protocol || !host) {
72
+ return '';
73
+ }
74
+
75
+ const pathname = parsedUrl.pathname || '';
76
+
77
+ // Strip trailing slash if pathname is just '/'
78
+ const normalizedPathname = pathname === '/' ? '' : pathname;
79
+
80
+ return `${protocol}//${host}${normalizedPathname}`;
81
+ } catch (error) {
82
+ LoggerProxy.logger.warn(
83
+ `Meeting:util#sanitizeWebSocketUrl --> unable to parse URL: ${error}`
84
+ );
85
+
86
+ return '';
87
+ }
88
+ },
89
+
90
+ /**
91
+ * Checks if two URLs partially match using an endsWith approach
92
+ * Combines host and pathname, then checks if one ends with the other
93
+ * This handles cases where one URL goes through a proxy (e.g., /webproxy/) while the other is direct
94
+ * @param {string} url1 - First URL to compare
95
+ * @param {string} url2 - Second URL to compare
96
+ * @returns {boolean} True if one URL path ends with the other (partial match), false otherwise
97
+ */
98
+ _urlsPartiallyMatch: (url1: string, url2: string): boolean => {
99
+ if (!url1 || !url2) {
100
+ return false;
101
+ }
102
+
103
+ try {
104
+ const parsedUrl1 = url.parse(url1);
105
+ const parsedUrl2 = url.parse(url2);
106
+
107
+ const host1 = parsedUrl1.host || '';
108
+ const host2 = parsedUrl2.host || '';
109
+ const pathname1 = parsedUrl1.pathname || '';
110
+ const pathname2 = parsedUrl2.pathname || '';
111
+
112
+ // If either failed to parse, they don't match
113
+ if (!host1 || !host2 || !pathname1 || !pathname2) {
114
+ return false;
115
+ }
116
+
117
+ // Combine host and pathname for comparison
118
+ const combined1 = host1 + pathname1;
119
+ const combined2 = host2 + pathname2;
120
+
121
+ // Check if one combined path ends with the other (handles proxy URLs)
122
+ return combined1.endsWith(combined2) || combined2.endsWith(combined1);
123
+ } catch (e) {
124
+ LoggerProxy.logger.warn('Meeting:util#_urlsPartiallyMatch --> error comparing URLs', e);
125
+
126
+ return false;
127
+ }
128
+ },
129
+
130
+ /**
131
+ * Gets socket URL information for metrics, including whether the socket URLs match
132
+ * Uses partial matching to handle proxy URLs (e.g., URLs with /webproxy/ prefix)
133
+ * @param {Object} webex - The webex instance
134
+ * @returns {Object} Object with hasMismatchedSocket, mercurySocketUrl, and deviceSocketUrl properties
135
+ */
136
+ getSocketUrlInfo: (
137
+ webex: any
138
+ ): {hasMismatchedSocket: boolean; mercurySocketUrl: string; deviceSocketUrl: string} => {
139
+ try {
140
+ const mercuryUrl = webex?.internal?.mercury?.socket?.url;
141
+ const deviceUrl = webex?.internal?.device?.webSocketUrl;
142
+
143
+ const sanitizedMercuryUrl = MeetingUtil.sanitizeWebSocketUrl(mercuryUrl);
144
+ const sanitizedDeviceUrl = MeetingUtil.sanitizeWebSocketUrl(deviceUrl);
145
+
146
+ // Only report a mismatch if both URLs are present and they don't match
147
+ // If either URL is missing, we can't determine if there's a mismatch, so return false
148
+ let hasMismatchedSocket = false;
149
+ if (sanitizedMercuryUrl && sanitizedDeviceUrl) {
150
+ hasMismatchedSocket = !MeetingUtil._urlsPartiallyMatch(mercuryUrl, deviceUrl);
151
+ }
152
+
153
+ return {
154
+ hasMismatchedSocket,
155
+ mercurySocketUrl: sanitizedMercuryUrl,
156
+ deviceSocketUrl: sanitizedDeviceUrl,
157
+ };
158
+ } catch (error) {
159
+ LoggerProxy.logger.warn(
160
+ `Meeting:util#getSocketUrlInfo --> error getting socket URL info: ${error}`
161
+ );
162
+
163
+ return {
164
+ hasMismatchedSocket: false,
165
+ mercurySocketUrl: '',
166
+ deviceSocketUrl: '',
167
+ };
168
+ }
169
+ },
170
+
50
171
  remoteUpdateAudioVideo: (meeting, audioMuted?: boolean, videoMuted?: boolean) => {
51
172
  if (!meeting) {
52
173
  return Promise.reject(new ParameterError('You need a meeting object.'));
@@ -203,6 +324,7 @@ const MeetingUtil = {
203
324
  const parsed = MeetingUtil.parseLocusJoin(res);
204
325
  meeting.setLocus(parsed);
205
326
  meeting.isoLocalClientMeetingJoinTime = res?.headers?.date; // read from header if exist, else fall back to system clock : https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-555657
327
+ const socketUrlInfo = MeetingUtil.getSocketUrlInfo(webex);
206
328
  webex.internal.newMetrics.submitClientEvent({
207
329
  name: 'client.locus.join.response',
208
330
  payload: {
@@ -210,6 +332,9 @@ const MeetingUtil = {
210
332
  identifiers: {
211
333
  trackingId: res.headers.trackingid,
212
334
  },
335
+ eventData: {
336
+ ...socketUrlInfo,
337
+ },
213
338
  },
214
339
  options: {
215
340
  meetingId: meeting.id,
@@ -220,12 +345,19 @@ const MeetingUtil = {
220
345
  return parsed;
221
346
  })
222
347
  .catch((err) => {
348
+ const socketUrlInfo = MeetingUtil.getSocketUrlInfo(webex);
223
349
  webex.internal.newMetrics.submitClientEvent({
224
350
  name: 'client.locus.join.response',
225
351
  payload: {
226
352
  identifiers: {meetingLookupUrl: meeting.meetingInfo?.meetingLookupUrl},
353
+ eventData: {
354
+ ...socketUrlInfo,
355
+ },
356
+ },
357
+ options: {
358
+ meetingId: meeting.id,
359
+ rawError: err,
227
360
  },
228
- options: {meetingId: meeting.id, rawError: err},
229
361
  });
230
362
 
231
363
  throw err;
@@ -237,6 +369,7 @@ const MeetingUtil = {
237
369
  meeting.stopPeriodicLogUpload();
238
370
 
239
371
  meeting.breakouts.cleanUp();
372
+ meeting.webinar.cleanUp();
240
373
  meeting.simultaneousInterpretation.cleanUp();
241
374
  meeting.locusMediaRequest = undefined;
242
375
 
@@ -261,8 +394,10 @@ const MeetingUtil = {
261
394
  .then(() => meeting.stopKeepAlive())
262
395
  .then(() => {
263
396
  if (meeting.config?.enableAutomaticLLM) {
264
- meeting.updateLLMConnection();
397
+ return meeting.cleanupLLMConneciton({throwOnError: false});
265
398
  }
399
+
400
+ return undefined;
266
401
  });
267
402
  },
268
403
 
@@ -528,6 +663,11 @@ const MeetingUtil = {
528
663
  displayHints.includes(DISPLAY_HINTS.LEAVE_TRANSFER_HOST_END_MEETING) ||
529
664
  displayHints.includes(DISPLAY_HINTS.LEAVE_END_MEETING),
530
665
 
666
+ requireHostEndMeetingBeforeLeave: (displayHints) =>
667
+ displayHints.includes(DISPLAY_HINTS.REQUIRE_HOST_END_MEETING_BEFORE_LEAVE) ||
668
+ (!displayHints.includes(DISPLAY_HINTS.LEAVE_TRANSFER_HOST_END_MEETING) &&
669
+ displayHints.includes(DISPLAY_HINTS.END_MEETING)),
670
+
531
671
  canManageBreakout: (displayHints) => displayHints.includes(DISPLAY_HINTS.BREAKOUT_MANAGEMENT),
532
672
 
533
673
  canStartBreakout: (displayHints) => !displayHints.includes(DISPLAY_HINTS.DISABLE_BREAKOUT_START),
@@ -772,6 +912,24 @@ const MeetingUtil = {
772
912
  return locusDeltaRequest;
773
913
  },
774
914
 
915
+ canAttendeeRequestAiAssistantEnabled: (displayHints = [], roles: any[] = []) => {
916
+ const isHostOrCoHost =
917
+ roles.includes(ServerRoles.Cohost) || roles.includes(ServerRoles.Moderator);
918
+
919
+ if (isHostOrCoHost) {
920
+ return false;
921
+ }
922
+
923
+ if (displayHints.includes(DISPLAY_HINTS.ATTENDEE_REQUEST_AI_ASSISTANT_ENABLED)) {
924
+ return true;
925
+ }
926
+
927
+ return false;
928
+ },
929
+
930
+ attendeeRequestAiAssistantDeclinedAll: (displayHints = []) =>
931
+ displayHints.includes(DISPLAY_HINTS.ATTENDEE_REQUEST_AI_ASSISTANT_DECLINED_ALL),
932
+
775
933
  selfSupportsFeature: (feature: SELF_POLICY, userPolicies: Record<SELF_POLICY, boolean>) => {
776
934
  if (!userPolicies) {
777
935
  return true;
@@ -47,7 +47,7 @@ import {
47
47
  import BEHAVIORAL_METRICS from '../metrics/constants';
48
48
  import MeetingInfo from '../meeting-info';
49
49
  import MeetingInfoV2 from '../meeting-info/meeting-info-v2';
50
- import Meeting, {CallStateForMetrics} from '../meeting';
50
+ import Meeting, {CallStateForMetrics, storeEventForDebugging} from '../meeting';
51
51
  import PersonalMeetingRoom from '../personal-meeting-room';
52
52
  import Reachability from '../reachability';
53
53
  import Request from './request';
@@ -69,6 +69,7 @@ import JoinForbiddenError from '../common/errors/join-forbidden-error';
69
69
  import {HashTreeMessage} from '../hashTree/hashTreeParser';
70
70
  import {HashTreeObject} from '../hashTree/types';
71
71
  import {isSelf} from '../hashTree/utils';
72
+ import {createLocusFromHashTreeMessage, findMeetingForHashTreeMessage} from '../locus-info';
72
73
 
73
74
  let mediaLogger;
74
75
 
@@ -195,6 +196,8 @@ export default class Meetings extends WebexPlugin {
195
196
  preferredWebexSite: any;
196
197
  reachability: Reachability;
197
198
  registered: any;
199
+ registrationPromise: Promise<void>;
200
+ unregistrationPromise: Promise<void>;
198
201
  request: any;
199
202
  geoHintInfo: any;
200
203
  meetingInfo: any;
@@ -433,6 +436,20 @@ export default class Meetings extends WebexPlugin {
433
436
  return existingMeeting;
434
437
  }
435
438
 
439
+ if (data.eventType === LOCUSEVENT.HASH_TREE_DATA_UPDATED) {
440
+ // need to check if maybe this event indicates a move to/from breakout
441
+ const meetingForHashTreeMessage = findMeetingForHashTreeMessage(
442
+ data.stateElementsMessage,
443
+ this.meetingCollection,
444
+ // @ts-ignore
445
+ this.webex.internal.device.url
446
+ );
447
+
448
+ if (meetingForHashTreeMessage) {
449
+ return meetingForHashTreeMessage;
450
+ }
451
+ }
452
+
436
453
  // if that didn't work, fallback to other fields like correlationId, sipUri, etc
437
454
 
438
455
  // If the event is a hash tree event, we need to extract "self" object from it
@@ -476,6 +493,11 @@ export default class Meetings extends WebexPlugin {
476
493
  private handleLocusEvent(data: LocusEvent, useRandomDelayForInfo = false) {
477
494
  let meeting = this.getCorrespondingMeetingByLocus(data);
478
495
 
496
+ // @ts-ignore
497
+ if (this.config.experimental.storeLocusHashTreeEventsForDebugging) {
498
+ storeEventForDebugging('mercury', data);
499
+ }
500
+
479
501
  // Special case when locus has got replaced, This only happend once if a replace locus exists
480
502
  // https://sqbu-github.cisco.com/WebExSquared/locus/wiki/Locus-changing-mid-call
481
503
 
@@ -489,7 +511,7 @@ export default class Meetings extends WebexPlugin {
489
511
  }
490
512
 
491
513
  if (meeting && !MeetingsUtil.isBreakoutLocusDTO(data.locus)) {
492
- meeting.locusInfo.updateMainSessionLocusCache(data.locus);
514
+ meeting.locusInfo.updateMainSessionLocusCache(data.locus); // here data.locus will never be a complete locus
493
515
  }
494
516
  if (!this.isNeedHandleLocusDTO(meeting, data.locus)) {
495
517
  LoggerProxy.logger.log(
@@ -520,41 +542,45 @@ export default class Meetings extends WebexPlugin {
520
542
  // };
521
543
  // rather then locus object change to locus url
522
544
 
523
- if (data.eventType !== LOCUSEVENT.HASH_TREE_DATA_UPDATED) {
524
- if (
525
- data.locus &&
526
- data.locus.fullState &&
527
- data.locus.fullState.state === LOCUS.STATE.INACTIVE
528
- ) {
529
- // just ignore the event as its already ended and not active
530
- LoggerProxy.logger.warn(
531
- 'Meetings:index#handleLocusEvent --> Locus event received for meeting, after it was ended.'
532
- );
533
-
534
- return;
535
- }
545
+ if (data.eventType === LOCUSEVENT.HASH_TREE_DATA_UPDATED) {
546
+ // We're about to create a new meeting object from this hash tree message.
547
+ // There is some existing (pre-hash trees) SDK logic here that requires a locus object
548
+ // (at the very minimum we need locus.url to be set)
549
+ // so we try to create locus from the received hash tree message
550
+ // it will not be complete, in most cases it will only have the self part, but that's still better than nothing
551
+ const {locus} = createLocusFromHashTreeMessage(data.stateElementsMessage);
552
+
553
+ data.locus = locus;
554
+ }
536
555
 
537
- // When its wireless share or guest and user leaves the meeting we dont have to keep the meeting object
538
- // Any future events will be neglected
539
-
540
- if (
541
- data.locus &&
542
- data.locus.self &&
543
- data.locus.self.state === _LEFT_ &&
544
- data.locus.self.removed === true
545
- ) {
546
- // just ignore the event as its already ended and not active
547
- LoggerProxy.logger.warn(
548
- 'Meetings:index#handleLocusEvent --> Locus event received for meeting, after it was ended.'
549
- );
556
+ if (
557
+ data.locus &&
558
+ data.locus.fullState &&
559
+ data.locus.fullState.state === LOCUS.STATE.INACTIVE
560
+ ) {
561
+ // just ignore the event as its already ended and not active
562
+ LoggerProxy.logger.warn(
563
+ 'Meetings:index#handleLocusEvent --> Locus event received for meeting, after it was ended.'
564
+ );
550
565
 
551
- return;
552
- }
566
+ return;
553
567
  }
554
568
 
555
- if (data.eventType === LOCUSEVENT.HASH_TREE_DATA_UPDATED) {
556
- // in hash tree messages we don't ge the locus object, but the meeting constructor needs at least locus.url
557
- set(data, 'locus.url', data.stateElementsMessage.locusUrl);
569
+ // When its wireless share or guest and user leaves the meeting we dont have to keep the meeting object
570
+ // Any future events will be neglected
571
+
572
+ if (
573
+ data.locus &&
574
+ data.locus.self &&
575
+ data.locus.self.state === _LEFT_ &&
576
+ data.locus.self.removed === true
577
+ ) {
578
+ // just ignore the event as its already ended and not active
579
+ LoggerProxy.logger.warn(
580
+ 'Meetings:index#handleLocusEvent --> Locus event received for meeting, after it was ended.'
581
+ );
582
+
583
+ return;
558
584
  }
559
585
 
560
586
  this.create(data.locus, DESTINATION_TYPE.LOCUS_ID, useRandomDelayForInfo)
@@ -624,7 +650,7 @@ export default class Meetings extends WebexPlugin {
624
650
  }
625
651
 
626
652
  /**
627
- * handles locus events through mercury that are not roap
653
+ * handles locus events through mercury that are not roap or approval request events
628
654
  * @param {Object} envelope
629
655
  * @param {Object} envelope.data
630
656
  * @param {String} envelope.data.eventType
@@ -637,7 +663,11 @@ export default class Meetings extends WebexPlugin {
637
663
  // eslint-disable-next-line @typescript-eslint/no-shadow
638
664
  const {eventType} = data;
639
665
 
640
- if (eventType && eventType !== LOCUSEVENT.MESSAGE_ROAP) {
666
+ if (
667
+ eventType &&
668
+ eventType !== LOCUSEVENT.MESSAGE_ROAP &&
669
+ eventType !== LOCUSEVENT.APPROVAL_REQUEST
670
+ ) {
641
671
  this.handleLocusEvent(data, true);
642
672
  }
643
673
  }
@@ -929,9 +959,20 @@ export default class Meetings extends WebexPlugin {
929
959
  * @returns {Promise} A promise that resolves when the step is completed.
930
960
  */
931
961
  executeRegistrationStep(step: () => Promise<any>, stepName: string) {
932
- return step().then(() => {
933
- this.registrationStatus[stepName] = true;
934
- });
962
+ return step()
963
+ .then(() => {
964
+ LoggerProxy.logger.info(
965
+ `Meetings:index#executeRegistrationStep --> INFO, ${stepName} completed`
966
+ );
967
+ this.registrationStatus[stepName] = true;
968
+ })
969
+ .catch((error) => {
970
+ LoggerProxy.logger.error(
971
+ `Meetings:index#executeRegistrationStep --> ERROR, ${stepName} failed: ${error.message}`
972
+ );
973
+
974
+ return Promise.reject(error);
975
+ });
935
976
  }
936
977
 
937
978
  /**
@@ -944,7 +985,33 @@ export default class Meetings extends WebexPlugin {
944
985
  * @memberof Meetings
945
986
  */
946
987
  public register(deviceRegistrationOptions?: DeviceRegistrationOptions): Promise<any> {
947
- this.registrationStatus = clone(INITIAL_REGISTRATION_STATUS);
988
+ if (this.unregistrationPromise) {
989
+ LoggerProxy.logger.info(
990
+ 'Meetings:index#register --> INFO, Meetings plugin unregistration in progress, waiting to register'
991
+ );
992
+
993
+ this.registrationPromise = this.unregistrationPromise
994
+ .catch(() => {}) // It doesn't matter what happened during unregistration
995
+ .finally(() => {
996
+ LoggerProxy.logger.info(
997
+ 'Meetings:index#register --> INFO, Meetings plugin unregistration completed, proceeding to register'
998
+ );
999
+
1000
+ this.registrationPromise = null;
1001
+
1002
+ return this.register(deviceRegistrationOptions);
1003
+ });
1004
+
1005
+ return this.registrationPromise;
1006
+ }
1007
+
1008
+ if (this.registrationPromise) {
1009
+ LoggerProxy.logger.info(
1010
+ 'Meetings:index#register --> INFO, Meetings plugin registration in progress, returning existing promise'
1011
+ );
1012
+
1013
+ return this.registrationPromise;
1014
+ }
948
1015
 
949
1016
  // @ts-ignore
950
1017
  if (!this.webex.canAuthorize) {
@@ -963,7 +1030,11 @@ export default class Meetings extends WebexPlugin {
963
1030
  return Promise.resolve();
964
1031
  }
965
1032
 
966
- return Promise.all([
1033
+ LoggerProxy.logger.info('Meetings:index#register --> INFO, Registering Meetings plugin');
1034
+
1035
+ this.registrationStatus = clone(INITIAL_REGISTRATION_STATUS);
1036
+
1037
+ this.registrationPromise = Promise.all([
967
1038
  this.executeRegistrationStep(() => this.fetchUserPreferredWebexSite(), 'fetchWebexSite'),
968
1039
  this.executeRegistrationStep(() => this.getGeoHint(), 'getGeoHint'),
969
1040
  this.executeRegistrationStep(
@@ -1022,7 +1093,12 @@ export default class Meetings extends WebexPlugin {
1022
1093
  });
1023
1094
 
1024
1095
  return Promise.reject(error);
1096
+ })
1097
+ .finally(() => {
1098
+ this.registrationPromise = null;
1025
1099
  });
1100
+
1101
+ return this.registrationPromise;
1026
1102
  }
1027
1103
 
1028
1104
  /**
@@ -1034,6 +1110,35 @@ export default class Meetings extends WebexPlugin {
1034
1110
  * @memberof Meetings
1035
1111
  */
1036
1112
  unregister() {
1113
+ if (this.unregistrationPromise) {
1114
+ LoggerProxy.logger.info(
1115
+ 'Meetings:index#unregister --> INFO, Meetings plugin unregistration in progress, returning existing promise'
1116
+ );
1117
+
1118
+ return this.unregistrationPromise;
1119
+ }
1120
+
1121
+ if (this.registrationPromise) {
1122
+ LoggerProxy.logger.info(
1123
+ 'Meetings:index#unregister --> INFO, Meetings plugin registration in progress, waiting to unregister'
1124
+ );
1125
+
1126
+ // Wait for registration to complete (success or failure), then call unregister again
1127
+ this.unregistrationPromise = this.registrationPromise
1128
+ .catch(() => {}) // It doesn't matter what happened during registration
1129
+ .finally(() => {
1130
+ LoggerProxy.logger.info(
1131
+ 'Meetings:index#unregister --> INFO, Meetings plugin registration completed, proceeding to unregister'
1132
+ );
1133
+
1134
+ this.unregistrationPromise = null;
1135
+
1136
+ return this.unregister();
1137
+ });
1138
+
1139
+ return this.unregistrationPromise;
1140
+ }
1141
+
1037
1142
  if (!this.registered) {
1038
1143
  LoggerProxy.logger.info(
1039
1144
  'Meetings:index#unregister --> INFO, Meetings plugin already unregistered'
@@ -1044,10 +1149,14 @@ export default class Meetings extends WebexPlugin {
1044
1149
 
1045
1150
  this.stopListeningForEvents();
1046
1151
 
1047
- return (
1152
+ this.unregistrationPromise =
1048
1153
  // @ts-ignore
1049
1154
  this.webex.internal.mercury
1050
- .disconnect()
1155
+ // Use code 3050 with a non-reconnecting reason to prevent Mercury auto-reconnect
1156
+ // during unregister. Without this, disconnect() defaults to code 1000/"Done" which
1157
+ // force-closes as "Done (forced)" - a normalReconnectReason that triggers auto-reconnect,
1158
+ // causing a race condition with device.unregister().
1159
+ .disconnect({code: 3050, reason: 'meetings unregister'})
1051
1160
  // @ts-ignore
1052
1161
  .then(() => this.webex.internal.device.unregister())
1053
1162
  .catch((error) => {
@@ -1077,7 +1186,11 @@ export default class Meetings extends WebexPlugin {
1077
1186
  this.registered = false;
1078
1187
  this.registrationStatus = clone(INITIAL_REGISTRATION_STATUS);
1079
1188
  })
1080
- );
1189
+ .finally(() => {
1190
+ this.unregistrationPromise = null;
1191
+ });
1192
+
1193
+ return this.unregistrationPromise;
1081
1194
  }
1082
1195
 
1083
1196
  /**
@@ -1888,7 +2001,7 @@ export default class Meetings extends WebexPlugin {
1888
2001
 
1889
2002
  const associateBreakoutLocus = this.breakoutLocusForHandleLater[existIndex];
1890
2003
  this.handleLocusEvent({
1891
- eventType: LOCUSEVENT.SDK_NO_EVENT,
2004
+ eventType: LOCUSEVENT.SDK_LOCUS_FROM_SYNC_MEETINGS,
1892
2005
  locus: associateBreakoutLocus,
1893
2006
  locusUrl: associateBreakoutLocus.url,
1894
2007
  });
@@ -13,6 +13,7 @@ export type MemberId = string;
13
13
  export default class Member {
14
14
  associatedUser: MemberId | null; // deprecated, use associatedUsers instead
15
15
  associatedUsers: Set<MemberId>; // users associated with this device, empty if this member is not a device
16
+ canApproveAIEnablement: boolean;
16
17
  canReclaimHost: boolean;
17
18
  id: MemberId;
18
19
  isAudioMuted: any;
@@ -291,6 +292,14 @@ export default class Member {
291
292
  */
292
293
  this.isPresenterAssignmentProhibited = null;
293
294
 
295
+ /**
296
+ * @instance
297
+ * @type {Boolean}
298
+ * @public
299
+ * @memberof Member
300
+ */
301
+ this.canApproveAIEnablement = null;
302
+
294
303
  /**
295
304
  * @instance
296
305
  * @type {Boolean}
@@ -360,6 +369,7 @@ export default class Member {
360
369
  MemberUtil.isModeratorAssignmentProhibited(participant);
361
370
  this.isPresenterAssignmentProhibited =
362
371
  MemberUtil.isPresenterAssignmentProhibited(participant);
372
+ this.canApproveAIEnablement = MemberUtil.canApproveAIEnablement(participant);
363
373
  this.processStatus(participant);
364
374
  this.processRoles(participant);
365
375
  // must be done last
@@ -42,6 +42,18 @@ const MemberUtil = {
42
42
  return participant.canReclaimHostRole || false;
43
43
  },
44
44
 
45
+ /**
46
+ * @param {Object} participant - The locus participant object.
47
+ * @returns {Boolean}
48
+ */
49
+ canApproveAIEnablement: (participant) => {
50
+ if (!participant) {
51
+ return false;
52
+ }
53
+
54
+ return !participant.attendeeRequestAiAssistantNotAllowed;
55
+ },
56
+
45
57
  /**
46
58
  * @param {Object} participant - The locus participant object.
47
59
  * @returns {[ServerRoleShape]}
@@ -90,6 +90,7 @@ const BEHAVIORAL_METRICS = {
90
90
  MEDIA_ISSUE_DETECTED: 'js_sdk_media_issue_detected',
91
91
  LOCUS_CLASSIC_VS_HASH_TREE_MISMATCH: 'js_sdk_locus_classic_vs_hash_tree_mismatch',
92
92
  LOCUS_HASH_TREE_UNSUPPORTED_OPERATION: 'js_sdk_locus_hash_tree_unsupported_operation',
93
+ MEDIA_STILL_NOT_CONNECTED: 'js_sdk_media_still_not_connected',
93
94
  };
94
95
 
95
96
  export {BEHAVIORAL_METRICS as default};