소스 검색

feat(av-moderation) Ask to Unmute and remove from Whitelist (#10043)

* feat(av-moderation) Ask to Unmute and remove from Whitelist

Make Ask to Unmute work without moderation
Add remove from moderation whitelist functionality

* chore(deps) lib-jitsi-meet@latest

* feat(av-moderation) Remove from moderation whitelist functionality (#1729)
* fix(chore) corrected typo in log message
* fix(e2ee) replace nullish coalescing with or
* fix(e2ee) restore initial key when RATCHET_WINDOW_SIZE reached

3b8baa9d3b/...0646bc3403807dbf1370c88f028d9e0a16bcab1a

Co-authored-by: Дамян Минков <damencho@jitsi.org>
master
robertpin 3 년 전
부모
커밋
ace53c880b
No account linked to committer's email address

+ 3
- 3
lang/main.json 파일 보기

@@ -271,7 +271,7 @@
271 271
         "muteParticipantDialog": "Are you sure you want to mute this participant? You won't be able to unmute them, but they can unmute themselves at any time.",
272 272
         "muteParticipantsVideoDialog": "Are you sure you want to turn off this participant's camera? You won't be able to turn the camera back on, but they can turn it back on at any time.",
273 273
         "muteParticipantTitle": "Mute this participant?",
274
-        "muteParticipantsVideoButton": "Stop camera",
274
+        "muteParticipantsVideoButton": "Stop video",
275 275
         "muteParticipantsVideoTitle": "Disable camera of this participant?",
276 276
         "muteParticipantsVideoBody": "You won't be able to turn the camera back on, but they can turn it back on at any time.",
277 277
         "noDropboxToken": "No valid Dropbox token",
@@ -897,8 +897,8 @@
897 897
             "mute": "Mute / Unmute",
898 898
             "muteEveryone": "Mute everyone",
899 899
             "muteEveryoneElse": "Mute everyone else",
900
-            "muteEveryonesVideo": "Disable everyone's camera",
901
-            "muteEveryoneElsesVideo": "Disable everyone else's camera",
900
+            "muteEveryonesVideo": "Disable everyone's video",
901
+            "muteEveryoneElsesVideo": "Disable everyone else's video",
902 902
             "participants": "Participants",
903 903
             "pip": "Toggle Picture-in-Picture mode",
904 904
             "privateMessage": "Send private message",

+ 2
- 2
package-lock.json 파일 보기

@@ -11117,8 +11117,8 @@
11117 11117
       }
11118 11118
     },
