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...0646bc3403

Co-authored-by: Дамян Минков <damencho@jitsi.org>
This commit is contained in:
robertpin 2021-09-28 19:11:13 +03:00 committed by GitHub
parent e3ac52908a
commit ace53c880b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 242 additions and 28 deletions

View File

@ -271,7 +271,7 @@
"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.", "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.",
"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.", "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.",
"muteParticipantTitle": "Mute this participant?", "muteParticipantTitle": "Mute this participant?",
"muteParticipantsVideoButton": "Stop camera", "muteParticipantsVideoButton": "Stop video",
"muteParticipantsVideoTitle": "Disable camera of this participant?", "muteParticipantsVideoTitle": "Disable camera of this participant?",
"muteParticipantsVideoBody": "You won't be able to turn the camera back on, but they can turn it back on at any time.", "muteParticipantsVideoBody": "You won't be able to turn the camera back on, but they can turn it back on at any time.",
"noDropboxToken": "No valid Dropbox token", "noDropboxToken": "No valid Dropbox token",
@ -897,8 +897,8 @@
"mute": "Mute / Unmute", "mute": "Mute / Unmute",
"muteEveryone": "Mute everyone", "muteEveryone": "Mute everyone",
"muteEveryoneElse": "Mute everyone else", "muteEveryoneElse": "Mute everyone else",
"muteEveryonesVideo": "Disable everyone's camera", "muteEveryonesVideo": "Disable everyone's video",
"muteEveryoneElsesVideo": "Disable everyone else's camera", "muteEveryoneElsesVideo": "Disable everyone else's video",
"participants": "Participants", "participants": "Participants",
"pip": "Toggle Picture-in-Picture mode", "pip": "Toggle Picture-in-Picture mode",
"privateMessage": "Send private message", "privateMessage": "Send private message",

4
package-lock.json generated
View File

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

View File

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

View File

