@webex/plugin-meetings 3.8.0-next.82 → 3.8.0-next.84

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.
@@ -1,18 +1,20 @@
1
1
  /*!
2
2
  * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
3
  */
4
- import {MEETINGS, _IN_LOBBY_, _NOT_IN_MEETING_, _IN_MEETING_} from '../constants';
5
- import {IExternalRoles, IMediaStatus, ParticipantWithBrb, ParticipantWithRoles} from './types';
4
+ import {MEETINGS, _IN_LOBBY_, _NOT_IN_MEETING_, _IN_MEETING_, _OBSERVE_} from '../constants';
5
+ import {IExternalRoles, IMediaStatus, Participant, ParticipantUrl} from './types';
6
6
 
7
7
  import MemberUtil from './util';
8
8
 
9
+ export type MemberId = string;
9
10
  /**
10
11
  * @class Member
11
12
  */
12
13
  export default class Member {
13
- associatedUser: any;
14
+ associatedUser: MemberId | null; // deprecated, use associatedUsers instead
15
+ associatedUsers: Set<MemberId>; // users associated with this device, empty if this member is not a device
14
16
  canReclaimHost: boolean;
15
- id: any;
17
+ id: MemberId;
16
18
  isAudioMuted: any;
17
19
  isContentSharing: any;
18
20
  isDevice: any;
@@ -29,6 +31,7 @@ export default class Member {
29
31
  isRecording: any;
30
32
  isRemovable: any;
31
33
  isSelf: any;
34
+ isPairedWithSelf: boolean; // true for a device that we are paired with
32
35
  isBrb: boolean;
33
36
  isUser: any;
34
37
  isVideoMuted: any;
@@ -42,6 +45,10 @@ export default class Member {
42
45
  supportLiveAnnotation: boolean;
43
46
  type: any;
44
47
  namespace = MEETINGS;
48
+ pairedWith: {
49
+ participantUrl?: ParticipantUrl;
50
+ memberId?: MemberId;
51
+ };
45
52
 
46
53
  /**
47
54
  * @param {Object} participant - the locus participant
@@ -54,7 +61,7 @@ export default class Member {
54
61
  * @memberof Member
55
62
  */
56
63
  constructor(
57
- participant: object,
64
+ participant: Participant,
58
65
  options:
59
66
  | {
60
67
  selfId: string;
@@ -201,13 +208,23 @@ export default class Member {
201
208
  */
202
209
  this.isUser = null;
203
210
  /**
211
+ * Deprecated: use associatedUsers instead
204
212
  * Is this member associated to another user by way of pairing (typical of devices)
205
213
  * @instance
206
- * @type {String}
214
+ * @type {MemberId|null}
215
+ * @deprecated
207
216
  * @public
208
217
  * @memberof Member
209
218
  */
210
219
  this.associatedUser = null;
220
+ /**
221
+ * Is this member associated to another user by way of pairing (typical of devices)
222
+ * @instance
223
+ * @type {String}
224
+ * @public
225
+ * @memberof Member
226
+ */
227
+ this.associatedUsers = new Set<MemberId>();
211
228
  /**
212
229
  * @instance
213
230
  * @type {Boolean}
@@ -266,6 +283,14 @@ export default class Member {
266
283
  */
267
284
  this.isPresenterAssignmentProhibited = null;
268
285
 
286
+ /**
287
+ * @instance
288
+ * @type {Boolean}
289
+ * @public
290
+ * @memberof Member
291
+ */
292
+ this.isPairedWithSelf = false;
293
+
269
294
  /**
270
295
  * @instance
271
296
  * @type {IExternalRoles}
@@ -274,6 +299,10 @@ export default class Member {
274
299
  */
275
300
  this.roles = null;
276
301
 
302
+ this.pairedWith = {
303
+ participantUrl: undefined,
304
+ memberId: undefined,
305
+ };
277
306
  /**
278
307
  * @instance
279
308
  * @type {IMediaStatus}
@@ -299,9 +328,10 @@ export default class Member {
299
328
  * @private
300
329
  * @memberof Member
301
330
  */
302
- private processParticipant(participant: object) {
331
+ private processParticipant(participant: Participant) {
303
332
  this.participant = participant;
304
333
  if (participant) {
334
+ this.processPairedDevice(participant);
305
335
  this.canReclaimHost = MemberUtil.canReclaimHost(participant);
306
336
  this.id = MemberUtil.extractId(participant);
307
337
  this.name = MemberUtil.extractName(participant);
@@ -312,7 +342,7 @@ export default class Member {
312
342
  this.supportsInterpretation = MemberUtil.isInterpretationSupported(participant);
313
343
  this.supportLiveAnnotation = MemberUtil.isLiveAnnotationSupported(participant);
314
344
  this.isGuest = MemberUtil.isGuest(participant);
315
- this.isBrb = MemberUtil.isBrb(participant as ParticipantWithBrb);
345
+ this.isBrb = MemberUtil.isBrb(participant);
316
346
  this.isUser = MemberUtil.isUser(participant);
317
347
  this.isDevice = MemberUtil.isDevice(participant);
318
348
  this.isModerator = MemberUtil.isModerator(participant);
@@ -321,12 +351,24 @@ export default class Member {
321
351
  this.isPresenterAssignmentProhibited =
322
352
  MemberUtil.isPresenterAssignmentProhibited(participant);
323
353
  this.processStatus(participant);
324
- this.processRoles(participant as ParticipantWithRoles);
354
+ this.processRoles(participant);
325
355
  // must be done last
326
356
  this.isNotAdmitted = MemberUtil.isNotAdmitted(participant, this.isGuest, this.status);
327
357
  }
328
358
  }
329
359
 
360
+ /**
361
+ * Checks if the participant is paired with another device
362
+ *
363
+ * @param {any} participant the locus participant object
364
+ * @returns {void}
365
+ */
366
+ processPairedDevice(participant: Participant) {
367
+ // we can't populate this.pairedWith.memberId here because the member for that device might not yet exist
368
+ // so only populating the participantUrl and memberId will be set later
369
+ this.pairedWith.participantUrl = MemberUtil.extractPairedWithParticipantUrl(participant);
370
+ }
371
+
330
372
  /**
331
373
  * Use the members options and participant values to set on the member
332
374
  * @param {Object} participant the locus participant object
@@ -335,7 +377,7 @@ export default class Member {
335
377
  * @private
336
378
  * @memberof Member
337
379
  */
338
- private processParticipantOptions(participant: object, options: any) {
380
+ private processParticipantOptions(participant: Participant, options: any) {
339
381
  if (participant && options) {
340
382
  this.processIsSelf(participant, options.selfId);
341
383
  this.processIsHost(participant, options.hostId);
@@ -378,7 +420,7 @@ export default class Member {
378
420
  * @private
379
421
  * @memberof Member
380
422
  */
381
- private processStatus(participant: object) {
423
+ private processStatus(participant: Participant) {
382
424
  this.status = MemberUtil.extractStatus(participant);
383
425
  switch (this.status) {
384
426
  case _IN_LOBBY_:
@@ -440,11 +482,9 @@ export default class Member {
440
482
  * @public
441
483
  * @memberof Member
442
484
  */
443
- public processIsContentSharing(participant: object, sharingId: string) {
485
+ public processIsContentSharing(participant: Participant, sharingId: string) {
444
486
  if (MemberUtil.isUser(participant)) {
445
487
  this.isContentSharing = MemberUtil.isSame(participant, sharingId);
446
- } else if (MemberUtil.isDevice(participant)) {
447
- this.isContentSharing = MemberUtil.isAssociatedSame(participant, sharingId);
448
488
  }
449
489
  }
450
490
 
@@ -456,7 +496,7 @@ export default class Member {
456
496
  * @public
457
497
  * @memberof Member
458
498
  */
459
- public processIsRecording(participant: object, recordingId: string) {
499
+ public processIsRecording(participant: Participant, recordingId: string) {
460
500
  this.isRecording = MemberUtil.isSame(participant, recordingId);
461
501
  }
462
502
 
@@ -468,12 +508,9 @@ export default class Member {
468
508
  * @private
469
509
  * @memberof Member
470
510
  */
471
- private processIsSelf(participant: object, selfId: string) {
511
+ private processIsSelf(participant: Participant, selfId: string) {
472
512
  if (MemberUtil.isUser(participant)) {
473
513
  this.isSelf = MemberUtil.isSame(participant, selfId);
474
- } else if (MemberUtil.isDevice(participant)) {
475
- this.isSelf = MemberUtil.isAssociatedSame(participant, selfId);
476
- this.associatedUser = selfId;
477
514
  }
478
515
  }
479
516
 
@@ -485,11 +522,9 @@ export default class Member {
485
522
  * @private
486
523
  * @memberof Member
487
524
  */
488
- private processIsHost(participant: object, hostId: string) {
525
+ private processIsHost(participant: Participant, hostId: string) {
489
526
  if (MemberUtil.isUser(participant)) {
490
527
  this.isHost = MemberUtil.isSame(participant, hostId);
491
- } else if (MemberUtil.isDevice(participant)) {
492
- this.isHost = MemberUtil.isAssociatedSame(participant, hostId);
493
528
  }
494
529
  }
495
530
 
@@ -500,7 +535,7 @@ export default class Member {
500
535
  * @private
501
536
  * @memberof Member
502
537
  */
503
- private processRoles(participant: ParticipantWithRoles) {
538
+ private processRoles(participant: Participant) {
504
539
  this.roles = MemberUtil.extractControlRoles(participant);
505
540
  }
506
541
 
@@ -15,22 +15,6 @@ export type ServerRoleShape = {
15
15
  hasRole: boolean;
16
16
  };
17
17
 
18
- export type ParticipantWithRoles = {
19
- controls: {
20
- role: {
21
- roles: Array<ServerRoleShape>;
22
- };
23
- };
24
- };
25
-
26
- export type ParticipantWithBrb = {
27
- controls: {
28
- brb?: {
29
- enabled: boolean;
30
- };
31
- };
32
- };
33
-
34
18
  // values are inherited from locus so don't update these
35
19
  export enum MediaStatus {
36
20
  RECVONLY = 'RECVONLY', // participant only receiving and not sending
@@ -44,3 +28,85 @@ export interface IMediaStatus {
44
28
  audio: MediaStatus;
45
29
  video: MediaStatus;
46
30
  }
31
+
32
+ export type Csi = number;
33
+ export type Direction = 'inactive' | 'sendrecv' | 'sendonly' | 'recvonly';
34
+ export type ParticipantUrl = string;
35
+ export interface MediaSession {
36
+ csi: Csi;
37
+ direction: Direction;
38
+ mediaContent: 'main' | 'slides';
39
+ mediaType: 'audio' | 'video';
40
+ state: string;
41
+ }
42
+
43
+ export interface Intent {
44
+ associatedWith: ParticipantUrl;
45
+ id: string;
46
+ type: string; // could be "WAIT" or "OBSERVE" or other....
47
+ }
48
+ export interface ParticipantDevice {
49
+ correlationId: string;
50
+ csis: Csi[];
51
+ deviceType: string; // WDM device type, could be "WEB", "TP_ENDPOINT", "MAC" or other things, don't know the full list, so keeping it as string
52
+ intent?: Intent;
53
+ intents: Array<Intent | null>;
54
+ isVideoCallback: boolean;
55
+ mediaSessions: Array<MediaSession>;
56
+ mediaSessionsExternal: boolean;
57
+ state: string; // probably one of MEETING_STATE.STATES
58
+ }
59
+
60
+ // this is not a complete type, Locus may send more fields
61
+ export interface ParticipantPerson {
62
+ id: string;
63
+ isExternal: boolean;
64
+ name: string;
65
+ orgId: string;
66
+ }
67
+
68
+ export interface ParticipantMediaStatus {
69
+ audioStatus: MediaStatus;
70
+ videoStatus: MediaStatus;
71
+ audioSlidesStatus?: MediaStatus;
72
+ videoSlidesStatus?: MediaStatus;
73
+ csis: Csi[];
74
+ }
75
+
76
+ // this is not a complete type, Locus may send more fields
77
+ export interface ParticipantControls {
78
+ role: {
79
+ roles: Array<ServerRoleShape>;
80
+ };
81
+ brb?: {
82
+ enabled: boolean;
83
+ };
84
+ hand: {
85
+ raised: boolean;
86
+ };
87
+ localRecord: {
88
+ recording: boolean;
89
+ };
90
+ }
91
+
92
+ // this is not a complete type, Locus may send more fields
93
+ export interface Participant {
94
+ canBeController: boolean;
95
+ controls: ParticipantControls;
96
+ deviceUrl: string;
97
+ devices: Array<ParticipantDevice>;
98
+ guest: boolean;
99
+ id: string;
100
+ identity: string;
101
+ identityTrustLevel: string; // could be 'INTERNAL', 'EXTERNAL' or other....
102
+ isCreator: boolean;
103
+ moderator: boolean; // Locus docs say this is deprecated and role control should be used instead
104
+ moderatorAssignmentNotAllowed: boolean;
105
+ presenterAssignmentNotAllowed: boolean;
106
+ person: ParticipantPerson;
107
+ resourceGuest: boolean;
108
+ state: string; // probably one of MEETING_STATE.STATES
109
+ status: ParticipantMediaStatus;
110
+ type: string;
111
+ url: ParticipantUrl;
112
+ }
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  IExternalRoles,
3
- ParticipantWithRoles,
4
3
  ServerRoles,
5
4
  ServerRoleShape,
6
5
  IMediaStatus,
7
- ParticipantWithBrb,
6
+ Participant,
7
+ ParticipantUrl,
8
8
  } from './types';
9
9
  import {
10
10
  _USER_,
@@ -46,23 +46,23 @@ const MemberUtil = {
46
46
  * @param {Object} participant - The locus participant object.
47
47
  * @returns {[ServerRoleShape]}
48
48
  */
49
- getControlsRoles: (participant: ParticipantWithRoles): Array<ServerRoleShape> =>
49
+ getControlsRoles: (participant: Participant): Array<ServerRoleShape> =>
50
50
  participant?.controls?.role?.roles,
51
51
 
52
52
  /**
53
53
  * Checks if the participant has the brb status enabled.
54
54
  *
55
- * @param {ParticipantWithBrb} participant - The locus participant object.
55
+ * @param {Participant} participant - The locus participant object.
56
56
  * @returns {boolean} - True if the participant has brb enabled, false otherwise.
57
57
  */
58
- isBrb: (participant: ParticipantWithBrb): boolean => participant.controls?.brb?.enabled || false,
58
+ isBrb: (participant: Participant): boolean => participant.controls?.brb?.enabled || false,
59
59
 
60
60
  /**
61
61
  * @param {Object} participant - The locus participant object.
62
62
  * @param {ServerRoles} controlRole the search role
63
63
  * @returns {Boolean}
64
64
  */
65
- hasRole: (participant: any, controlRole: ServerRoles): boolean =>
65
+ hasRole: (participant: Participant, controlRole: ServerRoles): boolean =>
66
66
  MemberUtil.getControlsRoles(participant)?.some(
67
67
  (role) => role.type === controlRole && role.hasRole
68
68
  ),
@@ -71,28 +71,28 @@ const MemberUtil = {
71
71
  * @param {Object} participant - The locus participant object.
72
72
  * @returns {Boolean}
73
73
  */
74
- hasCohost: (participant: ParticipantWithRoles): boolean =>
74
+ hasCohost: (participant: Participant): boolean =>
75
75
  MemberUtil.hasRole(participant, ServerRoles.Cohost) || false,
76
76
 
77
77
  /**
78
78
  * @param {Object} participant - The locus participant object.
79
79
  * @returns {Boolean}
80
80
  */
81
- hasModerator: (participant: ParticipantWithRoles): boolean =>
81
+ hasModerator: (participant: Participant): boolean =>
82
82
  MemberUtil.hasRole(participant, ServerRoles.Moderator) || false,
83
83
 
84
84
  /**
85
85
  * @param {Object} participant - The locus participant object.
86
86
  * @returns {Boolean}
87
87
  */
88
- hasPresenter: (participant: ParticipantWithRoles): boolean =>
88
+ hasPresenter: (participant: Participant): boolean =>
89
89
  MemberUtil.hasRole(participant, ServerRoles.Presenter) || false,
90
90
 
91
91
  /**
92
92
  * @param {Object} participant - The locus participant object.
93
93
  * @returns {IExternalRoles}
94
94
  */
95
- extractControlRoles: (participant: ParticipantWithRoles): IExternalRoles => {
95
+ extractControlRoles: (participant: Participant): IExternalRoles => {
96
96
  const roles = {
97
97
  cohost: MemberUtil.hasCohost(participant),
98
98
  moderator: MemberUtil.hasModerator(participant),
@@ -106,26 +106,26 @@ const MemberUtil = {
106
106
  * @param {Object} participant - The locus participant object.
107
107
  * @returns {Boolean}
108
108
  */
109
- isUser: (participant: any) => participant && participant.type === _USER_,
109
+ isUser: (participant: Participant) => participant && participant.type === _USER_,
110
110
 
111
- isModerator: (participant) => participant && participant.moderator,
111
+ isModerator: (participant: Participant) => participant && participant.moderator,
112
112
 
113
113
  /**
114
114
  * @param {Object} participant - The locus participant object.
115
115
  * @returns {Boolean}
116
116
  */
117
- isGuest: (participant: any) => participant && participant.guest,
117
+ isGuest: (participant: Participant) => participant && participant.guest,
118
118
 
119
119
  /**
120
120
  * @param {Object} participant - The locus participant object.
121
121
  * @returns {Boolean}
122
122
  */
123
- isDevice: (participant: any) => participant && participant.type === _RESOURCE_ROOM_,
123
+ isDevice: (participant: Participant) => participant && participant.type === _RESOURCE_ROOM_,
124
124
 
125
- isModeratorAssignmentProhibited: (participant) =>
125
+ isModeratorAssignmentProhibited: (participant: Participant) =>
126
126
  participant && participant.moderatorAssignmentNotAllowed,
127
127
 
128
- isPresenterAssignmentProhibited: (participant) =>
128
+ isPresenterAssignmentProhibited: (participant: Participant) =>
129
129
  participant && participant.presenterAssignmentNotAllowed,
130
130
 
131
131
  /**
@@ -135,30 +135,16 @@ const MemberUtil = {
135
135
  * @param {String} id
136
136
  * @returns {Boolean}
137
137
  */
138
- isSame: (participant: any, id: string) =>
138
+ isSame: (participant: Participant, id: string) =>
139
139
  participant && (participant.id === id || (participant.person && participant.person.id === id)),
140
140
 
141
- /**
142
- * checks to see if the participant id is the same as the passed id for associated devices
143
- * there are multiple ids that can be used
144
- * @param {Object} participant - The locus participant object.
145
- * @param {String} id
146
- * @returns {Boolean}
147
- */
148
- isAssociatedSame: (participant: any, id: string) =>
149
- participant &&
150
- participant.associatedUsers &&
151
- participant.associatedUsers.some(
152
- (user) => user.id === id || (user.person && user.person.id === id)
153
- ),
154
-
155
141
  /**
156
142
  * @param {Object} participant - The locus participant object.
157
143
  * @param {Boolean} isGuest
158
144
  * @param {String} status
159
145
  * @returns {Boolean}
160
146
  */
161
- isNotAdmitted: (participant: any, isGuest: boolean, status: string): boolean =>
147
+ isNotAdmitted: (participant: Participant, isGuest: boolean, status: string): boolean =>
162
148
  participant &&
163
149
  participant.guest &&
164
150
  ((participant.devices &&
@@ -175,7 +161,7 @@ const MemberUtil = {
175
161
  * @param {Object} participant - The locus participant object.
176
162
  * @returns {Boolean}
177
163
  */
178
- isAudioMuted: (participant: any) => {
164
+ isAudioMuted: (participant: Participant) => {
179
165
  if (!participant) {
180
166
  throw new ParameterError('Audio could not be processed, participant is undefined.');
181
167
  }
@@ -187,7 +173,7 @@ const MemberUtil = {
187
173
  * @param {Object} participant - The locus participant object.
188
174
  * @returns {Boolean}
189
175
  */
190
- isVideoMuted: (participant: any): boolean => {
176
+ isVideoMuted: (participant: Participant): boolean => {
191
177
  if (!participant) {
192
178
  throw new ParameterError('Video could not be processed, participant is undefined.');
193
179
  }
@@ -199,7 +185,7 @@ const MemberUtil = {
199
185
  * @param {Object} participant - The locus participant object.
200
186
  * @returns {Boolean}
201
187
  */
202
- isHandRaised: (participant: any) => {
188
+ isHandRaised: (participant: Participant) => {
203
189
  if (!participant) {
204
190
  throw new ParameterError('Raise hand could not be processed, participant is undefined.');
205
191
  }
@@ -256,7 +242,7 @@ const MemberUtil = {
256
242
  * @param {String} controlsAccessor
257
243
  * @returns {Boolean | undefined}
258
244
  */
259
- isMuted: (participant: any, statusAccessor: string, controlsAccessor: string) => {
245
+ isMuted: (participant: Participant, statusAccessor: string, controlsAccessor: string) => {
260
246
  // check remote mute
261
247
  const remoteMute = participant?.controls?.[controlsAccessor]?.muted;
262
248
  if (remoteMute === true) {
@@ -295,7 +281,7 @@ const MemberUtil = {
295
281
  * @param {Object} participant - The locus participant object.
296
282
  * @returns {Boolean}
297
283
  */
298
- isRecording: (participant: any) => {
284
+ isRecording: (participant: Participant) => {
299
285
  if (!participant) {
300
286
  throw new ParameterError('Recording could not be processed, participant is undefined.');
301
287
  }
@@ -341,7 +327,7 @@ const MemberUtil = {
341
327
  * @param {Object} participant - The locus participant object.
342
328
  * @returns {String}
343
329
  */
344
- extractStatus: (participant: any) => {
330
+ extractStatus: (participant: Participant) => {
345
331
  if (!(participant && participant.devices && participant.devices.length)) {
346
332
  return _NOT_IN_MEETING_;
347
333
  }
@@ -371,7 +357,7 @@ const MemberUtil = {
371
357
  * @param {Object} participant - The locus participant object.
372
358
  * @returns {String}
373
359
  */
374
- extractId: (participant: any) => {
360
+ extractId: (participant: Participant) => {
375
361
  if (participant) {
376
362
  return participant.id;
377
363
  }
@@ -384,7 +370,7 @@ const MemberUtil = {
384
370
  * @param {Object} participant - The locus participant object.
385
371
  * @returns {Object}
386
372
  */
387
- extractMediaStatus: (participant: any): IMediaStatus => {
373
+ extractMediaStatus: (participant: Participant): IMediaStatus => {
388
374
  if (!participant) {
389
375
  throw new ParameterError('Media status could not be extracted, participant is undefined.');
390
376
  }
@@ -399,12 +385,30 @@ const MemberUtil = {
399
385
  * @param {Object} participant - The locus participant object.
400
386
  * @returns {String}
401
387
  */
402
- extractName: (participant: any) => {
388
+ extractName: (participant: Participant) => {
403
389
  if (participant && participant.person) {
404
390
  return participant.person.name;
405
391
  }
406
392
 
407
393
  return null;
408
394
  },
395
+
396
+ /**
397
+ * @param {Object} participant - The locus participant object.
398
+ * @returns {String}
399
+ */
400
+ extractPairedWithParticipantUrl: (participant: Participant): ParticipantUrl | undefined => {
401
+ let participantUrl;
402
+
403
+ participant?.devices?.forEach((device) => {
404
+ device?.intents?.forEach((intent) => {
405
+ if (intent?.type === _OBSERVE_ && intent?.associatedWith) {
406
+ participantUrl = intent.associatedWith;
407
+ }
408
+ });
409
+ });
410
+
411
+ return participantUrl;
412
+ },
409
413
  };
410
414
  export default MemberUtil;
@@ -1,10 +1,11 @@
1
1
  import {MEETINGS} from '../constants';
2
+ import Member from '../member';
2
3
 
3
4
  /**
4
5
  * @class MembersCollection
5
6
  */
6
7
  export default class MembersCollection {
7
- members: any;
8
+ members: Record<string, Member>;
8
9
  namespace = MEETINGS;
9
10
  /**
10
11
  * @param {Object} locus
@@ -14,11 +15,11 @@ export default class MembersCollection {
14
15
  this.members = {};
15
16
  }
16
17
 
17
- set(id, member) {
18
+ set(id: string, member: Member) {
18
19
  this.members[id] = member;
19
20
  }
20
21
 
21
- setAll(members) {
22
+ setAll(members: Record<string, Member>) {
22
23
  this.members = members;
23
24
  }
24
25