From 3abd7479118d559ef451ee8179f49e33ea9e4d93 Mon Sep 17 00:00:00 2001 From: damencho Date: Fri, 30 Aug 2024 09:10:01 -0500 Subject: [PATCH] fix(transcriptions,recording): Allows non moderators with features to dial, record or transcribe. --- react/features/base/conference/reducer.ts | 1 + react/features/base/jwt/constants.ts | 2 +- react/features/invite/functions.ts | 5 +-- .../LiveStream/AbstractLiveStreamButton.ts | 2 +- .../AbstractStartRecordingDialogContent.tsx | 10 ++++- .../web/StartRecordingDialogContent.tsx | 6 ++- react/features/recording/functions.ts | 11 ++--- react/features/transcribing/functions.ts | 4 +- .../prosody-plugins/mod_filter_iq_jibri.lua | 33 ++++++++++++--- .../prosody-plugins/mod_filter_iq_rayo.lua | 41 +++++++++++++++++-- .../mod_muc_transcription_filter.lua | 31 -------------- .../mod_room_metadata_component.lua | 26 +++++++++--- .../mod_system_chat_message.lua | 1 - resources/prosody-plugins/util.lib.lua | 4 +- 14 files changed, 111 insertions(+), 66 deletions(-) delete mode 100644 resources/prosody-plugins/mod_muc_transcription_filter.lua diff --git a/react/features/base/conference/reducer.ts b/react/features/base/conference/reducer.ts index e0ece7bd23722..3af9c06f470c8 100644 --- a/react/features/base/conference/reducer.ts +++ b/react/features/base/conference/reducer.ts @@ -93,6 +93,7 @@ export interface IJitsiConference { getRole: Function; getSpeakerStats: () => ISpeakerStats; getSsrcByTrack: Function; + getTranscriptionStatus: Function; grantOwner: Function; isAVModerationSupported: Function; isE2EEEnabled: Function; diff --git a/react/features/base/jwt/constants.ts b/react/features/base/jwt/constants.ts index 12b4123997d53..ff075cb7e6aa6 100644 --- a/react/features/base/jwt/constants.ts +++ b/react/features/base/jwt/constants.ts @@ -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 and local recording. */ export const FEATURES_TO_BUTTONS_MAPPING = { 'livestreaming': 'livestreaming', - 'recording': 'recording', 'transcription': 'closedcaptions' }; diff --git a/react/features/invite/functions.ts b/react/features/invite/functions.ts index 872b6bdba8767..9c30b1cf863c6 100644 --- a/react/features/invite/functions.ts +++ b/react/features/invite/functions.ts @@ -492,7 +492,7 @@ export function isAddPeopleEnabled(state: IReduxState): boolean { export function isDialOutEnabled(state: IReduxState): boolean { const { conference } = state['features/base/conference']; - return isLocalParticipantModerator(state) + return (isLocalParticipantModerator(state) || isJwtFeatureEnabled(state, 'outbound-call', false, false)) && conference && conference.isSIPCallingSupported(); } @@ -505,8 +505,7 @@ export function isDialOutEnabled(state: IReduxState): boolean { export function isSipInviteEnabled(state: IReduxState): boolean { const { sipInviteUrl } = state['features/base/config']; - return isLocalParticipantModerator(state) - && isJwtFeatureEnabled(state, 'sip-outbound-call') + return (isLocalParticipantModerator(state) || isJwtFeatureEnabled(state, 'sip-outbound-call', false, false)) && Boolean(sipInviteUrl); } diff --git a/react/features/recording/components/LiveStream/AbstractLiveStreamButton.ts b/react/features/recording/components/LiveStream/AbstractLiveStreamButton.ts index ffb162d70d658..aed02e4199330 100644 --- a/react/features/recording/components/LiveStream/AbstractLiveStreamButton.ts +++ b/react/features/recording/components/LiveStream/AbstractLiveStreamButton.ts @@ -135,7 +135,7 @@ export function _mapStateToProps(state: IReduxState, ownProps: IProps) { visible = isLiveStreamingButtonVisible({ localParticipantIsModerator: isModerator, liveStreamingEnabled: liveStreaming?.enabled, - liveStreamingEnabledInJwt: isJwtFeatureEnabled(state, 'livestreaming', true), + liveStreamingEnabledInJwt: isJwtFeatureEnabled(state, 'livestreaming', false, false), isInBreakoutRoom: isInBreakoutRoom(state) }); } diff --git a/react/features/recording/components/Recording/AbstractStartRecordingDialogContent.tsx b/react/features/recording/components/Recording/AbstractStartRecordingDialogContent.tsx index 6251e6d02fd50..4863aca189ee2 100644 --- a/react/features/recording/components/Recording/AbstractStartRecordingDialogContent.tsx +++ b/react/features/recording/components/Recording/AbstractStartRecordingDialogContent.tsx @@ -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'; @@ -29,6 +30,11 @@ export interface IProps extends WithTranslation { */ _dialogStyles: any; + /** + * Whether recording feature is enabled. + */ + _hasRecordingFeature: boolean; + /** * Whether to hide the storage warning or not. */ @@ -412,13 +418,13 @@ class AbstractStartRecordingDialogContent extends Component { */ export function mapStateToProps(state: IReduxState) { const { localRecording, recordingService } = state['features/base/config']; - const _localRecordingAvailable - = !localRecording?.disable && supportsLocalRecording(); + const _localRecordingAvailable = !localRecording?.disable && supportsLocalRecording(); return { ..._abstractMapStateToProps(state), isVpaas: isVpaasMeeting(state), _canStartTranscribing: canAddTranscriber(state), + _hasRecordingFeature: isJwtFeatureEnabled(state, 'recording', false, false), _hideStorageWarning: Boolean(recordingService?.hideStorageWarning), _isModerator: isLocalParticipantModerator(state), _localRecordingAvailable, diff --git a/react/features/recording/components/Recording/web/StartRecordingDialogContent.tsx b/react/features/recording/components/Recording/web/StartRecordingDialogContent.tsx index 5534803f7c26a..6b7f14a57835b 100644 --- a/react/features/recording/components/Recording/web/StartRecordingDialogContent.tsx +++ b/react/features/recording/components/Recording/web/StartRecordingDialogContent.tsx @@ -37,9 +37,11 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent { * @returns {React$Component} */ render() { + const _renderRecording = this.props._isModerator || this.props._hasRecordingFeature; + return ( - { this.props._isModerator && ( + { _renderRecording && ( <> { this._renderNoIntegrationsContent() } { this._renderFileSharingContent() } @@ -48,7 +50,7 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent { )} { this._renderLocalRecordingContent() } - { this._renderAdvancedOptions() } + { _renderRecording && <> { this._renderAdvancedOptions() } } ); } diff --git a/react/features/recording/functions.ts b/react/features/recording/functions.ts index b7adffa3ea79f..d9d458e1af7b8 100644 --- a/react/features/recording/functions.ts +++ b/react/features/recording/functions.ts @@ -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 (isModerator || isJwtFeatureEnabled(state, 'recording', false, false)) { + visible = recordingEnabled; } // disable the button if the livestreaming is running. @@ -420,7 +420,8 @@ export function registerRecordingAudioFiles(dispatch: IStore['dispatch'], should * * @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} liveStreamingEnabledInJwt - True if the lives streaming feature is enabled in JWT. + * @param {boolean} isInBreakoutRoom - True if currently in breakout room. * @returns {boolean} */ export function isLiveStreamingButtonVisible({ @@ -436,8 +437,8 @@ export function isLiveStreamingButtonVisible({ }) { let visible = false; - if (localParticipantIsModerator && !isInBreakoutRoom) { - visible = liveStreamingEnabled ? liveStreamingEnabledInJwt : false; + if (!isInBreakoutRoom) { + visible = liveStreamingEnabled ? localParticipantIsModerator || liveStreamingEnabledInJwt : false; } return visible; diff --git a/react/features/transcribing/functions.ts b/react/features/transcribing/functions.ts index 430dfa9a64794..58d2c4d5fe7c1 100644 --- a/react/features/transcribing/functions.ts +++ b/react/features/transcribing/functions.ts @@ -77,13 +77,13 @@ export function isRecorderTranscriptionsRunning(state: IReduxState) { */ export function canAddTranscriber(state: IReduxState) { const { transcription } = state['features/base/config']; - const isJwtTranscribingEnabled = isJwtFeatureEnabled(state, 'transcription', isLocalParticipantModerator(state)); + const isJwtTranscribingEnabled = isJwtFeatureEnabled(state, 'transcription', false, false); if (!transcription?.enabled) { return false; } - if (isJwtTranscribingEnabled) { + if (isLocalParticipantModerator(state) || isJwtTranscribingEnabled) { return true; } diff --git a/resources/prosody-plugins/mod_filter_iq_jibri.lua b/resources/prosody-plugins/mod_filter_iq_jibri.lua index b56cf54a76a79..99fba5f6c60a8 100644 --- a/resources/prosody-plugins/mod_filter_iq_jibri.lua +++ b/resources/prosody-plugins/mod_filter_iq_jibri.lua @@ -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,12 +15,30 @@ module:hook("pre-iq/full", function(event) if jibri then local session = event.origin; local token = session.auth_token; + local feature = jibri.attr.recording_mode == 'file' and 'recording' or 'livestreaming'; + local is_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_allowed = false; + if session.granted_jitsi_meet_context_features then + is_granting_allowed = is_feature_allowed(session.granted_jitsi_meet_context_features, feature); + end 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 + if token == nil or not (is_allowed or is_granting_allowed) + then + if not session.jitsi_meet_context_features and not session.granted_jitsi_meet_context_features then + -- we need to check for moderator rights + -- when there are no features and the occupant is moderator we allow recording + 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); + + if occupant.role == 'moderator' then + return; + end + end + module:log("info", "Filtering jibri start recording, stanza:%s", tostring(stanza)); session.send(st.error_reply(stanza, "auth", "forbidden")); diff --git a/resources/prosody-plugins/mod_filter_iq_rayo.lua b/resources/prosody-plugins/mod_filter_iq_rayo.lua index 86bd0cc841fd4..9bcdad6ba7998 100644 --- a/resources/prosody-plugins/mod_filter_iq_rayo.lua +++ b/resources/prosody-plugins/mod_filter_iq_rayo.lua @@ -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"; @@ -75,13 +76,21 @@ module:hook("pre-iq/full", function(event) -- 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 + 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); + + if not session.jitsi_meet_context_features and not session.granted_jitsi_meet_context_features then + -- if there is no features in the token we need to check whether the participants is moderator + local room = main_muc_service.get_room_from_jid(room_real_jid); + is_session_allowed = room:get_affiliation(stanza.attr.from) == 'owner'; + end + if (token == nil or roomName == nil - or not token_util:verify_room(session, room_jid_match_rewrite(roomName)) + or not token_util:verify_room(session, room_real_jid) or not (is_session_allowed or is_granting_session_allowed)) then module:log("warn", "Filtering stanza dial, stanza:%s", tostring(stanza)); @@ -99,8 +108,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 +268,27 @@ 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 occupant.role == 'moderator' then + return data; + end + + if key == 'recording' and data and data.isTranscribingEnabled ~= nil + and occupant.role ~= 'moderator' + and is_feature_allowed(session.jitsi_meet_context_features, 'transcription') + and is_feature_allowed(session.jitsi_meet_context_features, 'recording') then + + local res = {}; + res.isTranscribingEnabled = data.isTranscribingEnabled; + return res; + end + + return nil; +end); + diff --git a/resources/prosody-plugins/mod_muc_transcription_filter.lua b/resources/prosody-plugins/mod_muc_transcription_filter.lua deleted file mode 100644 index b66e44a1d93b9..0000000000000 --- a/resources/prosody-plugins/mod_muc_transcription_filter.lua +++ /dev/null @@ -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!"); diff --git a/resources/prosody-plugins/mod_room_metadata_component.lua b/resources/prosody-plugins/mod_room_metadata_component.lua index 9459172b31807..2e29222f7b404 100644 --- a/resources/prosody-plugins/mod_room_metadata_component.lua +++ b/resources/prosody-plugins/mod_room_metadata_component.lua @@ -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); diff --git a/resources/prosody-plugins/mod_system_chat_message.lua b/resources/prosody-plugins/mod_system_chat_message.lua index 79c71e1f201b0..ddb37238bc061 100644 --- a/resources/prosody-plugins/mod_system_chat_message.lua +++ b/resources/prosody-plugins/mod_system_chat_message.lua @@ -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 diff --git a/resources/prosody-plugins/util.lib.lua b/resources/prosody-plugins/util.lib.lua index d51f5ae71c3a3..60769e8f3e5da 100644 --- a/resources/prosody-plugins/util.lib.lua +++ b/resources/prosody-plugins/util.lib.lua @@ -250,10 +250,8 @@ 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 + if features ~= nil and (features[ft] == "true" or features[ft] == true) then return true; else return false;