11119 11119
     "lib-jitsi-meet": {
11120
-      "version": "github:jitsi/lib-jitsi-meet#3b8baa9d3be2839510abaa954357d0b0ab023649",
11121
-      "from": "github:jitsi/lib-jitsi-meet#3b8baa9d3be2839510abaa954357d0b0ab023649",
11120
+      "version": "github:jitsi/lib-jitsi-meet#0646bc3403807dbf1370c88f028d9e0a16bcab1a",
11121
+      "from": "github:jitsi/lib-jitsi-meet#0646bc3403807dbf1370c88f028d9e0a16bcab1a",
11122 11122
       "requires": {
11123 11123
         "@jitsi/js-utils": "1.0.2",
11124 11124
         "@jitsi/sdp-interop": "github:jitsi/sdp-interop#4669790bb9020cc8f10c1d1f3823c26b08497547",

+ 1
- 1
package.json 파일 보기

@@ -59,7 +59,7 @@
59 59
     "jquery-i18next": "1.2.1",
60 60
     "js-md5": "0.6.1",
61 61
     "jwt-decode": "2.2.0",
62
-    "lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#3b8baa9d3be2839510abaa954357d0b0ab023649",
62
+    "lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#0646bc3403807dbf1370c88f028d9e0a16bcab1a",
63 63
     "libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
64 64
     "lodash": "4.17.21",
65 65
     "moment": "2.29.1",

+ 21
- 0
react/features/av-moderation/actionTypes.js 파일 보기

@@ -74,6 +74,16 @@ export const REQUEST_ENABLE_VIDEO_MODERATION = 'REQUEST_ENABLE_VIDEO_MODERATION'
74 74
  */
75 75
 export const LOCAL_PARTICIPANT_APPROVED = 'LOCAL_PARTICIPANT_APPROVED';
76 76
 
77
+/**
78
+ * The type of (redux) action which signals that the local participant had been blocked.
79
+ *
80
+ * {
81
+ *     type: LOCAL_PARTICIPANT_REJECTED,
82
+ *     mediaType: MediaType
83
+ * }
84
+ */
85
+export const LOCAL_PARTICIPANT_REJECTED = 'LOCAL_PARTICIPANT_REJECTED';
86
+
77 87
 /**
78 88
  * The type of (redux) action which signals to show notification to the local participant.
79 89
  *
@@ -94,6 +104,17 @@ export const LOCAL_PARTICIPANT_MODERATION_NOTIFICATION = 'LOCAL_PARTICIPANT_MODE
94 104
  */
95 105
 export const PARTICIPANT_APPROVED = 'PARTICIPANT_APPROVED';
96 106
 
107
+/**
108
+ * The type of (redux) action which signals that a participant was blocked for a media type.
109
+ *
110
+ * {
111
+ *     type: PARTICIPANT_REJECTED,
112
+ *     mediaType: MediaType
113
+ *     participantId: String
114
+ * }
115
+ */
116
+export const PARTICIPANT_REJECTED = 'PARTICIPANT_REJECTED';
117
+
97 118
 
98 119
 /**
99 120
  * The type of (redux) action which signals that a participant asked to have its audio umuted.

+ 81
- 4
react/features/av-moderation/actions.js 파일 보기

@@ -2,7 +2,7 @@
2 2
 
3 3
 import { getConferenceState } from '../base/conference';
4 4
 import { MEDIA_TYPE, type MediaType } from '../base/media/constants';
5
-import { getParticipantById } from '../base/participants';
5
+import { getParticipantById, isParticipantModerator } from '../base/participants';
6 6
 import { isForceMuted } from '../participants-pane/functions';
7 7
 
8 8
 import {
@@ -16,7 +16,9 @@ import {
16 16
     REQUEST_DISABLE_AUDIO_MODERATION,
17 17
     REQUEST_ENABLE_AUDIO_MODERATION,
18 18
     REQUEST_DISABLE_VIDEO_MODERATION,
19
-    REQUEST_ENABLE_VIDEO_MODERATION
19
+    REQUEST_ENABLE_VIDEO_MODERATION,
20
+    LOCAL_PARTICIPANT_REJECTED,
21
+    PARTICIPANT_REJECTED
20 22
 } from './actionTypes';
21 23
 import { isEnabledFromState } from './functions';
22 24
 
@@ -33,15 +35,57 @@ export const approveParticipant = (id: string) => (dispatch: Function, getState:
33 35
 
34 36
     const isAudioForceMuted = isForceMuted(participant, MEDIA_TYPE.AUDIO, state);
35 37
     const isVideoForceMuted = isForceMuted(participant, MEDIA_TYPE.VIDEO, state);
38
+    const isAudioModerationOn = isEnabledFromState(MEDIA_TYPE.AUDIO, state);
39
+    const isVideoModerationOn = isEnabledFromState(MEDIA_TYPE.VIDEO, state);
36 40
 
37
-    if (isEnabledFromState(MEDIA_TYPE.AUDIO, state) && isAudioForceMuted) {
41
+    if (!(isAudioModerationOn || isVideoModerationOn) || (isAudioModerationOn && isAudioForceMuted)) {
38 42
         conference.avModerationApprove(MEDIA_TYPE.AUDIO, id);
39 43
     }
40
-    if (isEnabledFromState(MEDIA_TYPE.VIDEO, state) && isVideoForceMuted) {
44
+    if (isVideoModerationOn && isVideoForceMuted) {
41 45
         conference.avModerationApprove(MEDIA_TYPE.VIDEO, id);
42 46
     }
43 47
 };
44 48
 
49
+/**
50
+ * Action used by moderator to reject audio for a participant.
51
+ *
52
+ * @param {staring} id - The id of the participant to be rejected.
53
+ * @returns {void}
54
+ */
55
+export const rejectParticipantAudio = (id: string) => (dispatch: Function, getState: Function) => {
56
+    const state = getState();
57
+    const { conference } = getConferenceState(state);
58
+    const audioModeration = isEnabledFromState(MEDIA_TYPE.AUDIO, state);
59
+
60
+    const participant = getParticipantById(state, id);
61
+    const isAudioForceMuted = isForceMuted(participant, MEDIA_TYPE.AUDIO, state);
62
+    const isModerator = isParticipantModerator(participant);
63
+
64
+    if (audioModeration && !isAudioForceMuted && !isModerator) {
65
+        conference.avModerationReject(MEDIA_TYPE.AUDIO, id);
66
+    }
67
+};
68
+
69
+/**
70
+ * Action used by moderator to reject video for a participant.
71
+ *
72
+ * @param {staring} id - The id of the participant to be rejected.
73
+ * @returns {void}
74
+ */
75
+export const rejectParticipantVideo = (id: string) => (dispatch: Function, getState: Function) => {
76
+    const state = getState();
77
+    const { conference } = getConferenceState(state);
78
+    const videoModeration = isEnabledFromState(MEDIA_TYPE.VIDEO, state);
79
+
80
+    const participant = getParticipantById(state, id);
81
+    const isVideoForceMuted = isForceMuted(participant, MEDIA_TYPE.VIDEO, state);
82
+    const isModerator = isParticipantModerator(participant);
83
+
84
+    if (videoModeration && !isVideoForceMuted && !isModerator) {
85
+        conference.avModerationReject(MEDIA_TYPE.VIDEO, id);
86
+    }
87
+};
88
+
45 89
 /**
46 90
  * Audio or video moderation is disabled.
47 91
  *
@@ -169,6 +213,21 @@ export const localParticipantApproved = (mediaType: MediaType) => {
169 213
     };
170 214
 };
171 215
 
216
+/**
217
+ * Local participant was blocked to be able to unmute audio and video.
218
+ *
219
+ * @param {MediaType} mediaType - The media type to disable.
220
+ * @returns {{
221
+ *     type: LOCAL_PARTICIPANT_REJECTED
222
+ * }}
223
+ */
224
+export const localParticipantRejected = (mediaType: MediaType) => {
225
+    return {
226
+        type: LOCAL_PARTICIPANT_REJECTED,
227
+        mediaType
228
+    };
229
+};
230
+
172 231
 /**
173 232
  * Shows notification when A/V moderation is enabled and local participant is still not approved.
174 233
  *
@@ -211,3 +270,21 @@ export function participantApproved(id: string, mediaType: MediaType) {
211 270
         mediaType
212 271
     };
213 272
 }
273
+
274
+/**
275
+ * A participant was blocked to unmute for a mediaType.
276
+ *
277
+ * @param {string} id - The id of the approved participant.
278
+ * @param {MediaType} mediaType - The media type which was approved.
279
+ * @returns {{
280
+ *     type: PARTICIPANT_REJECTED,
281
+ * }}
282
+ */
283
+export function participantRejected(id: string, mediaType: MediaType) {
284
+    return {
285
+        type: PARTICIPANT_REJECTED,
286
+        id,
287
+        mediaType
288
+    };
289
+}
290
+

+ 16
- 1
react/features/av-moderation/middleware.js 파일 보기

@@ -35,7 +35,9 @@ import {
35 35
     enableModeration,
36 36
     localParticipantApproved,
37 37
     participantApproved,
38
-    participantPendingAudio
38
+    participantPendingAudio,
39
+    localParticipantRejected,
40
+    participantRejected
39 41
 } from './actions';
40 42
 import {
41 43
     ASKED_TO_UNMUTE_SOUND_ID, AUDIO_MODERATION_NOTIFICATION_ID,
@@ -176,6 +178,10 @@ StateListenerRegistry.register(
176 178
                 }
177 179
             });
178 180
 
181
+            conference.on(JitsiConferenceEvents.AV_MODERATION_REJECTED, ({ mediaType }) => {
182
+                dispatch(localParticipantRejected(mediaType));
183
+            });
184
+
179 185
             conference.on(JitsiConferenceEvents.AV_MODERATION_CHANGED, ({ enabled, mediaType, actor }) => {
180 186
                 enabled ? dispatch(enableModeration(mediaType, actor)) : dispatch(disableModeration(mediaType, actor));
181 187
             });
@@ -194,5 +200,14 @@ StateListenerRegistry.register(
194 200
                         dispatch(dismissPendingParticipant(id, mediaType));
195 201
                     });
