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.",
"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?",
"muteParticipantsVideoButton": "Stop camera",
"muteParticipantsVideoButton": "Stop video",
"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.",
"noDropboxToken": "No valid Dropbox token",
@ -897,8 +897,8 @@
"mute": "Mute / Unmute",
"muteEveryone": "Mute everyone",
"muteEveryoneElse": "Mute everyone else",
"muteEveryonesVideo": "Disable everyone's camera",
"muteEveryoneElsesVideo": "Disable everyone else's camera",
"muteEveryonesVideo": "Disable everyone's video",
"muteEveryoneElsesVideo": "Disable everyone else's video",
"participants": "Participants",
"pip": "Toggle Picture-in-Picture mode",
"privateMessage": "Send private message",

4
package-lock.json generated
View File

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

View File

@ -59,7 +59,7 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"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",
"lodash": "4.17.21",
"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';
/**
* 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.
*
@ -94,6 +104,17 @@ export const LOCAL_PARTICIPANT_MODERATION_NOTIFICATION = 'LOCAL_PARTICIPANT_MODE
*/
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.

View File

@ -2,7 +2,7 @@
import { getConferenceState } from '../base/conference';
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 {
@ -16,7 +16,9 @@ import {
REQUEST_DISABLE_AUDIO_MODERATION,
REQUEST_ENABLE_AUDIO_MODERATION,
REQUEST_DISABLE_VIDEO_MODERATION,
REQUEST_ENABLE_VIDEO_MODERATION
REQUEST_ENABLE_VIDEO_MODERATION,
LOCAL_PARTICIPANT_REJECTED,
PARTICIPANT_REJECTED
} from './actionTypes';
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 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);
}
if (isEnabledFromState(MEDIA_TYPE.VIDEO, state) && isVideoForceMuted) {
if (isVideoModerationOn && isVideoForceMuted) {
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.
*
@ -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.
*
@ -211,3 +270,21 @@ export function participantApproved(id: string, 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,
localParticipantApproved,
participantApproved,
participantPendingAudio
participantPendingAudio,
localParticipantRejected,
participantRejected
} from './actions';
import {
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 }) => {
enabled ? dispatch(enableModeration(mediaType, actor)) : dispatch(disableModeration(mediaType, actor));
});
@ -194,5 +200,14 @@ StateListenerRegistry.register(
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,
ENABLE_MODERATION,
LOCAL_PARTICIPANT_APPROVED,
LOCAL_PARTICIPANT_REJECTED,
PARTICIPANT_APPROVED,
PARTICIPANT_PENDING_AUDIO
PARTICIPANT_PENDING_AUDIO,
PARTICIPANT_REJECTED
} from './actionTypes';
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: {
const { participant } = action;
@ -228,6 +240,32 @@ ReducerRegistry.register('features/av-moderation', (state = initialState, action
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;

View File

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

View File

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

View File

@ -10,7 +10,7 @@ import {
sendAnalytics,
VIDEO_MUTE
} from '../analytics';
import { showModeratedNotification } from '../av-moderation/actions';
import { rejectParticipantAudio, rejectParticipantVideo, showModeratedNotification } from '../av-moderation/actions';
import { shouldShowModeratedNotification } from '../av-moderation/functions';
import {
MEDIA_TYPE,
@ -112,6 +112,11 @@ export function muteAllParticipants(exclude: Array<string>, mediaType: MEDIA_TYP
}
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 { rejectParticipantVideo } from '../../av-moderation/actions';
import { MEDIA_TYPE } from '../../base/media';
import { muteRemote } from '../actions';
@ -59,6 +60,7 @@ export default class AbstractMuteRemoteParticipantsVideoDialog<P:Props = Props,
const { dispatch, participantID } = this.props;
dispatch(muteRemote(participantID, MEDIA_TYPE.VIDEO));
dispatch(rejectParticipantVideo(participantID));
return true;
}

View File

@ -13,6 +13,17 @@ end
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
-- @param to_jid the destination jid
-- @param json_message the message content to send
@ -46,20 +57,22 @@ function notify_occupants_enable(jid, enable, room, actorJid, mediaType)
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 moderators whether to notify all moderators in the room
-- @param room the room where to send it
-- @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 = {};
body_json.type = 'av_moderation';
body_json.room = internal_room_jid_match_rewrite(room.jid);
body_json.whitelists = room.av_moderation;
body_json.removed = removed;
body_json.mediaType = mediaType;
local moderators_body_json_str = json.encode(body_json);
body_json.whitelists = nil;
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);
for _, occupant in room:each_occupant() do
@ -77,6 +90,22 @@ function notify_whitelist_change(jid, moderators, room, mediaType)
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
-- jids to the whitelist
function on_message(event)
@ -166,7 +195,7 @@ function on_message(event)
-- send message to all occupants
notify_occupants_enable(nil, enabled, room, occupant.nick, mediaType);
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;
-- check if jid is in the room, if so add it to whitelist
-- inform all moderators and admins and the jid
@ -176,16 +205,44 @@ function on_message(event)
return false;
end
local whitelist = room.av_moderation[mediaType];
if not whitelist then
whitelist = {};
room.av_moderation[mediaType] = whitelist;
if room.av_moderation then
local whitelist = room.av_moderation[mediaType];
if not whitelist then
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
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