@ -74,6 +74,16 @@ export const REQUEST_ENABLE_VIDEO_MODERATION = 'REQUEST_ENABLE_VIDEO_MODERATION'
*/ */
export const LOCAL_PARTICIPANT_APPROVED = 'LOCAL_PARTICIPANT_APPROVED'; export const LOCAL_PARTICIPANT_APPROVED = 'LOCAL_PARTICIPANT_APPROVED';
/**
* The type of (redux) action which signals that the local participant had been blocked.
*
* {
* type: LOCAL_PARTICIPANT_REJECTED,
* mediaType: MediaType
* }
*/
export const LOCAL_PARTICIPANT_REJECTED = 'LOCAL_PARTICIPANT_REJECTED';
/** /**
* The type of (redux) action which signals to show notification to the local participant. * The type of (redux) action which signals to show notification to the local participant.
* *
@ -94,6 +104,17 @@ export const LOCAL_PARTICIPANT_MODERATION_NOTIFICATION = 'LOCAL_PARTICIPANT_MODE
*/ */
export const PARTICIPANT_APPROVED = 'PARTICIPANT_APPROVED'; export const PARTICIPANT_APPROVED = 'PARTICIPANT_APPROVED';
/**
* The type of (redux) action which signals that a participant was blocked for a media type.
*
* {
* type: PARTICIPANT_REJECTED,
* mediaType: MediaType
* participantId: String
* }
*/
export const PARTICIPANT_REJECTED = 'PARTICIPANT_REJECTED';
/** /**
* The type of (redux) action which signals that a participant asked to have its audio umuted. * The type of (redux) action which signals that a participant asked to have its audio umuted.

View File

@ -2,7 +2,7 @@
import { getConferenceState } from '../base/conference'; import { getConferenceState } from '../base/conference';
import { MEDIA_TYPE, type MediaType } from '../base/media/constants'; import { MEDIA_TYPE, type MediaType } from '../base/media/constants';
import { getParticipantById } from '../base/participants'; import { getParticipantById, isParticipantModerator } from '../base/participants';
import { isForceMuted } from '../participants-pane/functions'; import { isForceMuted } from '../participants-pane/functions';
import { import {
@ -16,7 +16,9 @@ import {
REQUEST_DISABLE_AUDIO_MODERATION, REQUEST_DISABLE_AUDIO_MODERATION,
REQUEST_ENABLE_AUDIO_MODERATION, REQUEST_ENABLE_AUDIO_MODERATION,
REQUEST_DISABLE_VIDEO_MODERATION, REQUEST_DISABLE_VIDEO_MODERATION,
REQUEST_ENABLE_VIDEO_MODERATION REQUEST_ENABLE_VIDEO_MODERATION,
LOCAL_PARTICIPANT_REJECTED,
PARTICIPANT_REJECTED
} from './actionTypes'; } from './actionTypes';
import { isEnabledFromState } from './functions'; import { isEnabledFromState } from './functions';
@ -33,15 +35,57 @@ export const approveParticipant = (id: string) => (dispatch: Function, getState:
const isAudioForceMuted = isForceMuted(participant, MEDIA_TYPE.AUDIO, state); const isAudioForceMuted = isForceMuted(participant, MEDIA_TYPE.AUDIO, state);
const isVideoForceMuted = isForceMuted(participant, MEDIA_TYPE.VIDEO, state); const isVideoForceMuted = isForceMuted(participant, MEDIA_TYPE.VIDEO, state);
const isAudioModerationOn = isEnabledFromState(MEDIA_TYPE.AUDIO, state);
const isVideoModerationOn = isEnabledFromState(MEDIA_TYPE.VIDEO, state);
if (isEnabledFromState(MEDIA_TYPE.AUDIO, state) && isAudioForceMuted) { if (!(isAudioModerationOn || isVideoModerationOn) || (isAudioModerationOn && isAudioForceMuted)) {
conference.avModerationApprove(MEDIA_TYPE.AUDIO, id); conference.avModerationApprove(MEDIA_TYPE.AUDIO, id);
} }
if (isEnabledFromState(MEDIA_TYPE.VIDEO, state) && isVideoForceMuted) { if (isVideoModerationOn && isVideoForceMuted) {
conference.avModerationApprove(MEDIA_TYPE.VIDEO, id); conference.avModerationApprove(MEDIA_TYPE.VIDEO, id);
} }
}; };
/**
* Action used by moderator to reject audio for a participant.
*
* @param {staring} id - The id of the participant to be rejected.
* @returns {void}
*/
export const rejectParticipantAudio = (id: string) => (dispatch: Function, getState: Function) => {
const state = getState();
const { conference } = getConferenceState(state);
const audioModeration = isEnabledFromState(MEDIA_TYPE.AUDIO, state);
const participant = getParticipantById(state, id);
const isAudioForceMuted = isForceMuted(participant, MEDIA_TYPE.AUDIO, state);
const isModerator = isParticipantModerator(participant);
if (audioModeration && !isAudioForceMuted && !isModerator) {
conference.avModerationReject(MEDIA_TYPE.AUDIO, id);
}
};
/**
* Action used by moderator to reject video for a participant.
*
* @param {staring} id - The id of the participant to be rejected.
* @returns {void}
*/
export const rejectParticipantVideo = (id: string) => (dispatch: Function, getState: Function) => {
const state = getState();
const { conference } = getConferenceState(state);
const videoModeration = isEnabledFromState(MEDIA_TYPE.VIDEO, state);
const participant = getParticipantById(state, id);
const isVideoForceMuted = isForceMuted(participant, MEDIA_TYPE.VIDEO, state);
const isModerator = isParticipantModerator(participant);
if (videoModeration && !isVideoForceMuted && !isModerator) {
conference.avModerationReject(MEDIA_TYPE.VIDEO, id);
}
};
/** /**
* Audio or video moderation is disabled. * Audio or video moderation is disabled.
* *
@ -169,6 +213,21 @@ export const localParticipantApproved = (mediaType: MediaType) => {
}; };
}; };
/**
* Local participant was blocked to be able to unmute audio and video.
*
* @param {MediaType} mediaType - The media type to disable.
* @returns {{
* type: LOCAL_PARTICIPANT_REJECTED
* }}
*/
export const localParticipantRejected = (mediaType: MediaType) => {
return {
type: LOCAL_PARTICIPANT_REJECTED,
mediaType
};
};
/** /**
* Shows notification when A/V moderation is enabled and local participant is still not approved. * Shows notification when A/V moderation is enabled and local participant is still not approved.
* *
@ -211,3 +270,21 @@ export function participantApproved(id: string, mediaType: MediaType) {
mediaType mediaType
}; };
} }
/**
* A participant was blocked to unmute for a mediaType.
*
* @param {string} id - The id of the approved participant.
* @param {MediaType} mediaType - The media type which was approved.
* @returns {{
* type: PARTICIPANT_REJECTED,
* }}
*/
export function participantRejected(id: string, mediaType: MediaType) {
return {
type: PARTICIPANT_REJECTED,
id,
mediaType
};
}