196 202
                 });
203
+
204
+            // this is received by moderators
205
+            conference.on(
206
+                JitsiConferenceEvents.AV_MODERATION_PARTICIPANT_REJECTED,
207
+                ({ participant, mediaType }) => {
208
+                    const { _id: id } = participant;
209
+
210
+                    dispatch(participantRejected(id, mediaType));
211
+                });
197 212
         }
198 213
     });

+ 39
- 1
react/features/av-moderation/reducer.js 파일 보기

@@ -13,8 +13,10 @@ import {
13 13
     DISMISS_PENDING_PARTICIPANT,
14 14
     ENABLE_MODERATION,
15 15
     LOCAL_PARTICIPANT_APPROVED,
16
+    LOCAL_PARTICIPANT_REJECTED,
16 17
     PARTICIPANT_APPROVED,
17
-    PARTICIPANT_PENDING_AUDIO
18
+    PARTICIPANT_PENDING_AUDIO,
19
+    PARTICIPANT_REJECTED
18 20
 } from './actionTypes';
19 21
 import { MEDIA_TYPE_TO_PENDING_STORE_KEY } from './constants';
20 22
 
@@ -105,6 +107,16 @@ ReducerRegistry.register('features/av-moderation', (state = initialState, action
105 107
         };
106 108
     }
