fix(transcriptions,recording): Allows non moderators with features to dial, record or transcribe. (#15074)

* fix(transcriptions): Uses dial command to invite transcriber.

* fix(transcriptions,recording): Allows non moderators with features to dial, record or transcribe.

* sqaush: Make sure filtering works when only is a moderator.

It works now and without a token and no features, but being moderator.

* squash: Rename constant.

* squash: Checks features first before defaulting to moderator when filtering metadata service.

* squash: Checks features first before defaulting to moderator in UI.

* squash: Fixes lint and one other check.

* squash: Moves more logic to is_feature_allowed.

* squash: Drops unnecessary check.

* squash: Uses constant coming from ljm.

* squash: Toggles back captions button on error.

* squash: Fix comment.

* squash: Reverting back isLiveStreamingButtonVisible.

* squash: Fix imports.
pull/15123/head jitsi-meet_9731
Дамян Минков 8 months ago committed by GitHub
parent 262cb0422c
commit b3742a3438
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      react/features/base/conference/reducer.ts
  2. 2
      react/features/base/jwt/constants.ts
  3. 7
      react/features/invite/functions.ts
  4. 3
      react/features/recording/components/LiveStream/AbstractLiveStreamButton.ts
  5. 17
      react/features/recording/components/Recording/AbstractStartRecordingDialogContent.tsx
  6. 6
      react/features/recording/components/Recording/web/StartRecordingDialogContent.tsx
  7. 27
      react/features/recording/functions.ts
  8. 7
      react/features/recording/hooks.web.ts
  9. 27
      react/features/subtitles/middleware.ts
  10. 13
      react/features/transcribing/functions.ts
  11. 29
      resources/prosody-plugins/mod_filter_iq_jibri.lua
  12. 61
      resources/prosody-plugins/mod_filter_iq_rayo.lua
  13. 31
      resources/prosody-plugins/mod_muc_transcription_filter.lua
  14. 26
      resources/prosody-plugins/mod_room_metadata_component.lua
  15. 1
      resources/prosody-plugins/mod_system_chat_message.lua
  16. 14
      resources/prosody-plugins/util.lib.lua

@ -93,6 +93,7 @@ export interface IJitsiConference {
getRole: Function;
getSpeakerStats: () => ISpeakerStats;
getSsrcByTrack: Function;
getTranscriptionStatus: Function;
grantOwner: Function;
isAVModerationSupported: Function;
isE2EEEnabled: Function;

@ -20,10 +20,10 @@ export const MEET_FEATURES = {
/**
* A mapping between jwt features and toolbar buttons keys.
* We don't need recording in here, as it will disable the local recording too.
*/
export const FEATURES_TO_BUTTONS_MAPPING = {
'livestreaming': 'livestreaming',
'recording': 'recording',
'transcription': 'closedcaptions'
};

@ -491,8 +491,9 @@ export function isAddPeopleEnabled(state: IReduxState): boolean {
*/
export function isDialOutEnabled(state: IReduxState): boolean {
const { conference } = state['features/base/conference'];
const isModerator = isLocalParticipantModerator(state);
return isLocalParticipantModerator(state)
return isJwtFeatureEnabled(state, 'outbound-call', isModerator, isModerator)
&& conference && conference.isSIPCallingSupported();
}
@ -504,9 +505,9 @@ export function isDialOutEnabled(state: IReduxState): boolean {
*/
export function isSipInviteEnabled(state: IReduxState): boolean {
const { sipInviteUrl } = state['features/base/config'];
const isModerator = isLocalParticipantModerator(state);
return isLocalParticipantModerator(state)
&& isJwtFeatureEnabled(state, 'sip-outbound-call')
return isJwtFeatureEnabled(state, 'sip-outbound-call', isModerator, isModerator)
&& Boolean(sipInviteUrl);
}

@ -133,9 +133,8 @@ export function _mapStateToProps(state: IReduxState, ownProps: IProps) {
const liveStreaming = getLiveStreaming(state);
visible = isLiveStreamingButtonVisible({
localParticipantIsModerator: isModerator,
liveStreamingAllowed: isJwtFeatureEnabled(state, 'livestreaming', isModerator, isModerator),
liveStreamingEnabled: liveStreaming?.enabled,
liveStreamingEnabledInJwt: isJwtFeatureEnabled(state, 'livestreaming', true),
isInBreakoutRoom: isInBreakoutRoom(state)
});
}

@ -6,6 +6,7 @@ import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState, IStore } from '../../../app/types';
import ColorSchemeRegistry from '../../../base/color-scheme/ColorSchemeRegistry';
import { _abstractMapStateToProps } from '../../../base/dialog/functions';
import { isJwtFeatureEnabled } from '../../../base/jwt/functions';
import { isLocalParticipantModerator } from '../../../base/participants/functions';
import { authorizeDropbox, updateDropboxToken } from '../../../dropbox/actions';
import { isVpaasMeeting } from '../../../jaas/functions';
@ -34,11 +35,6 @@ export interface IProps extends WithTranslation {
*/
_hideStorageWarning: boolean;
/**
* Whether local participant is moderator.
*/
_isModerator: boolean;
/**
* Whether local recording is available or not.
*/
@ -59,6 +55,11 @@ export interface IProps extends WithTranslation {
*/
_localRecordingSelfEnabled: boolean;
/**
* Whether to render recording.
*/
_renderRecording: boolean;
/**
* The color-schemed stylesheet of this component.
*/
@ -412,15 +413,15 @@ class AbstractStartRecordingDialogContent extends Component<IProps, IState> {
*/
export function mapStateToProps(state: IReduxState) {
const { localRecording, recordingService } = state['features/base/config'];
const _localRecordingAvailable
= !localRecording?.disable && supportsLocalRecording();
const _localRecordingAvailable = !localRecording?.disable && supportsLocalRecording();
const isModerator = isLocalParticipantModerator(state);
return {
..._abstractMapStateToProps(state),
isVpaas: isVpaasMeeting(state),
_canStartTranscribing: canAddTranscriber(state),
_hideStorageWarning: Boolean(recordingService?.hideStorageWarning),
_isModerator: isLocalParticipantModerator(state),
_renderRecording: isJwtFeatureEnabled(state, 'recording', isModerator, isModerator),
_localRecordingAvailable,
_localRecordingEnabled: !localRecording?.disable,
_localRecordingSelfEnabled: !localRecording?.disableSelfRecording,

@ -37,9 +37,11 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent {
* @returns {React$Component}
*/
render() {
const _renderRecording = this.props._renderRecording;
return (
<Container className = 'recording-dialog'>
{ this.props._isModerator && (
{ _renderRecording && (
<>
{ this._renderNoIntegrationsContent() }
{ this._renderFileSharingContent() }
@ -48,7 +50,7 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent {
</>
)}
{ this._renderLocalRecordingContent() }
{ this._renderAdvancedOptions() }
{ _renderRecording && <> { this._renderAdvancedOptions() } </> }
</Container>
);
}

@ -267,8 +267,8 @@ export function getRecordButtonProps(state: IReduxState) {
if (localRecordingEnabled) {
visible = true;
} else if (isModerator) {
visible = recordingEnabled ? isJwtFeatureEnabled(state, 'recording', true) : false;
} else if (isJwtFeatureEnabled(state, 'recording', isModerator, isModerator)) {
visible = recordingEnabled;
}
// disable the button if the livestreaming is running.
@ -416,29 +416,22 @@ export function registerRecordingAudioFiles(dispatch: IStore['dispatch'], should
}
/**
* Returns true if the live streaming button should be visible.
* Returns true if the live-streaming button should be visible.
*
* @param {boolean} localParticipantIsModerator - True if the local participant is moderator.
* @param {boolean} liveStreamingEnabled - True if the live streaming is enabled.
* @param {boolean} liveStreamingEnabledInJwt - True if the lives treaming feature is enabled in JWT.
* @param {boolean} liveStreamingEnabled - True if the live-streaming is enabled.
* @param {boolean} liveStreamingAllowed - True if the live-streaming feature is enabled in JWT
* or is a moderator if JWT is missing or features are missing in JWT.
* @param {boolean} isInBreakoutRoom - True if in breakout room.
* @returns {boolean}
*/
export function isLiveStreamingButtonVisible({
localParticipantIsModerator,
liveStreamingAllowed,
liveStreamingEnabled,
liveStreamingEnabledInJwt,
isInBreakoutRoom
}: {
isInBreakoutRoom: boolean;
liveStreamingAllowed: boolean;
liveStreamingEnabled: boolean;
liveStreamingEnabledInJwt: boolean;
localParticipantIsModerator: boolean;
}) {
let visible = false;
if (localParticipantIsModerator && !isInBreakoutRoom) {
visible = liveStreamingEnabled ? liveStreamingEnabledInJwt : false;
}
return visible;
return !isInBreakoutRoom && liveStreamingEnabled && liveStreamingAllowed;
}

@ -47,15 +47,14 @@ export function useLiveStreamingButton() {
const toolbarButtons = useSelector((state: IReduxState) => state['features/toolbox'].toolbarButtons);
const localParticipantIsModerator = useSelector(isLocalParticipantModerator);
const liveStreaming = useSelector(getLiveStreaming);
const liveStreamingEnabledInJwt
= useSelector((state: IReduxState) => isJwtFeatureEnabled(state, 'livestreaming', true));
const liveStreamingAllowed = useSelector((state: IReduxState) =>
isJwtFeatureEnabled(state, 'livestreaming', localParticipantIsModerator, localParticipantIsModerator));
const _isInBreakoutRoom = useSelector(isInBreakoutRoom);
if (toolbarButtons?.includes('recording')
&& isLiveStreamingButtonVisible({
localParticipantIsModerator,
liveStreamingAllowed,
liveStreamingEnabled: liveStreaming?.enabled,
liveStreamingEnabledInJwt,
isInBreakoutRoom: _isInBreakoutRoom
})) {
return livestreaming;

@ -2,6 +2,9 @@ import { AnyAction } from 'redux';
import { IStore } from '../app/types';
import { ENDPOINT_MESSAGE_RECEIVED } from '../base/conference/actionTypes';
import { isJwtFeatureEnabled } from '../base/jwt/functions';
import JitsiMeetJS from '../base/lib-jitsi-meet';
import { isLocalParticipantModerator } from '../base/participants/functions';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import {
@ -10,9 +13,11 @@ import {
} from './actionTypes';
import {
removeTranscriptMessage,
setRequestingSubtitles,
updateTranscriptMessage
} from './actions.any';
import { notifyTranscriptionChunkReceived } from './functions';
import logger from './logger';
import { ITranscriptMessage } from './types';
@ -40,6 +45,11 @@ const P_NAME_REQUESTING_TRANSCRIPTION = 'requestingTranscription';
*/
const P_NAME_TRANSLATION_LANGUAGE = 'translation_language';
/**
* The dial command to use for starting a transcriber.
*/
const TRANSCRIBER_DIAL_NUMBER = 'jitsi_meet_transcribe';
/**
* Time after which the rendered subtitles will be removed.
*/
@ -229,7 +239,7 @@ function _endpointMessageReceived(store: IStore, next: Function, action: AnyActi
* @returns {void}
*/
function _requestingSubtitlesChange(
{ getState }: IStore,
{ dispatch, getState }: IStore,
enabled: boolean,
language?: string | null) {
const state = getState();
@ -239,6 +249,21 @@ function _requestingSubtitlesChange(
P_NAME_REQUESTING_TRANSCRIPTION,
enabled);
if (enabled && conference?.getTranscriptionStatus() === JitsiMeetJS.constants.transcriptionStatus.OFF) {
const isModerator = isLocalParticipantModerator(state);
const featureAllowed = isJwtFeatureEnabled(getState(), 'transcription', isModerator, isModerator);
if (featureAllowed) {
conference?.dial(TRANSCRIBER_DIAL_NUMBER)
.catch((e: any) => {
logger.error('Error dialing', e);
// let's back to the correct state
dispatch(setRequestingSubtitles(false, false));
});
}
}
if (enabled && language) {
conference?.setLocalParticipantProperty(
P_NAME_TRANSLATION_LANGUAGE,

@ -77,15 +77,8 @@ export function isRecorderTranscriptionsRunning(state: IReduxState) {
*/
export function canAddTranscriber(state: IReduxState) {
const { transcription } = state['features/base/config'];
const isJwtTranscribingEnabled = isJwtFeatureEnabled(state, 'transcription', isLocalParticipantModerator(state));
const isModerator = isLocalParticipantModerator(state);
const isTranscribingAllowed = isJwtFeatureEnabled(state, 'transcription', isModerator, isModerator);
if (!transcription?.enabled) {
return false;
}
if (isJwtTranscribingEnabled) {
return true;
}
return false;
return Boolean(transcription?.enabled) && isTranscribingAllowed;
}

@ -1,5 +1,10 @@
-- This module is enabled under the main virtual host
local st = require "util.stanza";
local is_feature_allowed = module:require "util".is_feature_allowed;
local jid_bare = require "util.jid".bare;
local util = module:require 'util';
local is_feature_allowed = util.is_feature_allowed;
local get_room_from_jid = util.get_room_from_jid;
local room_jid_match_rewrite = util.room_jid_match_rewrite;
-- filters jibri iq in case of requested from jwt authenticated session that
-- has features in the user context, but without feature for recording
@ -10,17 +15,19 @@ module:hook("pre-iq/full", function(event)
if jibri then
local session = event.origin;
local token = session.auth_token;
local room = get_room_from_jid(room_jid_match_rewrite(jid_bare(stanza.attr.to)));
local occupant = room:get_occupant_by_real_jid(stanza.attr.from);
local feature = jibri.attr.recording_mode == 'file' and 'recording' or 'livestreaming';
local is_allowed = is_feature_allowed(
feature,
session.jitsi_meet_context_features,
session.granted_jitsi_meet_context_features,
occupant.role == 'moderator');
if jibri.attr.action == 'start' then
if token == nil
or not is_feature_allowed(session.jitsi_meet_context_features,
(jibri.attr.recording_mode == 'file' and 'recording' or 'livestreaming')
) then
module:log("info",
"Filtering jibri start recording, stanza:%s", tostring(stanza));
session.send(st.error_reply(stanza, "auth", "forbidden"));
return true;
end
if jibri.attr.action == 'start' and not is_allowed then
module:log('info', 'Filtering jibri start recording, stanza:%s', tostring(stanza));
session.send(st.error_reply(stanza, 'auth', 'forbidden'));
return true;
end
end
end

@ -1,3 +1,4 @@
-- This module is enabled under the main virtual host
local new_throttle = require "util.throttle".create;
local st = require "util.stanza";
@ -69,20 +70,19 @@ module:hook("pre-iq/full", function(event)
end
end
local feature = dial.attr.to == 'jitsi_meet_transcribe' and 'transcription' or 'outbound-call';
local is_session_allowed = is_feature_allowed(session.jitsi_meet_context_features, feature);
-- if current user is not allowed, but was granted moderation by a user
-- that is allowed by its features we want to allow it
local is_granting_session_allowed = false;
if (session.granted_jitsi_meet_context_features) then
is_granting_session_allowed = is_feature_allowed(session.granted_jitsi_meet_context_features, feature);
end
local room_real_jid = room_jid_match_rewrite(roomName);
local room = main_muc_service.get_room_from_jid(room_real_jid);
if (token == nil
or roomName == nil
or not token_util:verify_room(session, room_jid_match_rewrite(roomName))
or not (is_session_allowed or is_granting_session_allowed))
local feature = dial.attr.to == 'jitsi_meet_transcribe' and 'transcription' or 'outbound-call';
local is_session_allowed = is_feature_allowed(
feature,
session.jitsi_meet_context_features,
session.granted_jitsi_meet_context_features,
room:get_affiliation(stanza.attr.from) == 'owner');
if roomName == nil
or (token ~= nil and not token_util:verify_room(session, room_real_jid))
or not is_session_allowed
then
module:log("warn", "Filtering stanza dial, stanza:%s", tostring(stanza));
session.send(st.error_reply(stanza, "auth", "forbidden"));
@ -99,8 +99,8 @@ module:hook("pre-iq/full", function(event)
group_id = session.granted_jitsi_meet_context_group_id;
end
-- now lets check any limits if configured
if limit_outgoing_calls > 0 then
-- now lets check any limits for outgoing calls if configured
if feature == 'outbound-call' and limit_outgoing_calls > 0 then
if not session.dial_out_throttle then
-- module:log("debug", "Enabling dial-out throttle session=%s.", session);
session.dial_out_throttle = new_throttle(limit_outgoing_calls, OUTGOING_CALLS_THROTTLE_INTERVAL);
@ -259,3 +259,34 @@ process_host_module(main_muc_component_host, function(host_module, host)
end);
end
end);
-- when recording participants may enable and backend transcriptions
-- it is possible that participant is not moderator, but has the features enabled for
-- transcribing, we need to allow that operation
module:hook('jitsi-metadata-allow-moderation', function (event)
local data, key, occupant, session = event.data, event.key, event.actor, event.session;
if key == 'recording' and data and data.isTranscribingEnabled ~= nil then
-- if it is recording we want to allow setting in metadata if not moderator but features
-- are present
if session.jitsi_meet_context_features
and occupant.role ~= 'moderator'
and is_feature_allowed('transcription', session.jitsi_meet_context_features)
and is_feature_allowed('recording', session.jitsi_meet_context_features) then
local res = {};
res.isTranscribingEnabled = data.isTranscribingEnabled;
return res;
elseif not session.jitsi_meet_context_features and occupant.role == 'moderator' then
return data;
else
return nil;
end
end
if occupant.role == 'moderator' then
return data;
end
return nil;
end);

@ -1,31 +0,0 @@
--This module performs features checking when a transcription is requested.
--If the transcription feature is not allowed, the tag indicating that a
--transcription is being requested will be stripped from the presence stanza.
--The module must be enabled under the muc component.
local is_feature_allowed = module:require "util".is_feature_allowed;
module:log("info", "Loading mod_muc_transcription_filter!");
local filtered_tag_name = "jitsi_participant_requestingTranscription";
function filter_transcription_tag(event)
local stanza = event.stanza;
local session = event.origin;
if stanza and stanza.name == "presence" then
if not is_feature_allowed(session.jitsi_meet_context_features,'transcription') then
stanza:maptags(function(tag)
if tag and tag.name == filtered_tag_name then
module:log("info", "Removing %s tag from presence stanza!", filtered_tag_name);
return nil;
else
return tag;
end
end)
end
end
end
module:hook("presence/bare", filter_transcription_tag);
module:hook("presence/full", filter_transcription_tag);
module:hook("presence/host", filter_transcription_tag);
module:log("info", "Loaded mod_muc_transcription_filter!");

@ -28,7 +28,13 @@ local FORM_KEY = 'muc#roominfo_jitsimetadata';
local muc_component_host = module:get_option_string('muc_component');
if muc_component_host == nil then
module:log("error", "No muc_component specified. No muc to operate on!");
module:log('error', 'No muc_component specified. No muc to operate on!');
return;
end
local muc_domain_base = module:get_option_string('muc_mapper_domain_base');
if not muc_domain_base then
module:log('warn', 'No muc_domain_base option set.');
return;
end
@ -129,11 +135,6 @@ function on_message(event)
return false;
end
if occupant.role ~= 'moderator' then
module:log('warn', 'Occupant %s is not moderator and not allowed this operation for %s', from, room.jid);
return false;
end
local jsonData, error = json.decode(messageText);
if jsonData == nil then -- invalid JSON
module:log("error", "Invalid JSON message: %s error:%s", messageText, error);
@ -145,6 +146,19 @@ function on_message(event)
return false;
end
if occupant.role ~= 'moderator' then
-- will return a non nil filtered data to use, if it is nil, it is not allowed
local res = module:context(muc_domain_base):fire_event('jitsi-metadata-allow-moderation',
{ room = room; actor = occupant; key = jsonData.key ; data = jsonData.data; session = session; });
if not res then
module:log('warn', 'Occupant %s is not moderator and not allowed this operation for %s', from, room.jid);
return false;
end
jsonData.data = res;
end
room.jitsiMetadata[jsonData.key] = jsonData.data;
broadcastMetadata(room);

@ -15,7 +15,6 @@ local get_room_from_jid = util.get_room_from_jid;
local st = require "util.stanza";
local json = require "cjson.safe";
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
local asapKeyServer = module:get_option_string("prosody_password_public_key_repo_url", "");
if asapKeyServer then

@ -250,13 +250,15 @@ end
-- Utility function to check whether feature is present and enabled. Allow
-- a feature if there are features present in the session(coming from
-- the token) and the value of the feature is true.
-- If features is not present in the token we skip feature detection and allow
-- everything.
function is_feature_allowed(features, ft)
if (features == nil or features[ft] == "true" or features[ft] == true) then
return true;
-- If features are missing but we have granted_features check that
-- if features are missing from the token we check whether it is moderator
function is_feature_allowed(ft, features, granted_features, is_moderator)
if features then
return features[ft] == "true" or features[ft] == true;
elseif granted_features then
return granted_features[ft] == "true" or granted_features[ft] == true;
else
return false;
return is_moderator;
end
end

Loading…
Cancel
Save