View File

@ -35,7 +35,9 @@ import {
enableModeration, enableModeration,
localParticipantApproved, localParticipantApproved,
participantApproved, participantApproved,
participantPendingAudio participantPendingAudio,
localParticipantRejected,
participantRejected
} from './actions'; } from './actions';
import { import {
ASKED_TO_UNMUTE_SOUND_ID, AUDIO_MODERATION_NOTIFICATION_ID, ASKED_TO_UNMUTE_SOUND_ID, AUDIO_MODERATION_NOTIFICATION_ID,
@ -176,6 +178,10 @@ StateListenerRegistry.register(
} }
}); });
conference.on(JitsiConferenceEvents.AV_MODERATION_REJECTED, ({ mediaType }) => {
dispatch(localParticipantRejected(mediaType));
});
conference.on(JitsiConferenceEvents.AV_MODERATION_CHANGED, ({ enabled, mediaType, actor }) => { conference.on(JitsiConferenceEvents.AV_MODERATION_CHANGED, ({ enabled, mediaType, actor }) => {
enabled ? dispatch(enableModeration(mediaType, actor)) : dispatch(disableModeration(mediaType, actor)); enabled ? dispatch(enableModeration(mediaType, actor)) : dispatch(disableModeration(mediaType, actor));
}); });
@ -194,5 +200,14 @@ StateListenerRegistry.register(
dispatch(dismissPendingParticipant(id, mediaType)); dispatch(dismissPendingParticipant(id, mediaType));
}); });
}); });
// this is received by moderators
conference.on(
JitsiConferenceEvents.AV_MODERATION_PARTICIPANT_REJECTED,
({ participant, mediaType }) => {
const { _id: id } = participant;
dispatch(participantRejected(id, mediaType));
});
} }
}); });

View File

@ -13,8 +13,10 @@ import {
DISMISS_PENDING_PARTICIPANT, DISMISS_PENDING_PARTICIPANT,
ENABLE_MODERATION, ENABLE_MODERATION,
LOCAL_PARTICIPANT_APPROVED, LOCAL_PARTICIPANT_APPROVED,
LOCAL_PARTICIPANT_REJECTED,
PARTICIPANT_APPROVED, PARTICIPANT_APPROVED,
PARTICIPANT_PENDING_AUDIO PARTICIPANT_PENDING_AUDIO,
PARTICIPANT_REJECTED
} from './actionTypes'; } from './actionTypes';
import { MEDIA_TYPE_TO_PENDING_STORE_KEY } from './constants'; import { MEDIA_TYPE_TO_PENDING_STORE_KEY } from './constants';
@ -105,6 +107,16 @@ ReducerRegistry.register('features/av-moderation', (state = initialState, action
}; };
} }
case LOCAL_PARTICIPANT_REJECTED: {
const newState = action.mediaType === MEDIA_TYPE.AUDIO
? { audioUnmuteApproved: false } : { videoUnmuteApproved: false };
return {
...state,
...newState
};
}
case PARTICIPANT_PENDING_AUDIO: { case PARTICIPANT_PENDING_AUDIO: {
const { participant } = action; const { participant } = action;
@ -228,6 +240,32 @@ ReducerRegistry.register('features/av-moderation', (state = initialState, action
return state; return state;
} }
case PARTICIPANT_REJECTED: {
const { mediaType, id } = action;
if (mediaType === MEDIA_TYPE.AUDIO) {
return {
...state,
audioWhitelist: {
...state.audioWhitelist,
[id]: false
}
};
}
if (mediaType === MEDIA_TYPE.VIDEO) {
return {
...state,
videoWhitelist: {
...state.videoWhitelist,
[id]: false
}
};
}
return state;
}
} }
return state; return state;

