Bladeren bron

Initial implementation of lobby rooms. (#1138)

* Initial impl of lobby rooms.

* Fixes tests to check the new fulljid added to MUC_MEMBER_JOINED.

* Updates few of the comments, renaming some functions.

* Renames disableLobby ChatRoom option to enableLobby.

* Fixes a comment.

* Moves setMembersOnly method to ChatRoom.

* Fixes counting members, to exclude jicofo.

* Moves setLobbyRoomJid earlier and renames a method.

Rename _maybeEnableDisable to maybeJoinLeaveLobbyRoom.

* Drops using custom roomconfig lobbypassword field and reuse room lock.

* Handles destroying the lobby room.

* Handles clear lobby room on destroy for moderators.

We do not try to leave the lobby room as it is server-side destroyed and we handle that. The only case of leaving a lobby room is when request to join room is being approved.

* Join main room if lobby is disabled while waiting.

* Adds MEMBERS_ONLY_CHANGED conference event.

* fix: Make sure we leave lobby if main room is joined.

* fix: Setting password when joining locked room.

* fix: Fixes case where we enable lobby for already locked room.

* fix: Fixes case where we enable lobby and then lock room.

* fix: Fixes lint.

* ref: Removes shared password for lobby.

This functionality is handled by the lock room password and handled there.
Removes duplication and unnecessary complicated API for lobby room.

* fix: Fixes comments.
master
Дамян Минков 4 jaren geleden
bovenliggende
commit
c700fbd584
No account linked to committer's email address

+ 79
- 0
JitsiConference.js Bestand weergeven

@@ -3415,6 +3415,85 @@ JitsiConference.prototype.setE2EEKey = function(key) {
3415 3415
     this._e2eeCtx.setKey(key);
3416 3416
 };
3417 3417
 
3418
+/**
3419
+ * Returns <tt>true</tt> if lobby support is enabled in the backend.
3420
+ *
3421
+ * @returns {boolean} whether lobby is supported in the backend.
3422
+ */
3423
+JitsiConference.prototype.isLobbySupported = function() {
3424
+    return Boolean(this.room && this.room.getLobby().isSupported());
3425
+};
3426
+
3427
+/**
3428
+ * Returns <tt>true</tt> if the room has members only enabled.
3429
+ *
3430
+ * @returns {boolean} whether conference room is members only.
3431
+ */
3432
+JitsiConference.prototype.isMembersOnly = function() {
3433
+    return Boolean(this.room && this.room.membersOnlyEnabled);
3434
+};
3435
+
3436
+/**
3437
+ * Enables lobby by moderators
3438
+ *
3439
+ * @returns {Promise} resolves when lobby room is joined or rejects with the error.
3440
+ */
3441
+JitsiConference.prototype.enableLobby = function() {
3442
+    if (this.room && this.isModerator()) {
3443
+        return this.room.getLobby().enable();
3444
+    }
3445
+
3446
+    return Promise.reject(
3447
+        new Error('The conference not started or user is not moderator'));
3448
+};
3449
+
3450
+/**
3451
+ * Disabled lobby by moderators
3452
+ *
3453
+ * @returns {void}
3454
+ */
3455
+JitsiConference.prototype.disableLobby = function() {
3456
+    if (this.room && this.isModerator()) {
3457
+        this.room.getLobby().disable();
3458
+    }
3459
+};
3460
+
3461
+/**
3462
+ * Joins the lobby room with display name and optional email or with a shared password to skip waiting.
3463
+ *
3464
+ * @param {string} displayName Display name should be set to show it to moderators.
3465
+ * @param {string} email Optional email is used to present avatar to the moderator.
3466
+ * @returns {Promise<never>}
3467
+ */
3468
+JitsiConference.prototype.joinLobby = function(displayName, email) {
3469
+    if (this.room) {
3470
+        return this.room.getLobby().join(displayName, email);
3471
+    }
3472
+
3473
+    return Promise.reject(new Error('The conference not started'));
3474
+};
3475
+
3476
+/**
3477
+ * Denies an occupant in the lobby room access to the conference.
3478
+ * @param {string} id The participant id.
3479
+ */
3480
+JitsiConference.prototype.lobbyDenyAccess = function(id) {
3481
+    if (this.room) {
3482
+        this.room.getLobby().denyAccess(id);
3483
+    }
3484
+};
3485
+
3486
+/**
3487
+ * Approves the request to join the conference to a participant waiting in the lobby.
3488
+ *
3489
+ * @param {string} id The participant id.
3490
+ */
3491
+JitsiConference.prototype.lobbyApproveAccess = function(id) {
3492
+    if (this.room) {
3493
+        this.room.getLobby().approveAccess(id);
3494
+    }
3495
+};
3496
+
3418 3497
 /**
3419 3498
  * Setup E2EE for the sending side, if supported.
3420 3499
  * Note that this is only done for the JVB Peer Connecction.

+ 12
- 0
JitsiConferenceErrors.js Bestand weergeven

@@ -33,6 +33,18 @@ export const CONNECTION_ERROR = 'conference.connectionError';
33 33
  */
34 34
 export const NOT_ALLOWED_ERROR = 'conference.connectionError.notAllowed';
35 35
 
36
+/**
37
+ * Indicates that a connection error is due to not allowed,
38
+ * occurred when trying to join a conference, only approved members are allowed to join.
39
+ */
40
+export const MEMBERS_ONLY_ERROR = 'conference.connectionError.membersOnly';
41
+
42
+/**
43
+ * Indicates that a connection error is due to denied access to the room,
44
+ * occurred after joining a lobby room and access is denied by the room moderators.
45
+ */
46
+export const CONFERENCE_ACCESS_DENIED = 'conference.connectionError.accessDenied';
47
+
36 48
 /**
37 49
  * Indicates that focus error happened.
38 50
  */

+ 15
- 0
JitsiConferenceEventManager.js Bestand weergeven

@@ -159,6 +159,9 @@ JitsiConferenceEventManager.prototype.setupChatRoomListeners = function() {
159 159
     this.chatRoomForwarder.forward(XMPPEvents.ROOM_CONNECT_NOT_ALLOWED_ERROR,
160 160
         JitsiConferenceEvents.CONFERENCE_FAILED,
161 161
         JitsiConferenceErrors.NOT_ALLOWED_ERROR);
162
+    this.chatRoomForwarder.forward(XMPPEvents.ROOM_CONNECT_MEMBERS_ONLY_ERROR,
163
+        JitsiConferenceEvents.CONFERENCE_FAILED,
164
+        JitsiConferenceErrors.MEMBERS_ONLY_ERROR);
162 165
 
163 166
     this.chatRoomForwarder.forward(XMPPEvents.ROOM_MAX_USERS_ERROR,
164 167
         JitsiConferenceEvents.CONFERENCE_FAILED,
@@ -272,14 +275,26 @@ JitsiConferenceEventManager.prototype.setupChatRoomListeners = function() {
272 275
     this.chatRoomForwarder.forward(XMPPEvents.MUC_LOCK_CHANGED,
273 276
         JitsiConferenceEvents.LOCK_STATE_CHANGED);
274 277
 
278
+    this.chatRoomForwarder.forward(XMPPEvents.MUC_MEMBERS_ONLY_CHANGED,
279
+        JitsiConferenceEvents.MEMBERS_ONLY_CHANGED);
280
+
275 281
     chatRoom.addListener(XMPPEvents.MUC_MEMBER_JOINED,
276 282
         conference.onMemberJoined.bind(conference));
283
+    this.chatRoomForwarder.forward(XMPPEvents.MUC_LOBBY_MEMBER_JOINED,
284
+        JitsiConferenceEvents.LOBBY_USER_JOINED);
285
+    this.chatRoomForwarder.forward(XMPPEvents.MUC_LOBBY_MEMBER_UPDATED,
286
+        JitsiConferenceEvents.LOBBY_USER_UPDATED);
287
+    this.chatRoomForwarder.forward(XMPPEvents.MUC_LOBBY_MEMBER_LEFT,
288
+        JitsiConferenceEvents.LOBBY_USER_LEFT);
277 289
     chatRoom.addListener(XMPPEvents.MUC_MEMBER_BOT_TYPE_CHANGED,
278 290
         conference._onMemberBotTypeChanged.bind(conference));
279 291
     chatRoom.addListener(XMPPEvents.MUC_MEMBER_LEFT,
280 292
         conference.onMemberLeft.bind(conference));
281 293
     this.chatRoomForwarder.forward(XMPPEvents.MUC_LEFT,
282 294
         JitsiConferenceEvents.CONFERENCE_LEFT);
295
+    this.chatRoomForwarder.forward(XMPPEvents.MUC_DENIED_ACCESS,
296
+        JitsiConferenceEvents.CONFERENCE_FAILED,
297
+        JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED);
283 298
 
284 299
     chatRoom.addListener(XMPPEvents.DISPLAY_NAME_CHANGED,
285 300
         conference.onDisplayNameChanged.bind(conference));

+ 23
- 0
JitsiConferenceEvents.js Bestand weergeven

@@ -148,6 +148,14 @@ export const LOCK_STATE_CHANGED = 'conference.lock_state_changed';
148 148
  */
149 149
 export const SERVER_REGION_CHANGED = 'conference.server_region_changed';
150 150
 
151
+/**
152
+ * Indicates that the conference had changed to members only enabled/disabled.
153
+ * The first argument of this event is a <tt>boolean</tt> which when set to
154
+ * <tt>true</tt> means that the conference is running in members only mode.
155
+ * You may need to use Lobby if supported to ask for permissions to enter the conference.
156
+ */
157
+export const MEMBERS_ONLY_CHANGED = 'conference.membersOnlyChanged';
158
+
151 159
 /**
152 160
  * New text message was received.
153 161
  */
@@ -328,3 +336,18 @@ export const USER_STATUS_CHANGED = 'conference.statusChanged';
328 336
  * Event indicates that the bot participant type changed.
329 337
  */
330 338
 export const BOT_TYPE_CHANGED = 'conference.bot_type_changed';
339
+
340
+/**
341
+ * A new user joined the lobby room.
342
+ */
343
+export const LOBBY_USER_JOINED = 'conference.lobby.userJoined';
344
+
345
+/**
346
+ * A user from the lobby room has been update.
347
+ */
348
+export const LOBBY_USER_UPDATED = 'conference.lobby.userUpdated';
349
+
350
+/**
351
+ * A user left the lobby room.
352
+ */
353
+export const LOBBY_USER_LEFT = 'conference.lobby.userLeft';

+ 213
- 35
modules/xmpp/ChatRoom.js Bestand weergeven

@@ -9,6 +9,7 @@ import Listenable from '../util/Listenable';
9 9
 import * as MediaType from '../../service/RTC/MediaType';
10 10
 import XMPPEvents from '../../service/xmpp/XMPPEvents';
11 11
 
12
+import Lobby from './Lobby';
12 13
 import Moderator from './moderator';
13 14
 import XmppConnection from './XmppConnection';
14 15
 
@@ -80,6 +81,12 @@ function filterNodeFromPresenceJSON(pres, nodeName) {
80 81
 // of chaining function calls, allow long function call chains.
81 82
 /* eslint-disable newline-per-chained-call */
82 83
 
84
+/**
85
+ * Array of affiliations that are allowed in members only room.
86
+ * @type {string[]}
87
+ */
88
+const MEMBERS_AFFILIATIONS = [ 'owner', 'admin', 'member' ];
89
+
83 90
 /**
84 91
  *
85 92
  */
@@ -95,8 +102,10 @@ export default class ChatRoom extends Listenable {
95 102
      * @param XMPP
96 103
      * @param options
97 104
      * @param {boolean} options.disableFocus - when set to {@code false} will
98
-     * not invite Jicofo into the room. This is intended to be used only by
99
-     * jitsi-meet-spot.
105
+     * not invite Jicofo into the room.
106
+     * @param {boolean} options.disableDiscoInfo - when set to {@code false} will skip disco info.
107
+     * This is intended to be used only for lobby rooms.
108
+     * @param {boolean} options.enableLobby - when set to {@code false} will skip creating lobby room.
100 109
      */
101 110
     constructor(connection, jid, password, XMPP, options) {
102 111
         super();
@@ -120,6 +129,9 @@ export default class ChatRoom extends Listenable {
120 129
                 connection: this.xmpp.options,
121 130
                 conference: this.options
122 131
             });
132
+        if (typeof this.options.enableLobby === 'undefined' || this.options.enableLobby) {
133
+            this.lobby = new Lobby(this);
134
+        }
123 135
         this.initPresenceMap(options);
124 136
         this.lastPresences = {};
125 137
         this.phoneNumber = null;
@@ -168,16 +180,19 @@ export default class ChatRoom extends Listenable {
168 180
 
169 181
     /**
170 182
      * Joins the chat room.
171
-     * @param password
183
+     * @param {string} password - Password to unlock room on joining.
184
+     * @param {Object} customJoinPresenceExtensions - Key values object to be used
185
+     * for the initial presence, they key will be an xmpp node and its text is the value,
186
+     * and those will be added to the initial <x xmlns='http://jabber.org/protocol/muc'/>
172 187
      * @returns {Promise} - resolved when join completes. At the time of this
173 188
      * writing it's never rejected.
174 189
      */
175
-    join(password) {
190
+    join(password, customJoinPresenceExtensions) {
176 191
         this.password = password;
177 192
 
178 193
         return new Promise(resolve => {
179 194
             this.options.disableFocus
180
-                && logger.info('Conference focus disabled');
195
+                && logger.info(`Conference focus disabled for ${this.roomjid}`);
181 196
 
182 197
             const preJoin
183 198
                 = this.options.disableFocus
@@ -185,7 +200,7 @@ export default class ChatRoom extends Listenable {
185 200
                     : this.moderator.allocateConferenceFocus();
186 201
 
187 202
             preJoin.then(() => {
188
-                this.sendPresence(true);
203
+                this.sendPresence(true, customJoinPresenceExtensions);
189 204
                 this._removeConnListeners.push(
190 205
                     this.connection.addEventListener(
191 206
                         XmppConnection.Events.CONN_STATUS_CHANGED,
@@ -198,9 +213,10 @@ export default class ChatRoom extends Listenable {
198 213
 
199 214
     /**
200 215
      *
201
-     * @param fromJoin
216
+     * @param fromJoin - Whether this is initial presence to join the room.
217
+     * @param customJoinPresenceExtensions - Object of key values to be added to the initial presence only.
202 218
      */
203
-    sendPresence(fromJoin) {
219
+    sendPresence(fromJoin, customJoinPresenceExtensions) {
204 220
         const to = this.presMap.to;
205 221
 
206 222
         if (!this.connection || !this.connection.connected || !to || (!this.joined && !fromJoin)) {
@@ -221,6 +237,11 @@ export default class ChatRoom extends Listenable {
221 237
             if (this.password) {
222 238
                 pres.c('password').t(this.password).up();
223 239
             }
240
+            if (customJoinPresenceExtensions) {
241
+                Object.keys(customJoinPresenceExtensions).forEach(key => {
242
+                    pres.c(key).t(customJoinPresenceExtensions[key]).up();
243
+                });
244
+            }
224 245
             pres.up();
225 246
         }
226 247
 
@@ -298,8 +319,23 @@ export default class ChatRoom extends Listenable {
298 319
             if (meetingIdValEl.length) {
299 320
                 this.setMeetingId(meetingIdValEl.text());
300 321
             } else {
301
-                logger.trace('No meeting ID from backend');
322
+                logger.warn('No meeting ID from backend');
323
+            }
324
+
325
+            const membersOnly = $(result).find('>query>feature[var="muc_membersonly"]').length === 1;
326
+
327
+            const lobbyRoomField
328
+                = $(result).find('>query>x[type="result"]>field[var="muc#roominfo_lobbyroom"]>value');
329
+
330
+            if (this.lobby) {
331
+                this.lobby.setLobbyRoomJid(lobbyRoomField && lobbyRoomField.length ? lobbyRoomField.text() : undefined);
302 332
             }
333
+
334
+            if (membersOnly !== this.membersOnlyEnabled) {
335
+                this.membersOnlyEnabled = membersOnly;
336
+                this.eventEmitter.emit(XMPPEvents.MUC_MEMBERS_ONLY_CHANGED, membersOnly);
337
+            }
338
+
303 339
         }, error => {
304 340
             GlobalOnErrorHandler.callErrorHandler(error);
305 341
             logger.error('Error getting room info: ', error);
@@ -328,6 +364,10 @@ export default class ChatRoom extends Listenable {
328 364
     createNonAnonymousRoom() {
329 365
         // http://xmpp.org/extensions/xep-0045.html#createroom-reserved
330 366
 
367
+        if (this.options.disableDiscoInfo) {
368
+            return;
369
+        }
370
+
331 371
         const getForm = $iq({ type: 'get',
332 372
             to: this.roomjid })
333 373
             .c('query', { xmlns: 'http://jabber.org/protocol/muc#owner' })
@@ -533,7 +573,7 @@ export default class ChatRoom extends Listenable {
533 573
 
534 574
                 // Now let's check the disco-info to retrieve the
535 575
                 // meeting Id if any
536
-                this.discoRoomInfo();
576
+                !this.options.disableDiscoInfo && this.discoRoomInfo();
537 577
             }
538 578
         } else if (jid === undefined) {
539 579
             logger.info('Ignoring member with undefined JID');
@@ -558,7 +598,8 @@ export default class ChatRoom extends Listenable {
558 598
                     member.statsID,
559 599
                     member.status,
560 600
                     member.identity,
561
-                    member.botType);
601
+                    member.botType,
602
+                    member.jid);
562 603
 
563 604
                 // we are reporting the status with the join
564 605
                 // so we do not want a second event about status update
@@ -575,6 +616,11 @@ export default class ChatRoom extends Listenable {
575 616
                     XMPPEvents.MUC_ROLE_CHANGED, from, member.role);
576 617
             }
577 618
 
619
+            // affiliation changed
620
+            if (memberOfThis.affiliation !== member.affiliation) {
621
+                memberOfThis.affiliation = member.affiliation;
622
+            }
623
+
578 624
             // fire event that botType had changed
579 625
             if (memberOfThis.botType !== member.botType) {
580 626
                 memberOfThis.botType = member.botType;
@@ -856,8 +902,9 @@ export default class ChatRoom extends Listenable {
856 902
         }
857 903
 
858 904
         // room destroyed ?
859
-        if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]'
860
-            + '>destroy').length) {
905
+        const destroySelect = $(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>destroy');
906
+
907
+        if (destroySelect.length) {
861 908
             let reason;
862 909
             const reasonSelect
863 910
                 = $(pres).find(
@@ -868,7 +915,7 @@ export default class ChatRoom extends Listenable {
868 915
                 reason = reasonSelect.text();
869 916
             }
870 917
 
871
-            this.eventEmitter.emit(XMPPEvents.MUC_DESTROYED, reason);
918
+            this.eventEmitter.emit(XMPPEvents.MUC_DESTROYED, reason, destroySelect.attr('jid'));
872 919
             this.connection.emuc.doLeave(this.roomjid);
873 920
 
874 921
             return true;
@@ -900,24 +947,17 @@ export default class ChatRoom extends Listenable {
900 947
                 actorNick = actorSelect.attr('nick');
901 948
             }
902 949
 
903
-            // if no member is found this is the case we had kicked someone
904
-            // and we are not in the list of members
905
-            if (membersKeys.find(jid => Strophe.getResourceFromJid(jid) === actorNick)) {
906
-                // we first fire the kicked so we can show the participant
907
-                // who kicked, before notifying that participant left
908
-                // we fire kicked for us and for any participant kicked
909
-                this.eventEmitter.emit(
910
-                    XMPPEvents.KICKED,
911
-                    isSelfPresence,
912
-                    actorNick,
913
-                    Strophe.getResourceFromJid(from));
914
-            }
950
+            // we first fire the kicked so we can show the participant
951
+            // who kicked, before notifying that participant left
952
+            // we fire kicked for us and for any participant kicked
953
+            this.eventEmitter.emit(
954
+                XMPPEvents.KICKED,
955
+                isSelfPresence,
956
+                actorNick,
957
+                Strophe.getResourceFromJid(from));
915 958
         }
916 959
 
917
-        if (!isSelfPresence) {
918
-            delete this.members[from];
919
-            this.onParticipantLeft(from, false);
920
-        } else if (membersKeys.length > 0) {
960
+        if (isSelfPresence) {
921 961
             // If the status code is 110 this means we're leaving and we would
922 962
             // like to remove everyone else from our view, so we trigger the
923 963
             // event.
@@ -934,6 +974,9 @@ export default class ChatRoom extends Listenable {
934 974
             if (!isKick) {
935 975
                 this.eventEmitter.emit(XMPPEvents.MUC_LEFT);
936 976
             }
977
+        } else {
978
+            delete this.members[from];
979
+            this.onParticipantLeft(from, false);
937 980
         }
938 981
     }
939 982
 
@@ -986,9 +1029,23 @@ export default class ChatRoom extends Listenable {
986 1029
             }
987 1030
         }
988 1031
 
989
-        if (from === this.roomjid
990
-                && $(msg).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="104"]').length) {
991
-            this.discoRoomInfo();
1032
+        if (from === this.roomjid) {
1033
+            let invite;
1034
+
1035
+            if ($(msg).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="104"]').length) {
1036
+                this.discoRoomInfo();
1037
+            } else if ((invite = $(msg).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>invite'))
1038
+                        && invite.length) {
1039
+                const passwordSelect = $(msg).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>password');
1040
+                let password;
1041
+
1042
+                if (passwordSelect && passwordSelect.length) {
1043
+                    password = passwordSelect.text();
1044
+                }
1045
+
1046
+                this.eventEmitter.emit(XMPPEvents.INVITE_MESSAGE_RECEIVED,
1047
+                    from, invite.attr('from'), txt, password);
1048
+            }
992 1049
         }
993 1050
         const jsonMessage = $(msg).find('>json-message').text();
994 1051
         const parsedJson = this.xmpp.tryParseJSONAndVerify(jsonMessage);
@@ -1052,6 +1109,21 @@ export default class ChatRoom extends Listenable {
1052 1109
             logger.warn('Maximum users limit for the room has been reached',
1053 1110
                 pres);
1054 1111
             this.eventEmitter.emit(XMPPEvents.ROOM_MAX_USERS_ERROR);
1112
+        } else if ($(pres)
1113
+            .find(
1114
+                '>error[type="auth"]'
1115
+                + '>registration-required['
1116
+                + 'xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]').length) {
1117
+
1118
+            // let's extract the lobby jid from the custom field
1119
+            const lobbyRoomNode = $(pres).find('>lobbyroom');
1120
+            let lobbyRoomJid;
1121
+
1122
+            if (lobbyRoomNode.length) {
1123
+                lobbyRoomJid = lobbyRoomNode.text();
1124
+            }
1125
+
1126
+            this.eventEmitter.emit(XMPPEvents.ROOM_CONNECT_MEMBERS_ONLY_ERROR, lobbyRoomJid);
1055 1127
         } else {
1056 1128
             logger.warn('onPresError ', pres);
1057 1129
             this.eventEmitter.emit(XMPPEvents.ROOM_CONNECT_ERROR);
@@ -1132,6 +1204,16 @@ export default class ChatRoom extends Listenable {
1132 1204
                         .up()
1133 1205
                         .up();
1134 1206
 
1207
+                    // if members only enabled
1208
+                    if (this.membersOnlyEnabled) {
1209
+                        formsubmit
1210
+                            .c('field', { 'var': 'muc#roomconfig_membersonly' })
1211
+                            .c('value')
1212
+                            .t('true')
1213
+                            .up()
1214
+                            .up();
1215
+                    }
1216
+
1135 1217
                     // Fixes a bug in prosody 0.9.+
1136 1218
                     // https://prosody.im/issues/issue/373
1137 1219
                     formsubmit
@@ -1151,6 +1233,87 @@ export default class ChatRoom extends Listenable {
1151 1233
 
1152 1234
     /* eslint-enable max-params */
1153 1235
 
1236
+    /**
1237
+     * Turns off or on the members only config for the main room.
1238
+     *
1239
+     * @param {boolean} enabled - Whether to turn it on or off.
1240
+     * @param onSuccess - optional callback.
1241
+     * @param onError - optional callback.
1242
+     */
1243
+    setMembersOnly(enabled, onSuccess, onError) {
1244
+        if (enabled && Object.values(this.members).filter(m => !m.isFocus).length) {
1245
+            let sendGrantMembershipIq = false;
1246
+
1247
+            // first grant membership to all that are in the room
1248
+            const grantMembership = $iq({ to: this.roomjid,
1249
+                type: 'set' })
1250
+                .c('query', { xmlns: 'http://jabber.org/protocol/muc#admin' });
1251
+
1252
+            Object.values(this.members).forEach(m => {
1253
+                if (m.jid && !MEMBERS_AFFILIATIONS.includes(m.affiliation)) {
1254
+                    grantMembership.c('item', {
1255
+                        'affiliation': 'member',
1256
+                        'jid': m.jid }).up();
1257
+                    sendGrantMembershipIq = true;
1258
+                }
1259
+            });
1260
+
1261
+            if (sendGrantMembershipIq) {
1262
+                this.xmpp.connection.sendIQ(grantMembership.up());
1263
+            }
1264
+        }
1265
+
1266
+        const errorCallback = onError ? onError : () => {}; // eslint-disable-line no-empty-function
1267
+
1268
+        this.xmpp.connection.sendIQ(
1269
+            $iq({
1270
+                to: this.roomjid,
1271
+                type: 'get'
1272
+            }).c('query', { xmlns: 'http://jabber.org/protocol/muc#owner' }),
1273
+            res => {
1274
+                if ($(res).find('>query>x[xmlns="jabber:x:data"]>field[var="muc#roomconfig_membersonly"]').length) {
1275
+                    const formToSubmit
1276
+                        = $iq({
1277
+                            to: this.roomjid,
1278
+                            type: 'set'
1279
+                        }).c('query', { xmlns: 'http://jabber.org/protocol/muc#owner' });
1280
+
1281
+                    formToSubmit.c('x', {
1282
+                        xmlns: 'jabber:x:data',
1283
+                        type: 'submit'
1284
+                    });
1285
+                    formToSubmit
1286
+                        .c('field', { 'var': 'FORM_TYPE' })
1287
+                        .c('value')
1288
+                        .t('http://jabber.org/protocol/muc#roomconfig')
1289
+                        .up()
1290
+                        .up();
1291
+                    formToSubmit
1292
+                        .c('field', { 'var': 'muc#roomconfig_membersonly' })
1293
+                        .c('value')
1294
+                        .t(enabled ? 'true' : 'false')
1295
+                        .up()
1296
+                        .up();
1297
+
1298
+                    // if room is locked from other participant or we are locking it
1299
+                    if (this.locked) {
1300
+                        formToSubmit
1301
+                            .c('field',
1302
+                                { 'var': 'muc#roomconfig_passwordprotectedroom' })
1303
+                            .c('value')
1304
+                            .t('1')
1305
+                            .up()
1306
+                            .up();
1307
+                    }
1308
+
1309
+                    this.xmpp.connection.sendIQ(formToSubmit, onSuccess, errorCallback);
1310
+                } else {
1311
+                    errorCallback(new Error('Setting members only room not supported!'));
1312
+                }
1313
+            },
1314
+            errorCallback);
1315
+    }
1316
+
1154 1317
     /**
1155 1318
      * Adds the key to the presence map, overriding any previous value.
1156 1319
      * @param key
@@ -1404,6 +1567,14 @@ export default class ChatRoom extends Listenable {
1404 1567
         return this.connection.rayo.hangup();
1405 1568
     }
1406 1569
 
1570
+    /**
1571
+     *
1572
+     * @returns {Lobby}
1573
+     */
1574
+    getLobby() {
1575
+        return this.lobby;
1576
+    }
1577
+
1407 1578
     /**
1408 1579
      * Returns the phone number for joining the conference.
1409 1580
      */
@@ -1475,6 +1646,14 @@ export default class ChatRoom extends Listenable {
1475 1646
         }
1476 1647
     }
1477 1648
 
1649
+    /**
1650
+     * Clean any listeners or resources, executed on leaving.
1651
+     */
1652
+    clean() {
1653
+        this._removeConnListeners.forEach(remove => remove());
1654
+        this._removeConnListeners = [];
1655
+    }
1656
+
1478 1657
     /**
1479 1658
      * Leaves the room. Closes the jingle session.
1480 1659
      * @returns {Promise} which is resolved if XMPPEvents.MUC_LEFT is received
@@ -1486,8 +1665,7 @@ export default class ChatRoom extends Listenable {
1486 1665
             const timeout = setTimeout(() => onMucLeft(true), 5000);
1487 1666
             const eventEmitter = this.eventEmitter;
1488 1667
 
1489
-            this._removeConnListeners.forEach(remove => remove());
1490
-            this._removeConnListeners = [];
1668
+            this.clean();
1491 1669
 
1492 1670
             /**
1493 1671
              *

+ 8
- 4
modules/xmpp/ChatRoom.spec.js Bestand weergeven

@@ -172,7 +172,8 @@ describe('ChatRoom', () => {
172 172
                 undefined, // statsID
173 173
                 'status-text',
174 174
                 undefined,
175
-                undefined
175
+                undefined,
176
+                'fulljid'
176 177
             ]);
177 178
         });
178 179
 
@@ -200,7 +201,8 @@ describe('ChatRoom', () => {
200 201
                 undefined, // statsID
201 202
                 undefined,
202 203
                 undefined,
203
-                undefined);
204
+                undefined,
205
+                'jid=attr');
204 206
         });
205 207
 
206 208
         it('parses identity correctly', () => {
@@ -245,7 +247,8 @@ describe('ChatRoom', () => {
245 247
                 undefined, // statsID
246 248
                 'status-text',
247 249
                 expectedIdentity,
248
-                undefined
250
+                undefined,
251
+                'fulljid'
249 252
             ]);
250 253
         });
251 254
 
@@ -276,7 +279,8 @@ describe('ChatRoom', () => {
276 279
                 undefined, // statsID
277 280
                 'status-text',
278 281
                 undefined,
279
-                expectedBotType
282
+                expectedBotType,
283
+                'fulljid'
280 284
             ]);
281 285
         });
282 286
 

+ 320
- 0
modules/xmpp/Lobby.js Bestand weergeven

@@ -0,0 +1,320 @@
1
+import { $msg, Strophe } from 'strophe.js';
2
+import { getLogger } from 'jitsi-meet-logger';
3
+import XMPPEvents from '../../service/xmpp/XMPPEvents';
4
+
5
+const logger = getLogger(__filename);
6
+
7
+/**
8
+ * The command type for updating a lobby participant's e-mail address.
9
+ *
10
+ * @type {string}
11
+ */
12
+const EMAIL_COMMAND = 'email';
13
+
14
+/**
15
+ * The Lobby room implementation. Setting a room to members only, joining the lobby room
16
+ * approving or denying access to participants from the lobby room.
17
+ */
18
+export default class Lobby {
19
+
20
+    /**
21
+     * Constructs lobby room.
22
+     *
23
+     * @param {ChatRoom} room the main room.
24
+     */
25
+    constructor(room) {
26
+        this.xmpp = room.xmpp;
27
+        this.mainRoom = room;
28
+
29
+        const maybeJoinLobbyRoom = this._maybeJoinLobbyRoom.bind(this);
30
+
31
+        this.mainRoom.addEventListener(
32
+            XMPPEvents.LOCAL_ROLE_CHANGED,
33
+            maybeJoinLobbyRoom);
34
+
35
+        this.mainRoom.addEventListener(
36
+            XMPPEvents.MUC_MEMBERS_ONLY_CHANGED,
37
+            maybeJoinLobbyRoom);
38
+
39
+        this.mainRoom.addEventListener(
40
+            XMPPEvents.ROOM_CONNECT_MEMBERS_ONLY_ERROR,
41
+            jid => {
42
+                this.lobbyRoomJid = jid;
43
+            });
44
+    }
45
+
46
+    /**
47
+     * Whether lobby is supported on backend.
48
+     *
49
+     * @returns {boolean} whether lobby is supported on backend.
50
+     */
51
+    isSupported() {
52
+        return this.xmpp.lobbySupported;
53
+    }
54
+
55
+    /**
56
+     * Enables lobby by setting the main room to be members only and joins the lobby chat room.
57
+     *
58
+     * @returns {Promise}
59
+     */
60
+    enable() {
61
+        if (!this.isSupported()) {
62
+            return Promise.reject(new Error('Lobby not supported!'));
63
+        }
64
+
65
+        return new Promise((resolve, reject) => {
66
+            this.mainRoom.setMembersOnly(true, resolve, reject);
67
+        });
68
+    }
69
+
70
+    /**
71
+     * Disable lobby by setting the main room to be non members only and levaes the lobby chat room if joined.
72
+     *
73
+     * @returns {void}
74
+     */
75
+    disable() {
76
+        if (!this.isSupported() || !this.mainRoom.isModerator()
77
+                || !this.lobbyRoom || !this.mainRoom.membersOnlyEnabled) {
78
+            return;
79
+        }
80
+
81
+        this.mainRoom.setMembersOnly(false);
82
+    }
83
+
84
+    /**
85
+     * Leaves the lobby room.
86
+     * @private
87
+     */
88
+    _leaveLobbyRoom() {
89
+        if (this.lobbyRoom) {
90
+            this.lobbyRoom.leave()
91
+                .then(() => {
92
+                    this.lobbyRoom = undefined;
93
+                    logger.info('Lobby room left!');
94
+                })
95
+                .catch(() => {}); // eslint-disable-line no-empty-function
96
+        }
97
+    }
98
+
99
+    /**
100
+     * We had received a jid for the lobby room.
101
+     *
102
+     * @param jid the lobby room jid to join.
103
+     */
104
+    setLobbyRoomJid(jid) {
105
+        this.lobbyRoomJid = jid;
106
+    }
107
+
108
+    /**
109
+     * Checks the state of mainRoom, lobbyRoom and current user role to decide whether to join lobby room.
110
+     * @private
111
+     */
112
+    _maybeJoinLobbyRoom() {
113
+        if (!this.isSupported()) {
114
+            return;
115
+        }
116
+
117
+        const isModerator = this.mainRoom.joined && this.mainRoom.isModerator();
118
+
119
+        if (isModerator && this.mainRoom.membersOnlyEnabled && !this.lobbyRoom) {
120
+            // join the lobby
121
+            this.join()
122
+                .then(() => logger.info('Joined lobby room'))
123
+                .catch(e => logger.error('Failed joining lobby', e));
124
+        }
125
+    }
126
+
127
+    /**
128
+     * Joins a lobby room setting display name and eventually avatar(using the email provided).
129
+     *
130
+     * @param {string} username is required.
131
+     * @param {string} email is optional.
132
+     * @returns {Promise} resolves once we join the room.
133
+     */
134
+    join(displayName, email) {
135
+        const isModerator = this.mainRoom.joined && this.mainRoom.isModerator();
136
+
137
+        if (!this.lobbyRoomJid) {
138
+            return Promise.reject(new Error('Missing lobbyRoomJid, cannot join lobby room.'));
139
+        }
140
+
141
+        const roomName = Strophe.getNodeFromJid(this.lobbyRoomJid);
142
+        const customDomain = Strophe.getDomainFromJid(this.lobbyRoomJid);
143
+
144
+        this.lobbyRoom = this.xmpp.createRoom(
145
+            roomName, {
146
+                customDomain,
147
+                disableDiscoInfo: true,
148
+                disableFocus: true,
149
+                enableLobby: false
150
+            }
151
+        );
152
+
153
+        if (displayName) {
154
+            // remove previously set nickname
155
+            this.lobbyRoom.removeFromPresence('nick');
156
+            this.lobbyRoom.addToPresence('nick', {
157
+                attributes: { xmlns: 'http://jabber.org/protocol/nick' },
158
+                value: displayName
159
+            });
160
+        }
161
+
162
+        if (isModerator) {
163
+            this.lobbyRoom.addPresenceListener(EMAIL_COMMAND, (node, from) => {
164
+                this.mainRoom.eventEmitter.emit(XMPPEvents.MUC_LOBBY_MEMBER_UPDATED, from, { email: node.value });
165
+            });
166
+            this.lobbyRoom.addEventListener(
167
+                XMPPEvents.MUC_MEMBER_JOINED,
168
+                // eslint-disable-next-line max-params
169
+                (from, nick, role, isHiddenDomain, statsID, status, identity, botType, jid) => {
170
+                    // we need to ignore joins on lobby for participants that are already in the main room
171
+                    if (Object.values(this.mainRoom.members).find(m => m.jid === jid)) {
172
+                        return;
173
+                    }
174
+
175
+                    // we emit the new event on the main room so we can propagate
176
+                    // events to the conference
177
+                    this.mainRoom.eventEmitter.emit(
178
+                        XMPPEvents.MUC_LOBBY_MEMBER_JOINED,
179
+                        Strophe.getResourceFromJid(from),
180
+                        nick,
181
+                        identity ? identity.avatar : undefined
182
+                    );
183
+                });
184
+            this.lobbyRoom.addEventListener(
185
+                XMPPEvents.MUC_MEMBER_LEFT, from => {
186
+                    // we emit the new event on the main room so we can propagate
187
+                    // events to the conference
188
+                    this.mainRoom.eventEmitter.emit(
189
+                        XMPPEvents.MUC_LOBBY_MEMBER_LEFT,
190
+                        Strophe.getResourceFromJid(from)
191
+                    );
192
+                });
193
+            this.lobbyRoom.addEventListener(
194
+                XMPPEvents.MUC_DESTROYED,
195
+                () => {
196
+                    // let's make sure we emit that all lobby users had left
197
+                    Object.keys(this.lobbyRoom.members)
198
+                        .forEach(j => this.mainRoom.eventEmitter.emit(
199
+                            XMPPEvents.MUC_LOBBY_MEMBER_LEFT, Strophe.getResourceFromJid(j)));
200
+
201
+                    this.lobbyRoom = undefined;
202
+                    logger.info('Lobby room left(destroyed)!');
203
+                });
204
+        } else {
205
+            // this should only be handled by those waiting in lobby
206
+            this.lobbyRoom.addEventListener(XMPPEvents.KICKED, isSelfPresence => {
207
+                if (isSelfPresence) {
208
+                    this.mainRoom.eventEmitter.emit(XMPPEvents.MUC_DENIED_ACCESS);
209
+
210
+                    this.lobbyRoom.clean();
211
+
212
+                    return;
213
+                }
214
+            });
215
+
216
+            // As there is still reference of the main room
217
+            // the invite will be detected and addressed to its eventEmitter, even though we are not in it
218
+            // the invite message should be received directly to the xmpp conn in general
219
+            this.mainRoom.addEventListener(
220
+                XMPPEvents.INVITE_MESSAGE_RECEIVED,
221
+                (roomJid, from, txt, invitePassword) => {
222
+                    logger.debug(`Received approval to join ${roomJid} ${from} ${txt}`);
223
+                    if (roomJid === this.mainRoom.roomjid) {
224
+                        // we are now allowed let's join and leave lobby
225
+                        this.mainRoom.join(invitePassword);
226
+
227
+                        this._leaveLobbyRoom();
228
+                    }
229
+                });
230
+            this.lobbyRoom.addEventListener(
231
+                XMPPEvents.MUC_DESTROYED,
232
+                (reason, jid) => {
233
+                    // we are receiving the jid of the main room
234
+                    // means we are invited to join, maybe lobby was disabled
235
+                    if (jid && jid === this.mainRoom.roomjid) {
236
+                        this.mainRoom.join();
237
+
238
+                        return;
239
+                    }
240
+
241
+                    this.mainRoom.eventEmitter.emit(XMPPEvents.MUC_DESTROYED, reason);
242
+                });
243
+
244
+            // If participant retries joining shared password while waiting in the lobby
245
+            // and succeeds make sure we leave lobby
246
+            this.mainRoom.addEventListener(
247
+                XMPPEvents.MUC_JOINED,
248
+                () => {
249
+                    this._leaveLobbyRoom();
250
+                });
251
+        }
252
+
253
+        return new Promise((resolve, reject) => {
254
+            this.lobbyRoom.addEventListener(XMPPEvents.MUC_JOINED, () => {
255
+                resolve();
256
+
257
+                // send our email, as we do not handle this on initial presence we need a second one
258
+                if (email && !isModerator) {
259
+                    this.lobbyRoom.removeFromPresence(EMAIL_COMMAND);
260
+                    this.lobbyRoom.addToPresence(EMAIL_COMMAND, { value: email });
261
+                    this.lobbyRoom.sendPresence();
262
+                }
263
+            });
264
+            this.lobbyRoom.addEventListener(XMPPEvents.ROOM_JOIN_ERROR, reject);
265
+            this.lobbyRoom.addEventListener(XMPPEvents.ROOM_CONNECT_NOT_ALLOWED_ERROR, reject);
266
+            this.lobbyRoom.addEventListener(XMPPEvents.ROOM_CONNECT_ERROR, reject);
267
+
268
+            this.lobbyRoom.join();
269
+        });
270
+
271
+    }
272
+
273
+    /**
274
+     * Should be possible only for moderators.
275
+     * @param id
276
+     */
277
+    denyAccess(id) {
278
+        if (!this.isSupported() || !this.mainRoom.isModerator()) {
279
+            return;
280
+        }
281
+
282
+        const jid = Object.keys(this.lobbyRoom.members)
283
+            .find(j => Strophe.getResourceFromJid(j) === id);
284
+
285
+        if (jid) {
286
+            this.lobbyRoom.kick(jid);
287
+        } else {
288
+            logger.error(`Not found member for ${id} in lobby room.`);
289
+        }
290
+    }
291
+
292
+    /**
293
+     * Should be possible only for moderators.
294
+     * @param id
295
+     */
296
+    approveAccess(id) {
297
+        if (!this.isSupported() || !this.mainRoom.isModerator()) {
298
+            return;
299
+        }
300
+
301
+        const memberRoomJid = Object.keys(this.lobbyRoom.members)
302
+            .find(j => Strophe.getResourceFromJid(j) === id);
303
+
304
+        if (memberRoomJid) {
305
+            const jid = this.lobbyRoom.members[memberRoomJid].jid;
306
+            const msgToSend
307
+                = $msg({ to: this.mainRoom.roomjid })
308
+                    .c('x', { xmlns: 'http://jabber.org/protocol/muc#user' })
309
+                    .c('invite', { to: jid });
310
+
311
+            this.xmpp.connection.sendIQ(msgToSend,
312
+                () => { }, // eslint-disable-line no-empty-function
313
+                e => {
314
+                    logger.error(`Error sending invite for ${jid}`, e);
315
+                });
316
+        } else {
317
+            logger.error(`Not found member for ${memberRoomJid} in lobby room.`);
318
+        }
319
+    }
320
+}

+ 6
- 1
modules/xmpp/xmpp.js Bestand weergeven

@@ -248,6 +248,10 @@ export default class XMPP extends Listenable {
248 248
                         if (identity.type === 'conference_duration') {
249 249
                             this.conferenceDurationComponentAddress = identity.name;
250 250
                         }
251
+
252
+                        if (identity.type === 'lobbyrooms') {
253
+                            this.lobbySupported = true;
254
+                        }
251 255
                     });
252 256
 
253 257
                     if (this.speakerStatsComponentAddress
@@ -469,7 +473,8 @@ export default class XMPP extends Listenable {
469 473
      * @returns {Promise} Resolves with an instance of a strophe muc.
470 474
      */
471 475
     createRoom(roomName, options, onCreateResource) {
472
-        let roomjid = `${roomName}@${this.options.hosts.muc}/`;
476
+        let roomjid = `${roomName}@${options.customDomain
477
+            ? options.customDomain : this.options.hosts.muc}/`;
473 478
 
474 479
         const mucNickname = onCreateResource
475 480
             ? onCreateResource(this.connection.jid, this.authenticatedUser)

+ 20
- 0
service/xmpp/XMPPEvents.js Bestand weergeven

@@ -108,6 +108,10 @@ const XMPPEvents = {
108 108
     // received.
109 109
     MESSAGE_RECEIVED: 'xmpp.message_received',
110 110
 
111
+    // Designates an event indicating that an invite XMPP message in the MUC was
112
+    // received.
113
+    INVITE_MESSAGE_RECEIVED: 'xmpp.invite_message_received',
114
+
111 115
     // Designates an event indicating that a private XMPP message in the MUC was
112 116
     // received.
113 117
     PRIVATE_MESSAGE_RECEIVED: 'xmpp.private_message_received',
@@ -127,6 +131,18 @@ const XMPPEvents = {
127 131
     // Designates an event indicating that a participant left the XMPP MUC.
128 132
     MUC_MEMBER_LEFT: 'xmpp.muc_member_left',
129 133
 
134
+    // Designates an event indicating that a participant joined the lobby XMPP MUC.
135
+    MUC_LOBBY_MEMBER_JOINED: 'xmpp.muc_lobby_member_joined',
136
+
137
+    // Designates an event indicating that a participant in the lobby XMPP MUC has been updated
138
+    MUC_LOBBY_MEMBER_UPDATED: 'xmpp.muc_lobby_member_updated',
139
+
140
+    // Designates an event indicating that a participant left the XMPP MUC.
141
+    MUC_LOBBY_MEMBER_LEFT: 'xmpp.muc_lobby_member_left',
142
+
143
+    // Designates an event indicating that a participant was denied access to a conference from the lobby XMPP MUC.
144
+    MUC_DENIED_ACCESS: 'xmpp.muc_denied access',
145
+
130 146
     // Designates an event indicating that local participant left the muc
131 147
     MUC_LEFT: 'xmpp.muc_left',
132 148
 
@@ -137,6 +153,9 @@ const XMPPEvents = {
137 153
     // Designates an event indicating that the MUC has been locked or unlocked.
138 154
     MUC_LOCK_CHANGED: 'xmpp.muc_lock_changed',
139 155
 
156
+    // Designates an event indicating that the MUC members only config has changed.
157
+    MUC_MEMBERS_ONLY_CHANGED: 'xmpp.muc_members_only_changed',
158
+
140 159
     // Designates an event indicating that a participant in the XMPP MUC has
141 160
     // advertised that they have audio muted (or unmuted).
142 161
     PARTICIPANT_AUDIO_MUTED: 'xmpp.audio_muted',
@@ -186,6 +205,7 @@ const XMPPEvents = {
186 205
     ROOM_CONNECT_ERROR: 'xmpp.room_connect_error',
187 206
     ROOM_CONNECT_NOT_ALLOWED_ERROR: 'xmpp.room_connect_error.not_allowed',
188 207
     ROOM_JOIN_ERROR: 'xmpp.room_join_error',
208
+    ROOM_CONNECT_MEMBERS_ONLY_ERROR: 'xmpp.room_connect_error.members_only',
189 209
 
190 210
     /**
191 211
      * Indicates that max users limit has been reached.

Laden…
Annuleren
Opslaan