Explorar el Código

feat: Audio/Video moderation. (#1581)

* feat: Audio/Video moderation.

* squash: Fix docs.

* squash: Adds some warning logs when execution is rejected.

* squash: Changes a field name in the message for adding jid to whitelist.

* squash: Send to participants only message about approval.

Skips sending the whole list.

* squash: Fixes tests.

* squash: Adds more logs.

* feat: Separates enable/disable by media type.

Adds actor to the messages to inform who enabled it.

* squash: Fixes log line.

* squash: Fixes comments.

* squash: Fixes log messages.

* squash: Fixes comments.
dev1
Дамян Минков hace 4 años
padre
commit
88560a8a5e
No account linked to committer's email address

+ 72
- 0
JitsiConference.js Ver fichero

925
 JitsiConference.prototype.setSubject = function(subject) {
925
 JitsiConference.prototype.setSubject = function(subject) {
926
     if (this.room && this.isModerator()) {
926
     if (this.room && this.isModerator()) {
927
         this.room.setSubject(subject);
927
         this.room.setSubject(subject);
928
+    } else {
929
+        logger.warn(`Failed to set subject, ${this.room ? '' : 'not in a room, '}${
930
+            this.isModerator() ? '' : 'participant is not a moderator'}`);
928
     }
931
     }
929
 };
932
 };
930
 
933
 
2406
  */
2409
  */
2407
 JitsiConference.prototype.setStartMutedPolicy = function(policy) {
2410
 JitsiConference.prototype.setStartMutedPolicy = function(policy) {
2408
     if (!this.isModerator()) {
2411
     if (!this.isModerator()) {
2412
+        logger.warn(`Failed to set start muted policy, ${this.room ? '' : 'not in a room, '}${
2413
+            this.isModerator() ? '' : 'participant is not a moderator'}`);
2414
+
2409
         return;
2415
         return;
2410
     }
2416
     }
2411
     this.startMutedPolicy = policy;
2417
     this.startMutedPolicy = policy;
3614
 JitsiConference.prototype.disableLobby = function() {
3620
 JitsiConference.prototype.disableLobby = function() {
3615
     if (this.room && this.isModerator()) {
3621
     if (this.room && this.isModerator()) {
3616
         this.room.getLobby().disable();
3622
         this.room.getLobby().disable();
3623
+    } else {
3624
+        logger.warn(`Failed to disable lobby, ${this.room ? '' : 'not in a room, '}${
3625
+            this.isModerator() ? '' : 'participant is not a moderator'}`);
3617
     }
3626
     }
3618
 };
3627
 };
3619
 
3628
 
3652
         this.room.getLobby().approveAccess(id);
3661
         this.room.getLobby().approveAccess(id);
3653
     }
3662
     }
3654
 };
3663
 };
3664
+
3665
+/**
3666
+ * Returns <tt>true</tt> if AV Moderation support is enabled in the backend.
3667
+ *
3668
+ * @returns {boolean} whether AV Moderation is supported in the backend.
3669
+ */
3670
+JitsiConference.prototype.isAVModerationSupported = function() {
3671
+    return Boolean(this.room && this.room.getAVModeration().isSupported());
3672
+};
3673
+
3674
+/**
3675
+ * Enables AV Moderation.
3676
+ * @param {MediaType} mediaType "audio" or "video"
3677
+ */
3678
+JitsiConference.prototype.enableAVModeration = function(mediaType) {
3679
+    if (this.room && this.isModerator()
3680
+        && (mediaType === MediaType.AUDIO || mediaType === MediaType.VIDEO)) {
3681
+        this.room.getAVModeration().enable(true, mediaType);
3682
+    } else {
3683
+        logger.warn(`Failed to enable AV moderation, ${this.room ? '' : 'not in a room, '}${
3684
+            this.isModerator() ? '' : 'participant is not a moderator, '}${
3685
+            this.room && this.isModerator() ? 'wrong media type passed' : ''}`);
3686
+    }
3687
+};
3688
+
3689
+/**
3690
+ * Disables AV Moderation.
3691
+ * @param {MediaType} mediaType "audio" or "video"
3692
+ */
3693
+JitsiConference.prototype.disableAVModeration = function(mediaType) {
3694
+    if (this.room && this.isModerator()
3695
+        && (mediaType === MediaType.AUDIO || mediaType === MediaType.VIDEO)) {
3696
+        this.room.getAVModeration().enable(false, mediaType);
3697
+    } else {
3698
+        logger.warn(`Failed to disable AV moderation, ${this.room ? '' : 'not in a room, '}${
3699
+            this.isModerator() ? '' : 'participant is not a moderator, '}${
3700
+            this.room && this.isModerator() ? 'wrong media type passed' : ''}`);
3701
+    }
3702
+};
3703
+
3704
+/**
3705
+ * Approve participant access to certain media, allows unmuting audio or video.
3706
+ *
3707
+ * @param {MediaType} mediaType "audio" or "video"
3708
+ * @param id the id of the participant.
3709
+ */
3710
+JitsiConference.prototype.avModerationApprove = function(mediaType, id) {
3711
+    if (this.room && this.isModerator()
3712
+        && (mediaType === MediaType.AUDIO || mediaType === MediaType.VIDEO)) {
3713
+
3714
+        const participant = this.getParticipantById(id);
3715
+
3716
+        if (!participant) {
3717
+            return;
3718
+        }
3719
+
3720
+        this.room.getAVModeration().approve(mediaType, participant.getJid());
3721
+    } else {
3722
+        logger.warn(`AV moderation skipped , ${this.room ? '' : 'not in a room, '}${
3723
+            this.isModerator() ? '' : 'participant is not a moderator, '}${
3724
+            this.room && this.isModerator() ? 'wrong media type passed' : ''}`);
3725
+    }
3726
+};