View File

@ -63,15 +63,12 @@ export default function ParticipantQuickAction({
</QuickActionButton> </QuickActionButton>
); );
} }
case QUICK_ACTION_BUTTON.ASK_TO_UNMUTE: { default: {
return ( return (
<AskToUnmuteButton <AskToUnmuteButton
askUnmuteText = { askUnmuteText } askUnmuteText = { askUnmuteText }
participantID = { participantID } /> participantID = { participantID } />
); );
} }
default: {
return null;
}
} }
} }

View File

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

View File

@ -10,7 +10,7 @@ import {
sendAnalytics, sendAnalytics,
VIDEO_MUTE VIDEO_MUTE
} from '../analytics'; } from '../analytics';
import { showModeratedNotification } from '../av-moderation/actions'; import { rejectParticipantAudio, rejectParticipantVideo, showModeratedNotification } from '../av-moderation/actions';
import { shouldShowModeratedNotification } from '../av-moderation/functions'; import { shouldShowModeratedNotification } from '../av-moderation/functions';
import { import {
MEDIA_TYPE, MEDIA_TYPE,
@ -112,6 +112,11 @@ export function muteAllParticipants(exclude: Array<string>, mediaType: MEDIA_TYP
} }
dispatch(muteRemote(id, mediaType)); dispatch(muteRemote(id, mediaType));
if (mediaType === MEDIA_TYPE.AUDIO) {
dispatch(rejectParticipantAudio(id));
} else {
dispatch(rejectParticipantVideo(id));
}
}); });
}; };
} }

View File

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

View File

