@webex/plugin-meetings 3.12.0-next.7 → 3.12.0-next.70
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/AGENTS.md +9 -0
- package/dist/aiEnableRequest/index.js +15 -2
- package/dist/aiEnableRequest/index.js.map +1 -1
- package/dist/breakouts/breakout.js +8 -3
- package/dist/breakouts/breakout.js.map +1 -1
- package/dist/breakouts/index.js +26 -2
- package/dist/breakouts/index.js.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.js +30 -7
- package/dist/constants.js.map +1 -1
- package/dist/controls-options-manager/constants.js +11 -1
- package/dist/controls-options-manager/constants.js.map +1 -1
- package/dist/controls-options-manager/index.js +38 -24
- package/dist/controls-options-manager/index.js.map +1 -1
- package/dist/controls-options-manager/util.js +91 -0
- package/dist/controls-options-manager/util.js.map +1 -1
- package/dist/hashTree/constants.js +13 -1
- package/dist/hashTree/constants.js.map +1 -1
- package/dist/hashTree/hashTreeParser.js +880 -382
- package/dist/hashTree/hashTreeParser.js.map +1 -1
- package/dist/hashTree/utils.js +42 -0
- package/dist/hashTree/utils.js.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/interceptors/dataChannelAuthToken.js +75 -15
- package/dist/interceptors/dataChannelAuthToken.js.map +1 -1
- package/dist/interceptors/locusRetry.js +23 -8
- package/dist/interceptors/locusRetry.js.map +1 -1
- package/dist/interpretation/index.js +10 -1
- package/dist/interpretation/index.js.map +1 -1
- package/dist/interpretation/interpretation.types.js +7 -0
- package/dist/interpretation/interpretation.types.js.map +1 -0
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/controlsUtils.js +4 -1
- package/dist/locus-info/controlsUtils.js.map +1 -1
- package/dist/locus-info/index.js +298 -87
- package/dist/locus-info/index.js.map +1 -1
- package/dist/locus-info/types.js +19 -0
- package/dist/locus-info/types.js.map +1 -1
- package/dist/media/index.js +3 -1
- package/dist/media/index.js.map +1 -1
- package/dist/media/properties.js +1 -0
- package/dist/media/properties.js.map +1 -1
- package/dist/meeting/in-meeting-actions.js +3 -1
- package/dist/meeting/in-meeting-actions.js.map +1 -1
- package/dist/meeting/index.js +1046 -689
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/muteState.js +10 -1
- package/dist/meeting/muteState.js.map +1 -1
- package/dist/meeting/request.js +5 -2
- package/dist/meeting/request.js.map +1 -1
- package/dist/meeting/util.js +20 -2
- package/dist/meeting/util.js.map +1 -1
- package/dist/meeting-info/meeting-info-v2.js +2 -2
- package/dist/meeting-info/meeting-info-v2.js.map +1 -1
- package/dist/meetings/index.js +231 -78
- package/dist/meetings/index.js.map +1 -1
- package/dist/meetings/meetings.types.js +6 -1
- package/dist/meetings/meetings.types.js.map +1 -1
- package/dist/meetings/request.js +39 -0
- package/dist/meetings/request.js.map +1 -1
- package/dist/meetings/util.js +79 -5
- package/dist/meetings/util.js.map +1 -1
- package/dist/member/index.js +10 -0
- package/dist/member/index.js.map +1 -1
- package/dist/member/types.js.map +1 -1
- package/dist/member/util.js +3 -0
- package/dist/member/util.js.map +1 -1
- package/dist/metrics/constants.js +4 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/multistream/codec/constants.js +63 -0
- package/dist/multistream/codec/constants.js.map +1 -0
- package/dist/multistream/mediaRequestManager.js +62 -15
- package/dist/multistream/mediaRequestManager.js.map +1 -1
- package/dist/multistream/receiveSlot.js +9 -0
- package/dist/multistream/receiveSlot.js.map +1 -1
- package/dist/reactions/reactions.type.js.map +1 -1
- package/dist/recording-controller/index.js +1 -3
- package/dist/recording-controller/index.js.map +1 -1
- package/dist/types/config.d.ts +2 -0
- package/dist/types/constants.d.ts +9 -1
- package/dist/types/controls-options-manager/constants.d.ts +6 -1
- package/dist/types/controls-options-manager/index.d.ts +10 -0
- package/dist/types/hashTree/constants.d.ts +2 -0
- package/dist/types/hashTree/hashTreeParser.d.ts +146 -17
- package/dist/types/hashTree/utils.d.ts +18 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/interceptors/locusRetry.d.ts +4 -4
- package/dist/types/interpretation/interpretation.types.d.ts +10 -0
- package/dist/types/locus-info/index.d.ts +50 -6
- package/dist/types/locus-info/types.d.ts +21 -1
- package/dist/types/media/properties.d.ts +1 -0
- package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
- package/dist/types/meeting/index.d.ts +78 -5
- package/dist/types/meeting/request.d.ts +1 -0
- package/dist/types/meeting/util.d.ts +8 -0
- package/dist/types/meetings/index.d.ts +30 -2
- package/dist/types/meetings/meetings.types.d.ts +15 -0
- package/dist/types/meetings/request.d.ts +14 -0
- package/dist/types/member/index.d.ts +1 -0
- package/dist/types/member/types.d.ts +1 -0
- package/dist/types/member/util.d.ts +1 -0
- package/dist/types/metrics/constants.d.ts +3 -0
- package/dist/types/multistream/codec/constants.d.ts +7 -0
- package/dist/types/multistream/mediaRequestManager.d.ts +22 -5
- package/dist/types/reactions/reactions.type.d.ts +3 -0
- package/dist/webinar/index.js +305 -159
- package/dist/webinar/index.js.map +1 -1
- package/package.json +22 -22
- package/src/aiEnableRequest/index.ts +16 -0
- package/src/breakouts/breakout.ts +3 -1
- package/src/breakouts/index.ts +31 -0
- package/src/config.ts +2 -0
- package/src/constants.ts +13 -2
- package/src/controls-options-manager/constants.ts +14 -1
- package/src/controls-options-manager/index.ts +47 -24
- package/src/controls-options-manager/util.ts +81 -1
- package/src/hashTree/constants.ts +16 -0
- package/src/hashTree/hashTreeParser.ts +580 -196
- package/src/hashTree/utils.ts +36 -0
- package/src/index.ts +6 -0
- package/src/interceptors/dataChannelAuthToken.ts +88 -12
- package/src/interceptors/locusRetry.ts +25 -4
- package/src/interpretation/index.ts +27 -9
- package/src/interpretation/interpretation.types.ts +11 -0
- package/src/locus-info/controlsUtils.ts +3 -1
- package/src/locus-info/index.ts +293 -97
- package/src/locus-info/types.ts +25 -1
- package/src/media/index.ts +3 -0
- package/src/media/properties.ts +1 -0
- package/src/meeting/in-meeting-actions.ts +4 -0
- package/src/meeting/index.ts +386 -48
- package/src/meeting/muteState.ts +10 -1
- package/src/meeting/request.ts +11 -0
- package/src/meeting/util.ts +21 -2
- package/src/meeting-info/meeting-info-v2.ts +4 -2
- package/src/meetings/index.ts +134 -44
- package/src/meetings/meetings.types.ts +19 -0
- package/src/meetings/request.ts +43 -0
- package/src/meetings/util.ts +97 -1
- package/src/member/index.ts +10 -0
- package/src/member/types.ts +1 -0
- package/src/member/util.ts +3 -0
- package/src/metrics/constants.ts +3 -0
- package/src/multistream/codec/constants.ts +58 -0
- package/src/multistream/mediaRequestManager.ts +119 -28
- package/src/multistream/receiveSlot.ts +18 -0
- package/src/reactions/reactions.type.ts +3 -0
- package/src/recording-controller/index.ts +1 -2
- package/src/webinar/index.ts +214 -36
- package/test/unit/spec/aiEnableRequest/index.ts +86 -0
- package/test/unit/spec/breakouts/breakout.ts +9 -3
- package/test/unit/spec/breakouts/index.ts +49 -0
- package/test/unit/spec/controls-options-manager/index.js +140 -29
- package/test/unit/spec/controls-options-manager/util.js +165 -0
- package/test/unit/spec/hashTree/hashTreeParser.ts +1838 -180
- package/test/unit/spec/hashTree/utils.ts +125 -1
- package/test/unit/spec/interceptors/dataChannelAuthToken.ts +196 -0
- package/test/unit/spec/interceptors/locusRetry.ts +205 -4
- package/test/unit/spec/interpretation/index.ts +26 -4
- package/test/unit/spec/locus-info/controlsUtils.js +172 -57
- package/test/unit/spec/locus-info/index.js +487 -81
- package/test/unit/spec/media/index.ts +31 -0
- package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
- package/test/unit/spec/meeting/index.js +1240 -37
- package/test/unit/spec/meeting/muteState.js +81 -0
- package/test/unit/spec/meeting/request.js +12 -0
- package/test/unit/spec/meeting/utils.js +33 -0
- package/test/unit/spec/meeting-info/meetinginfov2.js +19 -10
- package/test/unit/spec/meetings/index.js +360 -10
- package/test/unit/spec/meetings/request.js +141 -0
- package/test/unit/spec/meetings/utils.js +189 -0
- package/test/unit/spec/member/index.js +7 -0
- package/test/unit/spec/member/util.js +24 -0
- package/test/unit/spec/multistream/mediaRequestManager.ts +501 -37
- package/test/unit/spec/recording-controller/index.js +9 -8
- package/test/unit/spec/webinar/index.ts +329 -28
package/src/webinar/index.ts
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
import {WebexPlugin, config} from '@webex/webex-core';
|
|
5
5
|
import uuid from 'uuid';
|
|
6
6
|
import {get} from 'lodash';
|
|
7
|
-
import {DataChannelTokenType} from '@webex/internal-plugin-llm';
|
|
8
7
|
import {
|
|
9
8
|
_ID_,
|
|
10
9
|
HEADERS,
|
|
@@ -14,12 +13,27 @@ import {
|
|
|
14
13
|
SHARE_STATUS,
|
|
15
14
|
DEFAULT_LARGE_SCALE_WEBINAR_ATTENDEE_SEARCH_LIMIT,
|
|
16
15
|
LLM_PRACTICE_SESSION,
|
|
16
|
+
LOCUS_LLM_EVENT,
|
|
17
17
|
} from '../constants';
|
|
18
18
|
|
|
19
19
|
import WebinarCollection from './collection';
|
|
20
20
|
import LoggerProxy from '../common/logs/logger-proxy';
|
|
21
|
+
import MeetingUtil from '../meeting/util';
|
|
21
22
|
import {sanitizeParams} from './utils';
|
|
22
23
|
|
|
24
|
+
const PS_LLM_EVENTS = [
|
|
25
|
+
{
|
|
26
|
+
event: `event:relay.event:${LLM_PRACTICE_SESSION}`,
|
|
27
|
+
listenerKey: 'relay',
|
|
28
|
+
handlerKey: 'processRelayEvent',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
event: `${LOCUS_LLM_EVENT}:${LLM_PRACTICE_SESSION}`,
|
|
32
|
+
listenerKey: 'locusLLM',
|
|
33
|
+
handlerKey: 'processLocusLLMEvent',
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
|
|
23
37
|
/**
|
|
24
38
|
* @class Webinar
|
|
25
39
|
*/
|
|
@@ -98,13 +112,49 @@ const Webinar = WebexPlugin.extend({
|
|
|
98
112
|
return {isPromoted, isDemoted};
|
|
99
113
|
},
|
|
100
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Resolves the meeting associated with this webinar instance, guarded against the
|
|
117
|
+
* meetingId pointer drifting onto an unrelated transient meeting (e.g. an inbound
|
|
118
|
+
* 1:1 call) that may exist in the meeting collection. Returns the meeting only when
|
|
119
|
+
* its locusUrl matches this webinar's tracked locusUrl. Returns undefined (with a
|
|
120
|
+
* warning) when the meeting cannot be resolved or when the webinar's locusUrl has
|
|
121
|
+
* not been initialized yet — callers must treat this as "no owned meeting" rather
|
|
122
|
+
* than fall through to an unvalidated lookup.
|
|
123
|
+
* @returns {object|undefined}
|
|
124
|
+
*/
|
|
125
|
+
getValidatedWebinarMeeting() {
|
|
126
|
+
const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
|
|
127
|
+
|
|
128
|
+
if (!meeting) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!this.locusUrl) {
|
|
133
|
+
LoggerProxy.logger.warn(
|
|
134
|
+
`Webinar:index#getValidatedWebinarMeeting --> skipping; webinar locusUrl is not yet initialized for meetingId ${this.meetingId}`
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (meeting.locusUrl !== this.locusUrl) {
|
|
141
|
+
LoggerProxy.logger.warn(
|
|
142
|
+
`Webinar:index#getValidatedWebinarMeeting --> skipping; meeting ${this.meetingId} locusUrl ${meeting.locusUrl} does not match webinar locusUrl ${this.locusUrl}`
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return meeting;
|
|
149
|
+
},
|
|
150
|
+
|
|
101
151
|
/**
|
|
102
152
|
* should join practice session data channel or not
|
|
103
153
|
* @param {Object} {isPromoted: boolean, isDemoted: boolean}} Role transition states
|
|
104
154
|
* @returns {void}
|
|
105
155
|
*/
|
|
106
156
|
updateStatusByRole({isPromoted, isDemoted}) {
|
|
107
|
-
const meeting = this.
|
|
157
|
+
const meeting = this.getValidatedWebinarMeeting();
|
|
108
158
|
|
|
109
159
|
if (
|
|
110
160
|
(isDemoted && meeting?.shareStatus === SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE) ||
|
|
@@ -128,34 +178,73 @@ const Webinar = WebexPlugin.extend({
|
|
|
128
178
|
|
|
129
179
|
/**
|
|
130
180
|
* Disconnects the practice session data channel and removes its relay listener.
|
|
181
|
+
* The listener reference removed here is the exact callback captured at subscribe
|
|
182
|
+
* time (see updatePSDataChannel) so that cleanup is correct even if the underlying
|
|
183
|
+
* meeting can no longer be resolved (e.g. locusUrl mismatch).
|
|
131
184
|
* @returns {Promise<void>}
|
|
132
185
|
*/
|
|
133
186
|
async cleanupPSDataChannel() {
|
|
187
|
+
const {isOwner} = this.webex.internal.llm.resolveSessionOwnership(
|
|
188
|
+
this.meetingId,
|
|
189
|
+
LLM_PRACTICE_SESSION
|
|
190
|
+
);
|
|
191
|
+
this.llmListeners = this.llmListeners || {};
|
|
192
|
+
|
|
134
193
|
if (this._pendingOnlineListener) {
|
|
135
194
|
// @ts-ignore - Fix type
|
|
136
195
|
this.webex.internal.llm.off('online', this._pendingOnlineListener);
|
|
137
196
|
this._pendingOnlineListener = null;
|
|
138
197
|
}
|
|
139
198
|
|
|
140
|
-
|
|
199
|
+
try {
|
|
200
|
+
// @ts-ignore - Fix type
|
|
201
|
+
const disconnected = await this.webex.internal.llm.disconnectLLM(
|
|
202
|
+
{
|
|
203
|
+
code: 3050,
|
|
204
|
+
reason: 'done (permanent)',
|
|
205
|
+
},
|
|
206
|
+
LLM_PRACTICE_SESSION,
|
|
207
|
+
this.meetingId
|
|
208
|
+
);
|
|
141
209
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
210
|
+
if (!disconnected) {
|
|
211
|
+
LoggerProxy.logger.info(
|
|
212
|
+
`Webinar:index#cleanupPSDataChannel --> skipping disconnect; practice-session LLM is not owned by meeting ${this.meetingId}`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
} catch (error) {
|
|
216
|
+
// disconnectLLM clears ownership only on success; release a stale owner
|
|
217
|
+
// tag here so other meeting instances can reclaim practice-session LLM.
|
|
218
|
+
if (isOwner) {
|
|
219
|
+
// @ts-ignore - Fix type
|
|
220
|
+
this.webex.internal.llm.setOwnerMeetingId?.(undefined, LLM_PRACTICE_SESSION);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
throw error;
|
|
224
|
+
} finally {
|
|
225
|
+
if (this._practiceSessionRelayListener) {
|
|
226
|
+
// @ts-ignore - Fix type
|
|
227
|
+
this.webex.internal.llm.off(
|
|
228
|
+
`event:relay.event:${LLM_PRACTICE_SESSION}`,
|
|
229
|
+
this._practiceSessionRelayListener
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
this._practiceSessionRelayListener = null;
|
|
233
|
+
|
|
234
|
+
for (const {event, listenerKey} of PS_LLM_EVENTS) {
|
|
235
|
+
if (this.llmListeners[listenerKey]) {
|
|
236
|
+
// @ts-ignore - Fix type
|
|
237
|
+
this.webex.internal.llm.off(event, this.llmListeners[listenerKey]);
|
|
238
|
+
this.llmListeners[listenerKey] = null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
155
242
|
},
|
|
156
243
|
|
|
157
244
|
/**
|
|
158
245
|
* Ensures practice-session token exists before registering the practice LLM channel.
|
|
246
|
+
* Caller is responsible for passing a meeting that has already been resolved via
|
|
247
|
+
* getValidatedWebinarMeeting() — this method does not re-validate ownership.
|
|
159
248
|
* @param {object} meeting
|
|
160
249
|
* @returns {Promise<string|undefined>}
|
|
161
250
|
*/
|
|
@@ -169,7 +258,8 @@ const Webinar = WebexPlugin.extend({
|
|
|
169
258
|
|
|
170
259
|
// @ts-ignore
|
|
171
260
|
const cachedToken = this.webex.internal.llm.getDatachannelToken(
|
|
172
|
-
|
|
261
|
+
LLM_PRACTICE_SESSION,
|
|
262
|
+
this.meetingId
|
|
173
263
|
);
|
|
174
264
|
|
|
175
265
|
if (cachedToken) {
|
|
@@ -187,7 +277,8 @@ const Webinar = WebexPlugin.extend({
|
|
|
187
277
|
// @ts-ignore
|
|
188
278
|
this.webex.internal.llm.setDatachannelToken(
|
|
189
279
|
datachannelToken,
|
|
190
|
-
dataChannelTokenType ||
|
|
280
|
+
dataChannelTokenType || LLM_PRACTICE_SESSION,
|
|
281
|
+
this.meetingId
|
|
191
282
|
);
|
|
192
283
|
|
|
193
284
|
return datachannelToken;
|
|
@@ -208,11 +299,17 @@ const Webinar = WebexPlugin.extend({
|
|
|
208
299
|
* @returns {Promise}
|
|
209
300
|
*/
|
|
210
301
|
async updatePSDataChannel() {
|
|
302
|
+
this.llmListeners = this.llmListeners || {};
|
|
303
|
+
|
|
211
304
|
this._updatePSDataChannelSequence = (this._updatePSDataChannelSequence || 0) + 1;
|
|
212
305
|
const invocationSequence = this._updatePSDataChannelSequence;
|
|
213
306
|
|
|
214
|
-
const meeting = this.
|
|
307
|
+
const meeting = this.getValidatedWebinarMeeting();
|
|
215
308
|
const isPracticeSession = meeting?.isJoined() && this.isJoinPracticeSessionDataChannel();
|
|
309
|
+
const {currentOwner, isOwner} = this.webex.internal.llm.resolveSessionOwnership(
|
|
310
|
+
this.meetingId,
|
|
311
|
+
LLM_PRACTICE_SESSION
|
|
312
|
+
);
|
|
216
313
|
|
|
217
314
|
if (!isPracticeSession) {
|
|
218
315
|
await this.cleanupPSDataChannel();
|
|
@@ -220,13 +317,22 @@ const Webinar = WebexPlugin.extend({
|
|
|
220
317
|
return undefined;
|
|
221
318
|
}
|
|
222
319
|
|
|
320
|
+
if (!isOwner) {
|
|
321
|
+
LoggerProxy.logger.info(
|
|
322
|
+
`Webinar:index#updatePSDataChannel --> skipping; practice-session LLM owned by meeting ${currentOwner}, not ${this.meetingId}`
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
223
328
|
// @ts-ignore - Fix type
|
|
224
329
|
const {url = undefined, info: {practiceSessionDatachannelUrl = undefined} = {}} =
|
|
225
330
|
meeting?.locusInfo || {};
|
|
226
331
|
|
|
227
332
|
// @ts-ignore
|
|
228
333
|
let practiceSessionDatachannelToken = this.webex.internal.llm.getDatachannelToken(
|
|
229
|
-
|
|
334
|
+
LLM_PRACTICE_SESSION,
|
|
335
|
+
this.meetingId
|
|
230
336
|
);
|
|
231
337
|
|
|
232
338
|
const isCaptionBoxOn = this.webex.internal.voicea.getIsCaptionBoxOn();
|
|
@@ -303,6 +409,30 @@ const Webinar = WebexPlugin.extend({
|
|
|
303
409
|
practiceSessionDatachannelToken = refreshedPracticeSessionToken;
|
|
304
410
|
}
|
|
305
411
|
|
|
412
|
+
const {currentOwner: currentOwnerBeforeConnect, isOwner: isOwnerBeforeConnect} =
|
|
413
|
+
this.webex.internal.llm.resolveSessionOwnership(this.meetingId, LLM_PRACTICE_SESSION);
|
|
414
|
+
|
|
415
|
+
if (!isOwnerBeforeConnect) {
|
|
416
|
+
LoggerProxy.logger.info(
|
|
417
|
+
`Webinar:index#updatePSDataChannel --> skipping pre-connect owner write; practice-session LLM owned by meeting ${currentOwnerBeforeConnect}, not ${this.meetingId}`
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
return undefined;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Ensure refresh for practice datachannel requests is routed to this
|
|
424
|
+
// meeting only when we are actually about to connect the practice session.
|
|
425
|
+
// This avoids claiming ownership in flows that return early (e.g. missing
|
|
426
|
+
// practiceSessionDatachannelUrl or waiting for default session online).
|
|
427
|
+
// @ts-ignore - Fix type
|
|
428
|
+
this.webex.internal.llm.setRefreshHandler(
|
|
429
|
+
() => meeting.refreshDataChannelToken(),
|
|
430
|
+
LLM_PRACTICE_SESSION,
|
|
431
|
+
this.meetingId
|
|
432
|
+
);
|
|
433
|
+
// @ts-ignore - Fix type
|
|
434
|
+
this.webex.internal.llm.setOwnerMeetingId?.(this.meetingId, LLM_PRACTICE_SESSION);
|
|
435
|
+
|
|
306
436
|
// @ts-ignore - Fix type
|
|
307
437
|
return this.webex.internal.llm
|
|
308
438
|
.registerAndConnect(
|
|
@@ -312,16 +442,30 @@ const Webinar = WebexPlugin.extend({
|
|
|
312
442
|
LLM_PRACTICE_SESSION
|
|
313
443
|
)
|
|
314
444
|
.then((registerAndConnectResult) => {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
445
|
+
const {currentOwner: currentOwnerAfterConnect, isOwner: isOwnerAfterConnect} =
|
|
446
|
+
this.webex.internal.llm.resolveSessionOwnership(this.meetingId, LLM_PRACTICE_SESSION);
|
|
447
|
+
|
|
448
|
+
if (this.meetingId && isOwnerAfterConnect) {
|
|
449
|
+
// @ts-ignore - Fix type
|
|
450
|
+
this.webex.internal.llm.setOwnerMeetingId?.(this.meetingId, LLM_PRACTICE_SESSION);
|
|
451
|
+
} else {
|
|
452
|
+
LoggerProxy.logger.info(
|
|
453
|
+
`Webinar:index#updatePSDataChannel --> skipping post-connect owner write; practice-session LLM owned by meeting ${currentOwnerAfterConnect}, not ${this.meetingId}`
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Track the exact listener references so cleanupPSDataChannel can
|
|
458
|
+
// unsubscribe deterministically, even if the meeting can no longer
|
|
459
|
+
// be resolved at cleanup time.
|
|
460
|
+
for (const {event, listenerKey, handlerKey} of PS_LLM_EVENTS) {
|
|
461
|
+
if (this.llmListeners[listenerKey]) {
|
|
462
|
+
// @ts-ignore - Fix type
|
|
463
|
+
this.webex.internal.llm.off(event, this.llmListeners[listenerKey]);
|
|
464
|
+
}
|
|
465
|
+
this.llmListeners[listenerKey] = meeting?.[handlerKey];
|
|
466
|
+
// @ts-ignore - Fix type
|
|
467
|
+
this.webex.internal.llm.on(event, this.llmListeners[listenerKey]);
|
|
468
|
+
}
|
|
325
469
|
// @ts-ignore - Fix type
|
|
326
470
|
this.webex.internal.voicea?.announce?.();
|
|
327
471
|
if (isCaptionBoxOn) {
|
|
@@ -332,6 +476,23 @@ const Webinar = WebexPlugin.extend({
|
|
|
332
476
|
);
|
|
333
477
|
|
|
334
478
|
return Promise.resolve(registerAndConnectResult);
|
|
479
|
+
})
|
|
480
|
+
.catch((error) => {
|
|
481
|
+
const {
|
|
482
|
+
currentOwner: currentOwnerAfterRegisterFailure,
|
|
483
|
+
isOwner: isOwnerAfterRegisterFailure,
|
|
484
|
+
} = this.webex.internal.llm.resolveSessionOwnership(this.meetingId, LLM_PRACTICE_SESSION);
|
|
485
|
+
|
|
486
|
+
if (isOwnerAfterRegisterFailure) {
|
|
487
|
+
// @ts-ignore - Fix type
|
|
488
|
+
this.webex.internal.llm.setOwnerMeetingId?.(undefined, LLM_PRACTICE_SESSION);
|
|
489
|
+
} else {
|
|
490
|
+
LoggerProxy.logger.info(
|
|
491
|
+
`Webinar:index#updatePSDataChannel --> skipping failure owner release; practice-session LLM owned by meeting ${currentOwnerAfterRegisterFailure}, not ${this.meetingId}`
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
throw error;
|
|
335
496
|
});
|
|
336
497
|
},
|
|
337
498
|
|
|
@@ -341,6 +502,8 @@ const Webinar = WebexPlugin.extend({
|
|
|
341
502
|
* @returns {Promise}
|
|
342
503
|
*/
|
|
343
504
|
setPracticeSessionState(enabled) {
|
|
505
|
+
const meeting = this.getValidatedWebinarMeeting();
|
|
506
|
+
|
|
344
507
|
return this.request({
|
|
345
508
|
method: HTTP_VERBS.PATCH,
|
|
346
509
|
uri: `${this.locusUrl}/controls`,
|
|
@@ -349,10 +512,16 @@ const Webinar = WebexPlugin.extend({
|
|
|
349
512
|
enabled,
|
|
350
513
|
},
|
|
351
514
|
},
|
|
352
|
-
})
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
515
|
+
})
|
|
516
|
+
.then((response) => {
|
|
517
|
+
MeetingUtil.updateLocusFromApiResponse(meeting, response);
|
|
518
|
+
|
|
519
|
+
return response;
|
|
520
|
+
})
|
|
521
|
+
.catch((error) => {
|
|
522
|
+
LoggerProxy.logger.error('Meeting:webinar#setPracticeSessionState failed', error);
|
|
523
|
+
throw error;
|
|
524
|
+
});
|
|
356
525
|
},
|
|
357
526
|
|
|
358
527
|
/**
|
|
@@ -532,7 +701,14 @@ const Webinar = WebexPlugin.extend({
|
|
|
532
701
|
* @returns {Promise}
|
|
533
702
|
*/
|
|
534
703
|
async searchLargeScaleWebinarAttendees(payload) {
|
|
535
|
-
const meeting = this.
|
|
704
|
+
const meeting = this.getValidatedWebinarMeeting();
|
|
705
|
+
if (!meeting) {
|
|
706
|
+
LoggerProxy.logger.error(
|
|
707
|
+
'Meeting:webinar5k#searchLargeScaleWebinarAttendees failed --> webinar meeting could not be validated'
|
|
708
|
+
);
|
|
709
|
+
throw new Error('Meeting:webinar5k#Webinar meeting is not resolvable for the current locus');
|
|
710
|
+
}
|
|
711
|
+
|
|
536
712
|
const rawParams = {
|
|
537
713
|
search_text: payload?.queryString,
|
|
538
714
|
limit: payload?.limit ?? DEFAULT_LARGE_SCALE_WEBINAR_ATTENDEE_SEARCH_LIMIT,
|
|
@@ -540,7 +716,9 @@ const Webinar = WebexPlugin.extend({
|
|
|
540
716
|
};
|
|
541
717
|
const attendeeSearchUrl = meeting?.locusInfo?.links?.resources?.attendeeSearch?.url;
|
|
542
718
|
if (!attendeeSearchUrl) {
|
|
543
|
-
LoggerProxy.logger.error(
|
|
719
|
+
LoggerProxy.logger.error(
|
|
720
|
+
'Meeting:webinar5k#searchLargeScaleWebinarAttendees failed --> attendee search url unavailable'
|
|
721
|
+
);
|
|
544
722
|
throw new Error('Meeting:webinar5k#Attendee search url is not available');
|
|
545
723
|
}
|
|
546
724
|
|
|
@@ -55,6 +55,27 @@ describe('plugin-meetings', () => {
|
|
|
55
55
|
});
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
describe('#locusUrlUpdate', () => {
|
|
59
|
+
it('should update the locusUrl property', () => {
|
|
60
|
+
const testLocusUrl = 'https://locus-a.wbx2.com/locus/api/v1/loci/test-id';
|
|
61
|
+
|
|
62
|
+
aiEnableRequest.locusUrlUpdate(testLocusUrl);
|
|
63
|
+
|
|
64
|
+
assert.equal(aiEnableRequest.locusUrl, testLocusUrl);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should handle updating locusUrl multiple times', () => {
|
|
68
|
+
const firstUrl = 'https://locus-a.wbx2.com/locus/api/v1/loci/test-id-1';
|
|
69
|
+
const secondUrl = 'https://locus-a.wbx2.com/locus/api/v1/loci/test-id-2';
|
|
70
|
+
|
|
71
|
+
aiEnableRequest.locusUrlUpdate(firstUrl);
|
|
72
|
+
assert.equal(aiEnableRequest.locusUrl, firstUrl);
|
|
73
|
+
|
|
74
|
+
aiEnableRequest.locusUrlUpdate(secondUrl);
|
|
75
|
+
assert.equal(aiEnableRequest.locusUrl, secondUrl);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
58
79
|
describe('#selfParticipantIdUpdate', () => {
|
|
59
80
|
it('should update the selfParticipantId property', () => {
|
|
60
81
|
const testSelfParticipantId = 'participant-123';
|
|
@@ -254,6 +275,71 @@ describe('plugin-meetings', () => {
|
|
|
254
275
|
sinon.assert.notCalled(triggerSpy);
|
|
255
276
|
});
|
|
256
277
|
|
|
278
|
+
it('should not trigger event when locusUrl does not match', () => {
|
|
279
|
+
const testLocusUrl = 'https://locus-a.wbx2.com/locus/api/v1/loci/test-id';
|
|
280
|
+
const differentLocusUrl = 'https://locus-a.wbx2.com/locus/api/v1/loci/different-id';
|
|
281
|
+
|
|
282
|
+
aiEnableRequest.locusUrl = testLocusUrl;
|
|
283
|
+
|
|
284
|
+
// Reset the spy after setting locusUrl to avoid counting property change events
|
|
285
|
+
triggerSpy.resetHistory();
|
|
286
|
+
|
|
287
|
+
aiEnableRequest.listenToApprovalRequests();
|
|
288
|
+
|
|
289
|
+
const event = {
|
|
290
|
+
data: {
|
|
291
|
+
locusUrl: differentLocusUrl,
|
|
292
|
+
approval: {
|
|
293
|
+
resourceType: AI_ENABLE_REQUEST.RESOURCE_TYPE,
|
|
294
|
+
receivers: [{participantId: testSelfParticipantId}],
|
|
295
|
+
initiator: {participantId: testInitiatorId},
|
|
296
|
+
actionType: AI_ENABLE_REQUEST.ACTION_TYPE.REQUESTED,
|
|
297
|
+
url: testUrl,
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
webex.internal.mercury.emit(`event:${LOCUSEVENT.APPROVAL_REQUEST}`, event);
|
|
303
|
+
|
|
304
|
+
sinon.assert.notCalled(triggerSpy);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should trigger event when locusUrl matches', () => {
|
|
308
|
+
const testLocusUrl = 'https://locus-a.wbx2.com/locus/api/v1/loci/test-id';
|
|
309
|
+
|
|
310
|
+
aiEnableRequest.locusUrl = testLocusUrl;
|
|
311
|
+
|
|
312
|
+
// Reset the spy after setting locusUrl to avoid counting property change events
|
|
313
|
+
triggerSpy.resetHistory();
|
|
314
|
+
|
|
315
|
+
aiEnableRequest.listenToApprovalRequests();
|
|
316
|
+
|
|
317
|
+
const event = {
|
|
318
|
+
data: {
|
|
319
|
+
locusUrl: testLocusUrl,
|
|
320
|
+
approval: {
|
|
321
|
+
resourceType: AI_ENABLE_REQUEST.RESOURCE_TYPE,
|
|
322
|
+
receivers: [{participantId: testSelfParticipantId}],
|
|
323
|
+
initiator: {participantId: testInitiatorId},
|
|
324
|
+
actionType: AI_ENABLE_REQUEST.ACTION_TYPE.REQUESTED,
|
|
325
|
+
url: testUrl,
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
webex.internal.mercury.emit(`event:${LOCUSEVENT.APPROVAL_REQUEST}`, event);
|
|
331
|
+
|
|
332
|
+
sinon.assert.calledOnce(triggerSpy);
|
|
333
|
+
sinon.assert.calledWith(triggerSpy, AI_ENABLE_REQUEST.EVENTS.APPROVAL_REQUEST_ARRIVED, {
|
|
334
|
+
actionType: AI_ENABLE_REQUEST.ACTION_TYPE.REQUESTED,
|
|
335
|
+
isApprover: true,
|
|
336
|
+
isInitiator: false,
|
|
337
|
+
initiatorId: testInitiatorId,
|
|
338
|
+
approverId: testSelfParticipantId,
|
|
339
|
+
url: testUrl,
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
257
343
|
it('should handle events with different action types', () => {
|
|
258
344
|
aiEnableRequest.listenToApprovalRequests();
|
|
259
345
|
|
|
@@ -26,6 +26,7 @@ describe('plugin-meetings', () => {
|
|
|
26
26
|
breakout.sessionId = 'sessionId';
|
|
27
27
|
breakout.sessionType = 'BREAKOUT';
|
|
28
28
|
breakout.url = 'url';
|
|
29
|
+
breakout.resourceLink = 'resource-link';
|
|
29
30
|
breakout.collection = {
|
|
30
31
|
parent: {
|
|
31
32
|
meetingId: 'activeMeetingId',
|
|
@@ -45,6 +46,7 @@ describe('plugin-meetings', () => {
|
|
|
45
46
|
describe('initialize', () => {
|
|
46
47
|
it('creates the object correctly', () => {
|
|
47
48
|
assert.instanceOf(breakout.breakoutRequest, BreakoutRequest);
|
|
49
|
+
assert.equal(breakout.resourceLink, 'resource-link');
|
|
48
50
|
});
|
|
49
51
|
});
|
|
50
52
|
|
|
@@ -217,10 +219,14 @@ describe('plugin-meetings', () => {
|
|
|
217
219
|
locusParticipantsUpdate: sinon.stub(),
|
|
218
220
|
};
|
|
219
221
|
|
|
220
|
-
const locusData = {
|
|
222
|
+
const locusData = {participants: [{id: 'participant-1'}], sequence: {entries: [123]}};
|
|
221
223
|
const result = breakout.parseRoster(locusData);
|
|
222
224
|
|
|
223
|
-
assert.calledOnceWithExactly(breakout.members.locusParticipantsUpdate,
|
|
225
|
+
assert.calledOnceWithExactly(breakout.members.locusParticipantsUpdate, {
|
|
226
|
+
participants: [{id: 'participant-1'}],
|
|
227
|
+
isReplace: true,
|
|
228
|
+
});
|
|
229
|
+
assert.equal(breakout.breakoutRosterLocus, locusData);
|
|
224
230
|
assert.equal(result, undefined);
|
|
225
231
|
});
|
|
226
232
|
it('not call locusParticipantsUpdate if sequence is expired', () => {
|
|
@@ -228,7 +234,7 @@ describe('plugin-meetings', () => {
|
|
|
228
234
|
locusParticipantsUpdate: sinon.stub(),
|
|
229
235
|
};
|
|
230
236
|
breakout.isNeedHandleRoster = sinon.stub().returns(false);
|
|
231
|
-
const locusData = {
|
|
237
|
+
const locusData = {participants: [{id: 'participant-1'}], sequence: {entries: [123]}};
|
|
232
238
|
breakout.parseRoster(locusData);
|
|
233
239
|
|
|
234
240
|
assert.notCalled(breakout.members.locusParticipantsUpdate);
|
|
@@ -313,6 +313,7 @@ describe('plugin-meetings', () => {
|
|
|
313
313
|
groupId: 'groupId',
|
|
314
314
|
sessionType: 'sessionType',
|
|
315
315
|
url: 'url',
|
|
316
|
+
resourceLink: 'resource-link',
|
|
316
317
|
name: 'name',
|
|
317
318
|
allowBackToMain: true,
|
|
318
319
|
delayCloseTime: 10,
|
|
@@ -339,6 +340,7 @@ describe('plugin-meetings', () => {
|
|
|
339
340
|
assert.equal(breakouts.currentBreakoutSession.current, true);
|
|
340
341
|
assert.equal(breakouts.currentBreakoutSession.sessionType, 'sessionType');
|
|
341
342
|
assert.equal(breakouts.currentBreakoutSession.url, 'url');
|
|
343
|
+
assert.equal(breakouts.currentBreakoutSession.resourceLink, 'resource-link');
|
|
342
344
|
assert.equal(breakouts.currentBreakoutSession.active, false);
|
|
343
345
|
assert.equal(breakouts.currentBreakoutSession.allowed, false);
|
|
344
346
|
assert.equal(breakouts.currentBreakoutSession.assigned, false);
|
|
@@ -1847,6 +1849,53 @@ describe('plugin-meetings', () => {
|
|
|
1847
1849
|
});
|
|
1848
1850
|
});
|
|
1849
1851
|
|
|
1852
|
+
describe('#removeFromBreakout', () => {
|
|
1853
|
+
it('should make a POST request with correct body and return the result', async () => {
|
|
1854
|
+
breakouts.request = sinon.stub().returns(Promise.resolve('REQUEST_RETURN_VALUE'));
|
|
1855
|
+
breakouts.set('url', 'url');
|
|
1856
|
+
breakouts.set('mainGroupId', 'mainGroupId');
|
|
1857
|
+
breakouts.set('mainSessionId', 'mainSessionId');
|
|
1858
|
+
|
|
1859
|
+
const participants = ['participant1', 'participant2'];
|
|
1860
|
+
const result = await breakouts.removeFromBreakout(participants);
|
|
1861
|
+
|
|
1862
|
+
assert.calledOnceWithExactly(breakouts.request, {
|
|
1863
|
+
method: 'POST',
|
|
1864
|
+
uri: 'url/move',
|
|
1865
|
+
body: {
|
|
1866
|
+
groups: [
|
|
1867
|
+
{
|
|
1868
|
+
id: 'mainGroupId',
|
|
1869
|
+
sessions: [
|
|
1870
|
+
{
|
|
1871
|
+
id: 'mainSessionId',
|
|
1872
|
+
participants,
|
|
1873
|
+
},
|
|
1874
|
+
],
|
|
1875
|
+
},
|
|
1876
|
+
],
|
|
1877
|
+
},
|
|
1878
|
+
});
|
|
1879
|
+
assert.equal(result, 'REQUEST_RETURN_VALUE');
|
|
1880
|
+
});
|
|
1881
|
+
|
|
1882
|
+
it('should throw an error if mainGroupId is missing', () => {
|
|
1883
|
+
breakouts.set('mainSessionId', 'mainSessionId');
|
|
1884
|
+
assert.throws(
|
|
1885
|
+
() => breakouts.removeFromBreakout(['participant1']),
|
|
1886
|
+
'Main group ID and session ID must be available to remove participants from breakout'
|
|
1887
|
+
);
|
|
1888
|
+
});
|
|
1889
|
+
|
|
1890
|
+
it('should throw an error if mainSessionId is missing', () => {
|
|
1891
|
+
breakouts.set('mainGroupId', 'mainGroupId');
|
|
1892
|
+
assert.throws(
|
|
1893
|
+
() => breakouts.removeFromBreakout(['participant1']),
|
|
1894
|
+
'Main group ID and session ID must be available to remove participants from breakout'
|
|
1895
|
+
);
|
|
1896
|
+
});
|
|
1897
|
+
});
|
|
1898
|
+
|
|
1850
1899
|
describe('#triggerReturnToMainEvent', () => {
|
|
1851
1900
|
const checkTrigger = ({breakout, shouldTrigger}) => {
|
|
1852
1901
|
breakouts.trigger = sinon.stub();
|