+ 24
- 0
JitsiConferenceEventManager.js Ver fichero

699
         createdTimestamp => {
699
         createdTimestamp => {
700
             conference.eventEmitter.emit(JitsiConferenceEvents.CONFERENCE_CREATED_TIMESTAMP, createdTimestamp);
700
             conference.eventEmitter.emit(JitsiConferenceEvents.CONFERENCE_CREATED_TIMESTAMP, createdTimestamp);
701
         });
701
         });
702
+
703
+    this._addConferenceXMPPListener(XMPPEvents.AV_MODERATION_CHANGED,
704
+        (value, mediaType, actorJid) => {
705
+            const actorParticipant = conference.getParticipants().find(p => p.getJid() === actorJid);
706
+
707
+            conference.eventEmitter.emit(JitsiConferenceEvents.AV_MODERATION_CHANGED, {
708
+                enabled: value,
709
+                mediaType,
710
+                actor: actorParticipant
711
+            });
712
+        });
713
+    this._addConferenceXMPPListener(XMPPEvents.AV_MODERATION_PARTICIPANT_APPROVED,
714
+        (mediaType, jid) => {
715
+            const participant = conference.getParticipantById(Strophe.getResourceFromJid(jid));
716
+
717
+            if (participant) {
718
+                conference.eventEmitter.emit(JitsiConferenceEvents.AV_MODERATION_PARTICIPANT_APPROVED, {
719
+                    participant,
720
+                    mediaType
721
+                });
722
+            }
723
+        });
724
+    this._addConferenceXMPPListener(XMPPEvents.AV_MODERATION_APPROVED,
725
+        value => conference.eventEmitter.emit(JitsiConferenceEvents.AV_MODERATION_APPROVED, { mediaType: value }));
702
 };
726
 };
703
 
727
 
704
 /**
728
 /**

+ 29
- 0
JitsiConferenceEvents.js Ver fichero

370
  * A user left the lobby room.
370
  * A user left the lobby room.
371
  */
371
  */
372
 export const LOBBY_USER_LEFT = 'conference.lobby.userLeft';
372
 export const LOBBY_USER_LEFT = 'conference.lobby.userLeft';
373
+
374
+/**
375
+ * The local participant was approved to be able to unmute.
376
+ * @param {options} event - {
377
+ *     {MediaType} mediaType
378
+ * }.
379
+ */
380
+export const AV_MODERATION_APPROVED = 'conference.av_moderation.approved';
381
+
382
+/**
383
+ * AV Moderation was enabled/disabled. The actor is the participant that is currently in the meeting,
384
+ * or undefined if that participant has left the meeting.
385
+ *
386
+ * @param {options} event - {
387
+ *     {boolean} enabled,
388
+ *     {MediaType} mediaType,
389
+ *     {JitsiParticipant} actor
390
+ * }.
391
+ */
392
+export const AV_MODERATION_CHANGED = 'conference.av_moderation.changed';
393
+
394
+/**
395
+ * AV Moderation, report for user being approved to unmute.
396
+ * @param {options} event - {
397
+ *     {JitsiParticipant} participant,
398
+ *     {MediaType} mediaType
399
+ * }.
400
+ */
401
+export const AV_MODERATION_PARTICIPANT_APPROVED = 'conference.av_moderation.participant.approved';

+ 123
- 0
modules/xmpp/AVModeration.js Ver fichero