@ -13,6 +13,17 @@ end
module:log('info', 'Starting av_moderation for %s', muc_component_host); module:log('info', 'Starting av_moderation for %s', muc_component_host);
-- Returns the index of the given element in the table
-- @param table in which to look
-- @param elem the element for which to find the index
function get_index_in_table(table, elem)
for index, value in pairs(table) do
if value == elem then
return index
end
end
end
-- Sends a json-message to the destination jid -- Sends a json-message to the destination jid
-- @param to_jid the destination jid -- @param to_jid the destination jid
-- @param json_message the message content to send -- @param json_message the message content to send
@ -46,20 +57,22 @@ function notify_occupants_enable(jid, enable, room, actorJid, mediaType)
end end
end end
-- Notifies about a jid added to the whitelist. Notifies all moderators and admin and the jid itself -- Notifies about a change to the whitelist. Notifies all moderators and admin and the jid itself
-- @param jid the jid to notify about the change -- @param jid the jid to notify about the change
-- @param moderators whether to notify all moderators in the room -- @param moderators whether to notify all moderators in the room
-- @param room the room where to send it -- @param room the room where to send it
-- @param mediaType used only when a participant is approved (not sent to moderators) -- @param mediaType used only when a participant is approved (not sent to moderators)
function notify_whitelist_change(jid, moderators, room, mediaType) -- @param removed whether the jid is removed or added
function notify_whitelist_change(jid, moderators, room, mediaType, removed)
local body_json = {}; local body_json = {};
body_json.type = 'av_moderation'; body_json.type = 'av_moderation';
body_json.room = internal_room_jid_match_rewrite(room.jid); body_json.room = internal_room_jid_match_rewrite(room.jid);
body_json.whitelists = room.av_moderation; body_json.whitelists = room.av_moderation;
body_json.removed = removed;
body_json.mediaType = mediaType;
local moderators_body_json_str = json.encode(body_json); local moderators_body_json_str = json.encode(body_json);
body_json.whitelists = nil; body_json.whitelists = nil;
body_json.approved = true; -- we want to send to participants only that they were approved to unmute body_json.approved = true; -- we want to send to participants only that they were approved to unmute
body_json.mediaType = mediaType;
local participant_body_json_str = json.encode(body_json); local participant_body_json_str = json.encode(body_json);
for _, occupant in room:each_occupant() do for _, occupant in room:each_occupant() do
@ -77,6 +90,22 @@ function notify_whitelist_change(jid, moderators, room, mediaType)
end end
end end
-- Notifies jid that is approved. This is a moderator to jid message to ask to unmute,
-- @param jid the jid to notify about the change
-- @param from the jid that triggered this
-- @param room the room where to send it
-- @param mediaType the mediaType it was approved for
function notify_jid_approved(jid, from, room, mediaType)
local body_json = {};
body_json.type = 'av_moderation';
body_json.room = internal_room_jid_match_rewrite(room.jid);
body_json.approved = true; -- we want to send to participants only that they were approved to unmute
body_json.mediaType = mediaType;
body_json.from = from;
send_json_message(jid, json.encode(body_json));
end
-- receives messages from clients to the component sending A/V moderation enable/disable commands or adding -- receives messages from clients to the component sending A/V moderation enable/disable commands or adding
-- jids to the whitelist -- jids to the whitelist
function on_message(event) function on_message(event)
@ -166,7 +195,7 @@ function on_message(event)
-- send message to all occupants -- send message to all occupants
notify_occupants_enable(nil, enabled, room, occupant.nick, mediaType); notify_occupants_enable(nil, enabled, room, occupant.nick, mediaType);
return true; return true;
elseif moderation_command.attr.jidToWhitelist and room.av_moderation then elseif moderation_command.attr.jidToWhitelist then
local occupant_jid = moderation_command.attr.jidToWhitelist; local occupant_jid = moderation_command.attr.jidToWhitelist;
-- check if jid is in the room, if so add it to whitelist -- check if jid is in the room, if so add it to whitelist
-- inform all moderators and admins and the jid -- inform all moderators and admins and the jid
@ -176,16 +205,44 @@ function on_message(event)
return false; return false;
end end
local whitelist = room.av_moderation[mediaType]; if room.av_moderation then
if not whitelist then local whitelist = room.av_moderation[mediaType];
whitelist = {}; if not whitelist then
room.av_moderation[mediaType] = whitelist; whitelist = {};
room.av_moderation[mediaType] = whitelist;
end
table.insert(whitelist, occupant_jid);
notify_whitelist_change(occupant_to_add.jid, true, room, mediaType, false);
return true;
else
-- this is a moderator asking the jid to unmute without enabling av moderation
-- let's just send the event
notify_jid_approved(occupant_to_add.jid, occupant.nick, room, mediaType);
end
elseif moderation_command.attr.jidToBlacklist then
local occupant_jid = moderation_command.attr.jidToBlacklist;
-- check if jid is in the room, if so remove it from the whitelist
-- inform all moderators and admins
local occupant_to_remove = room:get_occupant_by_nick(room_jid_match_rewrite(occupant_jid));
if not occupant_to_remove then
module:log('warn', 'No occupant %s found for %s', occupant_jid, room.jid);
return false;
end end
table.insert(whitelist, occupant_jid);
notify_whitelist_change(occupant_to_add.jid, true, room, mediaType); if room.av_moderation then
local whitelist = room.av_moderation[mediaType];
if whitelist then
local index = get_index_in_table(whitelist, occupant_jid)
if(index) then
table.remove(whitelist, index);
notify_whitelist_change(occupant_to_remove.jid, true, room, mediaType, true);
end
end
return true; return true;
end
end end
end end