107 109
 
110
+    case LOCAL_PARTICIPANT_REJECTED: {
111
+        const newState = action.mediaType === MEDIA_TYPE.AUDIO
112
+            ? { audioUnmuteApproved: false } : { videoUnmuteApproved: false };
113
+
114
+        return {
115
+            ...state,
116
+            ...newState
117
+        };
118
+    }
119
+
108 120
     case PARTICIPANT_PENDING_AUDIO: {
109 121
         const { participant } = action;
110 122
 
@@ -228,6 +240,32 @@ ReducerRegistry.register('features/av-moderation', (state = initialState, action
228 240
         return state;
229 241
     }
230 242
 
243
+    case PARTICIPANT_REJECTED: {
244
+        const { mediaType, id } = action;
245
+
246
+        if (mediaType === MEDIA_TYPE.AUDIO) {
247
+            return {
248
+                ...state,
249
+                audioWhitelist: {
250
+                    ...state.audioWhitelist,
251
+                    [id]: false
252
+                }
253
+            };
254
+        }
255
+
256
+        if (mediaType === MEDIA_TYPE.VIDEO) {
257
+            return {
258
+                ...state,
259
+                videoWhitelist: {
260
+                    ...state.videoWhitelist,
261
+                    [id]: false
262
+                }
263
+            };
264
+        }
265
+
266
+        return state;
267
+    }
268
+
231 269
     }
232 270
 
233 271
     return state;

+ 1
- 4
react/features/participants-pane/components/ParticipantQuickAction.js 파일 보기

@@ -63,15 +63,12 @@ export default function ParticipantQuickAction({
63 63
             </QuickActionButton>
64 64
         );
65 65
     }
66
-    case QUICK_ACTION_BUTTON.ASK_TO_UNMUTE: {
66
+    default: {
67 67
         return (
68 68
             <AskToUnmuteButton
69 69
                 askUnmuteText = { askUnmuteText }
70 70
                 participantID = { participantID } />
71 71
         );
72 72
     }
73
-    default: {
74
-        return null;
75
-    }
76 73
     }
77 74
 }

+ 2
- 0
react/features/participants-pane/components/web/MeetingParticipants.js 파일 보기

@@ -4,6 +4,7 @@ import React, { useCallback, useRef, useState } from 'react';
4 4
 import { useTranslation } from 'react-i18next';
5 5
 import { useDispatch } from 'react-redux';
6 6
 
7
+import { rejectParticipantAudio } from '../../../av-moderation/actions';
7 8
 import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
8 9
 import { MEDIA_TYPE } from '../../../base/media';