1
+import { getLogger } from 'jitsi-meet-logger';
2
+import { $msg } from 'strophe.js';
3
+
4
+import * as MediaType from '../../service/RTC/MediaType';
5
+import XMPPEvents from '../../service/xmpp/XMPPEvents';
6
+
7
+const logger = getLogger(__filename);
8
+
9
+/**
10
+ * The AVModeration logic.
11
+ */
12
+export default class AVModeration {
13
+
14
+    /**
15
+     * Constructs AV moderation room.
16
+     *
17
+     * @param {ChatRoom} room the main room.
18
+     */
19
+    constructor(room) {
20
+        this._xmpp = room.xmpp;
21
+
22
+        this._mainRoom = room;
23
+
24
+        this._momderationEnabledByType = {
25
+            [MediaType.AUDIO]: false,
26
+            [MediaType.VIDEO]: false
27
+        };
28
+
29
+        this._whitelistAudio = [];
30
+        this._whitelistVideo = [];
31
+
32
+        this._xmpp.addListener(XMPPEvents.AV_MODERATION_RECEIVED, this._onMessage.bind(this));
33
+    }
34
+
35
+    /**
36
+     * Whether AV moderation is supported on backend.
37
+     *
38
+     * @returns {boolean} whether AV moderation is supported on backend.
39
+     */
40
+    isSupported() {
41
+        return Boolean(this._xmpp.avModerationComponentAddress);
42
+    }
43
+
44
+    /**
45
+     * Enables or disables AV Moderation by sending a msg with command to the component.
46
+     */
47
+    enable(state, mediaType) {
48
+        if (!this.isSupported() || !this._mainRoom.isModerator()) {
49
+            logger.error(`Cannot enable:${state} AV moderation supported:${this.isSupported()}, 
50
+                moderator:${this._mainRoom.isModerator()}`);
51
+
52
+            return;
53
+        }
54
+
55
+        if (state === this._momderationEnabledByType[mediaType]) {
56
+            logger.warn(`Moderation already in state:${state} for mediaType:${mediaType}`);
57
+
58
+            return;
59
+        }
60
+
61
+        // send the enable/disable message
62
+        const msg = $msg({ to: this._xmpp.avModerationComponentAddress });
63
+
64
+        msg.c('av_moderation', {
65
+            enable: state,
66
+            mediaType
67
+        }).up();
68
+
69
+        this._xmpp.connection.send(msg);
70
+    }
71
+
72
+    /**
73
+     * Approves that a participant can unmute by sending a msg with its jid to the component.
74
+     */
75
+    approve(mediaType, jid) {
76
+        if (!this.isSupported() || !this._mainRoom.isModerator()) {
77
+            logger.error(`Cannot approve in AV moderation supported:${this.isSupported()}, 
78
+                moderator:${this._mainRoom.isModerator()}`);
79
+
80
+            return;
81
+        }
82
+
83
+        // send a message to whitelist the jid and approve it to unmute
84
+        const msg = $msg({ to: this._xmpp.avModerationComponentAddress });
85
+
86
+        msg.c('av_moderation', {
87
+            mediaType,
88
+            jidToWhitelist: jid }).up();
89
+
90
+        this._xmpp.connection.send(msg);
91
+    }
92
+
93
+    /**
94
+     * Receives av_moderation parsed messages as json.
95
+     * @param obj the parsed json content of the message to process.
96
+     * @private
97
+     */
98
+    _onMessage(obj) {
99
+        const newWhitelists = obj.whitelists;
100
+
101
+        if (newWhitelists) {
102
+            const fireEventApprovedJids = (mediaType, oldList, newList) => {
103
+                newList.filter(x => !oldList.includes(x))
104
+                    .forEach(jid => this._xmpp.eventEmitter
105
+                        .emit(XMPPEvents.AV_MODERATION_PARTICIPANT_APPROVED, mediaType, jid));
106
+            };
107
+
108
+            if (newWhitelists[MediaType.AUDIO]) {
109
+                fireEventApprovedJids(MediaType.AUDIO, this._whitelistAudio, newWhitelists[MediaType.AUDIO]);
110
+            }
111
+
112
+            if (newWhitelists[MediaType.VIDEO]) {
113
+                fireEventApprovedJids(MediaType.VIDEO, this._whitelistVideo, newWhitelists[MediaType.VIDEO]);
114
+            }
115
+        } else if (this._momderationEnabledByType[obj.mediaType] !== obj.enabled) {
116
+            this._momderationEnabledByType[obj.mediaType] = obj.enabled;
117
+
118
+            this._xmpp.eventEmitter.emit(XMPPEvents.AV_MODERATION_CHANGED, obj.enabled, obj.mediaType, obj.actor);
119
+        } else if (obj.approved) {
120
+            this._xmpp.eventEmitter.emit(XMPPEvents.AV_MODERATION_APPROVED, obj.mediaType);
121
+        }
122
+    }
123
+}