9 10
 import {
@@ -104,6 +105,7 @@ function MeetingParticipants({ participantsCount, showInviteButton, overflowDraw
104 105
 
105 106
     const muteAudio = useCallback(id => () => {
106 107
         dispatch(muteRemote(id, MEDIA_TYPE.AUDIO));
108
+        dispatch(rejectParticipantAudio(id));
107 109
     }, [ dispatch ]);
108 110
     const [ drawerParticipant, closeDrawer, openDrawerForParticipant ] = useParticipantDrawer();
109 111
 

+ 6
- 1
react/features/video-menu/actions.any.js 파일 보기

@@ -10,7 +10,7 @@ import {
10 10
     sendAnalytics,
11 11
     VIDEO_MUTE
12 12
 } from '../analytics';
13
-import { showModeratedNotification } from '../av-moderation/actions';
13
+import { rejectParticipantAudio, rejectParticipantVideo, showModeratedNotification } from '../av-moderation/actions';
14 14
 import { shouldShowModeratedNotification } from '../av-moderation/functions';
15 15
 import {
16 16
     MEDIA_TYPE,
@@ -112,6 +112,11 @@ export function muteAllParticipants(exclude: Array<string>, mediaType: MEDIA_TYP
112 112
             }
113 113
 
114 114
             dispatch(muteRemote(id, mediaType));
115
+            if (mediaType === MEDIA_TYPE.AUDIO) {
116
+                dispatch(rejectParticipantAudio(id));
117
+            } else {
118
+                dispatch(rejectParticipantVideo(id));
119
+            }
115 120
         });
116 121
     };
117 122
 }

+ 2
- 0
react/features/video-menu/components/AbstractMuteRemoteParticipantsVideoDialog.js 파일 보기

@@ -2,6 +2,7 @@
2 2
 
3 3
 import { Component } from 'react';
4 4
 
5
+import { rejectParticipantVideo } from '../../av-moderation/actions';
5 6
 import { MEDIA_TYPE } from '../../base/media';
6 7
 import { muteRemote } from '../actions';
7 8
 
@@ -59,6 +60,7 @@ export default class AbstractMuteRemoteParticipantsVideoDialog<P:Props = Props,
59 60
         const { dispatch, participantID } = this.props;
60 61
 
61 62
         dispatch(muteRemote(participantID, MEDIA_TYPE.VIDEO));
63
+        dispatch(rejectParticipantVideo(participantID));
62 64
 
63 65
         return true;
64 66
     }

+ 68
- 10
resources/prosody-plugins/mod_av_moderation_component.lua 파일 보기

@@ -13,6 +13,17 @@ end
13 13
 
14 14
 module:log('info', 'Starting av_moderation for %s', muc_component_host);
15 15
 
16
+-- Returns the index of the given element in the table
17
+-- @param table in which to look
18
+-- @param elem the element for which to find the index
19
+function get_index_in_table(table, elem)
20
+    for index, value in pairs(table) do
21
+        if value == elem then
22
+            return index
23
+        end
24
+    end
25
+end
26
+
16 27
 -- Sends a json-message to the destination jid
17 28
 -- @param to_jid the destination jid
18 29
 -- @param json_message the message content to send
@@ -46,20 +57,22 @@ function notify_occupants_enable(jid, enable, room, actorJid, mediaType)
46 57
     end
47 58
 end
48 59
 
60
+-- Notifies about a change to the whitelist. Notifies all moderators and admin and the jid itself
49 61
 -- @param jid the jid to notify about the change
50 62
 -- @param moderators whether to notify all moderators in the room
51 63
 -- @param room the room where to send it
52 64
 -- @param mediaType used only when a participant is approved (not sent to moderators)
53
-function notify_whitelist_change(jid, moderators, room, mediaType)
65
+-- @param removed whether the jid is removed or added
66
+function notify_whitelist_change(jid, moderators, room, mediaType, removed)
54 67
     local body_json = {};
55 68
     body_json.type = 'av_moderation';
56 69
     body_json.room = internal_room_jid_match_rewrite(room.jid);
57 70
     body_json.whitelists = room.av_moderation;
71
+    body_json.removed = removed;
72
+    body_json.mediaType = mediaType;
58 73
     local moderators_body_json_str = json.encode(body_json);
59 74
     body_json.whitelists = nil;
60 75
     body_json.approved = true; -- we want to send to participants only that they were approved to unmute
61
-    body_json.mediaType = mediaType;
62 76
     local participant_body_json_str = json.encode(body_json);
63 77
 
64 78
     for _, occupant in room:each_occupant() do
@@ -77,6 +90,22 @@ function notify_whitelist_change(jid, moderators, room, mediaType)
77 90
     end
78 91
 end
79 92
 
93
+-- Notifies jid that is approved. This is a moderator to jid message to ask to unmute,
94
+-- @param jid the jid to notify about the change
95
+-- @param from the jid that triggered this
96
+-- @param room the room where to send it
97
+-- @param mediaType the mediaType it was approved for
98
+function notify_jid_approved(jid, from, room, mediaType)
99
+    local body_json = {};
100
+    body_json.type = 'av_moderation';
101
+    body_json.room = internal_room_jid_match_rewrite(room.jid);
102
+    body_json.approved = true; -- we want to send to participants only that they were approved to unmute
103
+    body_json.mediaType = mediaType;
104
+    body_json.from = from;
105
+
106
+    send_json_message(jid, json.encode(body_json));
107
+end
108
+
80 109
 -- receives messages from clients to the component sending A/V moderation enable/disable commands or adding
81 110
 -- jids to the whitelist
82 111
 function on_message(event)
@@ -166,7 +195,7 @@ function on_message(event)
166 195
             -- send message to all occupants
167 196
             notify_occupants_enable(nil, enabled, room, occupant.nick, mediaType);
168 197
             return true;
169
-        elseif moderation_command.attr.jidToWhitelist and room.av_moderation then
198
+        elseif moderation_command.attr.jidToWhitelist then
170 199
             local occupant_jid = moderation_command.attr.jidToWhitelist;
171 200
             -- check if jid is in the room, if so add it to whitelist
172 201
             -- inform all moderators and admins and the jid
@@ -176,16 +205,44 @@ function on_message(event)
176 205
                 return false;
177 206
             end
178 207
 
179
-            local whitelist = room.av_moderation[mediaType];
180
-            if not whitelist then
181
-                whitelist = {};
182
-                room.av_moderation[mediaType] = whitelist;
208
+            if room.av_moderation then
209
+                local whitelist = room.av_moderation[mediaType];
210
+                if not whitelist then
211
+                    whitelist = {};
212
+                    room.av_moderation[mediaType] = whitelist;
213
+                end
214
+                table.insert(whitelist, occupant_jid);
215
+
216
+                notify_whitelist_change(occupant_to_add.jid, true, room, mediaType, false);
217
+
218
+                return true;
219
+            else
220
+                -- this is a moderator asking the jid to unmute without enabling av moderation
221
+                -- let's just send the event
222
+                notify_jid_approved(occupant_to_add.jid, occupant.nick, room, mediaType);
223
+            end
224
+        elseif moderation_command.attr.jidToBlacklist then
225
+            local occupant_jid = moderation_command.attr.jidToBlacklist;
226
+            -- check if jid is in the room, if so remove it from the whitelist
227
+            -- inform all moderators and admins
228
+            local occupant_to_remove = room:get_occupant_by_nick(room_jid_match_rewrite(occupant_jid));
229
+            if not occupant_to_remove then
230
+                module:log('warn', 'No occupant %s found for %s', occupant_jid, room.jid);
231
+                return false;
183 232
             end
184
-            table.insert(whitelist, occupant_jid);
185 233
 
186
-            notify_whitelist_change(occupant_to_add.jid, true, room, mediaType);
234
+            if room.av_moderation then
235
+                local whitelist = room.av_moderation[mediaType];
236
+                if whitelist then
237
+                    local index = get_index_in_table(whitelist, occupant_jid)
238
+                    if(index) then
239
+                        table.remove(whitelist, index);
240
+                        notify_whitelist_change(occupant_to_remove.jid, true, room, mediaType, true);
241
+                    end
242
+                end
187 243
 
188
-            return true;
244
+                return true;
245
+            end
189 246
         end
190 247
     end
191 248
 

Loading…
취소
저장