+ 10
- 0
modules/xmpp/ChatRoom.js Ver fichero

10
 import GlobalOnErrorHandler from '../util/GlobalOnErrorHandler';
10
 import GlobalOnErrorHandler from '../util/GlobalOnErrorHandler';
11
 import Listenable from '../util/Listenable';
11
 import Listenable from '../util/Listenable';
12
 
12
 
13
+import AVModeration from './AVModeration';
13
 import Lobby from './Lobby';
14
 import Lobby from './Lobby';
14
 import XmppConnection from './XmppConnection';
15
 import XmppConnection from './XmppConnection';
15
 import Moderator from './moderator';
16
 import Moderator from './moderator';
133
         if (typeof this.options.enableLobby === 'undefined' || this.options.enableLobby) {
134
         if (typeof this.options.enableLobby === 'undefined' || this.options.enableLobby) {
134
             this.lobby = new Lobby(this);
135
             this.lobby = new Lobby(this);
135
         }
136
         }
137
+        this.avModeration = new AVModeration(this);
136
         this.initPresenceMap(options);
138
         this.initPresenceMap(options);
137
         this.lastPresences = {};
139
         this.lastPresences = {};
138
         this.phoneNumber = null;
140
         this.phoneNumber = null;
1680
         return this.lobby;
1682
         return this.lobby;
1681
     }
1683
     }
1682
 
1684
 
1685
+    /**
1686
+     * @returns {AVModeration}
1687
+     */
1688
+    getAVModeration() {
1689
+        return this.avModeration;
1690
+    }
1691
+
1692
+
1683
     /**
1693
     /**
1684
      * Returns the phone number for joining the conference.
1694
      * Returns the phone number for joining the conference.
1685
      */
1695
      */

+ 2
- 1
modules/xmpp/ChatRoom.spec.js Ver fichero

138
 
138
 
139
         beforeEach(() => {
139
         beforeEach(() => {
140
             const xmpp = {
140
             const xmpp = {
141
-                options: {}
141
+                options: {},
142
+                addListener: () => {} // eslint-disable-line no-empty-function
142
             };
143
             };
143
 
144
 
144
             room = new ChatRoom(
145
             room = new ChatRoom(

+ 27
- 13
modules/xmpp/xmpp.js Ver fichero

408
     _processDiscoInfoIdentities(identities, features) {
408
     _processDiscoInfoIdentities(identities, features) {
409
         // check for speakerstats
409
         // check for speakerstats
410
         identities.forEach(identity => {
410
         identities.forEach(identity => {
411
+            if (identity.type === 'av_moderation') {
412
+                this.avModerationComponentAddress = identity.name;
413
+            }
414
+
411
             if (identity.type === 'speakerstats') {
415
             if (identity.type === 'speakerstats') {
412
                 this.speakerStatsComponentAddress = identity.name;
416
                 this.speakerStatsComponentAddress = identity.name;
413
             }
417
             }
436
             }
440
             }
437
         });
441
         });
438
 
442
 
439
-        if (this.speakerStatsComponentAddress
443
+        if (this.avModerationComponentAddress
444
+            || this.speakerStatsComponentAddress
440
             || this.conferenceDurationComponentAddress) {
445
             || this.conferenceDurationComponentAddress) {
441
             this.connection.addHandler(this._onPrivateMessage.bind(this), null, 'message', null, null);
446
             this.connection.addHandler(this._onPrivateMessage.bind(this), null, 'message', null, null);
442
         }
447
         }
522
      * @private
527
      * @private
523
      */
528
      */
524
     _onSystemMessage(msg) {
529
     _onSystemMessage(msg) {
530
+        // proceed only if the message has any of the expected information
531
+        if ($(msg).find('>services').length === 0 && $(msg).find('>query').length === 0) {
532
+            return;
533
+        }
534
+
525
         this.sendDiscoInfo = false;
535
         this.sendDiscoInfo = false;
526
 
536
 
527
         const foundIceServers = this.connection.jingle.onReceiveStunAndTurnCredentials(msg);
537
         const foundIceServers = this.connection.jingle.onReceiveStunAndTurnCredentials(msg);
869
      * the json object. Otherwise, returns false.
879
      * the json object. Otherwise, returns false.
870
      */
880
      */
871
     tryParseJSONAndVerify(jsonString) {
881
     tryParseJSONAndVerify(jsonString) {
882
+        // ignore empty strings, like message errors
883
+        if (!jsonString) {
884
+            return false;
885
+        }
886
+
872
         try {
887
         try {
873
             const json = JSON.parse(jsonString);
888
             const json = JSON.parse(jsonString);
874
 
889
 
890
                     + 'structure', 'topic: ', type);
905
                     + 'structure', 'topic: ', type);
891
             }
906
             }
892
         } catch (e) {
907
         } catch (e) {
893
-            logger.error(e);
908
+            logger.error(`Error parsing json ${jsonString}`, e);
894
 
909
 
895
             return false;
910
             return false;
896
         }
911
         }
909
         const from = msg.getAttribute('from');
924
         const from = msg.getAttribute('from');
910
 
925
 
911
         if (!(from === this.speakerStatsComponentAddress
926
         if (!(from === this.speakerStatsComponentAddress
912
-            || from === this.conferenceDurationComponentAddress)) {
927
+            || from === this.conferenceDurationComponentAddress
928
+            || from === this.avModerationComponentAddress)) {
913
             return true;
929
             return true;
914
         }
930
         }
915
 
931
 
917
             .text();
933
             .text();
918
         const parsedJson = this.tryParseJSONAndVerify(jsonMessage);
934
         const parsedJson = this.tryParseJSONAndVerify(jsonMessage);
919
 
935
 
920
-        if (parsedJson
921
-            && parsedJson[JITSI_MEET_MUC_TYPE] === 'speakerstats'
922
-            && parsedJson.users) {
923
-            this.eventEmitter.emit(
924
-                XMPPEvents.SPEAKER_STATS_RECEIVED, parsedJson.users);
936
+        if (!parsedJson) {
937
+            return true;
925
         }
938
         }
926
 
939
 
927
-        if (parsedJson
928
-            && parsedJson[JITSI_MEET_MUC_TYPE] === 'conference_duration'
929
-            && parsedJson.created_timestamp) {
930
-            this.eventEmitter.emit(
931
-                XMPPEvents.CONFERENCE_TIMESTAMP_RECEIVED, parsedJson.created_timestamp);
940
+        if (parsedJson[JITSI_MEET_MUC_TYPE] === 'speakerstats' && parsedJson.users) {
941
+            this.eventEmitter.emit(XMPPEvents.SPEAKER_STATS_RECEIVED, parsedJson.users);
942
+        } else if (parsedJson[JITSI_MEET_MUC_TYPE] === 'conference_duration' && parsedJson.created_timestamp) {
943
+            this.eventEmitter.emit(XMPPEvents.CONFERENCE_TIMESTAMP_RECEIVED, parsedJson.created_timestamp);
944
+        } else if (parsedJson[JITSI_MEET_MUC_TYPE] === 'av_moderation') {
945
+            this.eventEmitter.emit(XMPPEvents.AV_MODERATION_RECEIVED, parsedJson);
932
         }
946
         }
933
 
947
 
934
         return true;
948
         return true;

+ 20
- 0
service/xmpp/XMPPEvents.js Ver fichero

248
      */
248
      */
249
     CONFERENCE_TIMESTAMP_RECEIVED: 'xmpp.conference_timestamp_received',
249
     CONFERENCE_TIMESTAMP_RECEIVED: 'xmpp.conference_timestamp_received',
250
 
250
 
251
+    /**
252
+     * Event fired when we receive a message for AV moderation approved for the local participant.
253
+     */
254
+    AV_MODERATION_APPROVED: 'xmpp.av_moderation.approved',
255
+
256
+    /**
257
+     * Event fired when we receive a message for AV moderation.
258
+     */
259
+    AV_MODERATION_RECEIVED: 'xmpp.av_moderation.received',
260
+
261
+    /**
262
+     * Event fired when the moderation enable/disable changes.
263
+     */
264
+    AV_MODERATION_CHANGED: 'xmpp.av_moderation.changed',
265
+
266
+    /**
267
+     * Event fired when we receive message that a new jid was approved.
268
+     */
269
+    AV_MODERATION_PARTICIPANT_APPROVED: 'xmpp.av_moderation.participant.approved',
270
+
251
     // Designates an event indicating that we should join the conference with
271
     // Designates an event indicating that we should join the conference with
252
     // audio and/or video muted.
272
     // audio and/or video muted.
253
     START_MUTED_FROM_FOCUS: 'xmpp.start_muted_from_focus',
273
     START_MUTED_FROM_FOCUS: 'xmpp.start_muted_from_focus',

Loading…
Cancelar
Guardar