Compare commits

...

16 Commits

Author SHA1 Message Date
damencho
afcb791087 fix(subtitles): Make sure we show captions button when no features but is transcribing. 2024-09-11 12:08:40 -05:00
damencho
e4d490c745 squash: Handles visitors count that request transcriptions and the languages requested. 2024-09-10 18:13:50 -05:00
damencho
63d07ab96a feat: Rewrites room name requests in rayo iq for visitors. 2024-09-10 18:13:50 -05:00
damencho
3540ae27c5 squash: Fixes filter iq. 2024-09-10 18:13:50 -05:00
damencho
3fb359b2eb feat(visitors): Transcriptions for visitors. 2024-09-10 18:13:50 -05:00
damencho
7e7b17e138 squash: Toggles back captions button on error. 2024-09-10 18:13:27 -05:00
damencho
bccbb5b926 squash: Uses constant coming from ljm. 2024-09-10 16:03:21 -05:00
damencho
ef7ef96d21 squash: Drops unnecessary check. 2024-09-10 16:03:21 -05:00
damencho
853ad68dd6 squash: Moves more logic to is_feature_allowed. 2024-09-10 16:03:21 -05:00
damencho
936e35e938 squash: Fixes lint and one other check. 2024-09-10 16:03:21 -05:00
damencho
08eff23188 squash: Checks features first before defaulting to moderator in UI. 2024-09-10 16:03:21 -05:00
damencho
1756c43959 squash: Checks features first before defaulting to moderator when filtering metadata service. 2024-09-10 16:03:21 -05:00
damencho
3770eabf8c squash: Rename constant. 2024-09-10 16:03:21 -05:00
damencho
1a0164f519 sqaush: Make sure filtering works when only is a moderator.
It works now and without a token and no features, but being moderator.
2024-09-10 16:03:21 -05:00
damencho
1f4ae52887 fix(transcriptions,recording): Allows non moderators with features to dial, record or transcribe. 2024-09-10 16:03:21 -05:00
damencho
e5f6c78346 fix(transcriptions): Uses dial command to invite transcriber. 2024-09-10 16:03:21 -05:00
22 changed files with 351 additions and 190 deletions

View File

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

View File

@@ -18,15 +18,6 @@ export const MEET_FEATURES = {
TRANSCRIPTION: 'transcription'
};
/**
* A mapping between jwt features and toolbar buttons keys.
*/
export const FEATURES_TO_BUTTONS_MAPPING = {
'livestreaming': 'livestreaming',
'recording': 'recording',
'transcription': 'closedcaptions'
};
/**
* The JWT validation errors for JaaS.
*/

View File

@@ -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);
}

View File

@@ -7,7 +7,7 @@ import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/too
import { isInBreakoutRoom } from '../../../breakout-rooms/functions';
import { maybeShowPremiumFeatureDialog } from '../../../jaas/actions';
import { isRecorderTranscriptionsRunning } from '../../../transcribing/functions';
import { isCloudRecordingRunning, isLiveStreamingButtonVisible, isLiveStreamingRunning } from '../../functions';
import { isCloudRecordingRunning, isLiveStreamingRunning } from '../../functions';
import { getLiveStreaming } from './functions';
@@ -132,12 +132,9 @@ export function _mapStateToProps(state: IReduxState, ownProps: IProps) {
const isModerator = isLocalParticipantModerator(state);
const liveStreaming = getLiveStreaming(state);
visible = isLiveStreamingButtonVisible({
localParticipantIsModerator: isModerator,
liveStreamingEnabled: liveStreaming?.enabled,
liveStreamingEnabledInJwt: isJwtFeatureEnabled(state, 'livestreaming', true),
isInBreakoutRoom: isInBreakoutRoom(state)
});
visible = !isInBreakoutRoom(state)
&& liveStreaming?.enabled
&& isJwtFeatureEnabled(state, 'livestreaming', isModerator, isModerator);
}
// disable the button if the recording is running.

View File

@@ -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,

View File

@@ -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>
);
}

View File

@@ -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.
@@ -414,31 +414,3 @@ export function registerRecordingAudioFiles(dispatch: IStore['dispatch'], should
RECORDING_ON_SOUND_ID,
getSoundFileSrc(RECORDING_ON_SOUND_FILE, language)));
}
/**
* 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.
* @returns {boolean}
*/
export function isLiveStreamingButtonVisible({
localParticipantIsModerator,
liveStreamingEnabled,
liveStreamingEnabledInJwt,
isInBreakoutRoom
}: {
isInBreakoutRoom: boolean;
liveStreamingEnabled: boolean;
liveStreamingEnabledInJwt: boolean;
localParticipantIsModerator: boolean;
}) {
let visible = false;
if (localParticipantIsModerator && !isInBreakoutRoom) {
visible = liveStreamingEnabled ? liveStreamingEnabledInJwt : false;
}
return visible;
}

View File

@@ -8,7 +8,7 @@ import { isInBreakoutRoom } from '../breakout-rooms/functions';
import { getLiveStreaming } from './components/LiveStream/functions';
import LiveStreamButton from './components/LiveStream/web/LiveStreamButton';
import RecordButton from './components/Recording/web/RecordButton';
import { getRecordButtonProps, isLiveStreamingButtonVisible } from './functions';
import { getRecordButtonProps } from './functions';
const recording = {
@@ -47,17 +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,
liveStreamingEnabled: liveStreaming?.enabled,
liveStreamingEnabledInJwt,
isInBreakoutRoom: _isInBreakoutRoom
})) {
&& !_isInBreakoutRoom
&& liveStreaming?.enabled
&& liveStreamingAllowed) {
return livestreaming;
}
}

View File

@@ -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 {
@@ -9,10 +12,11 @@ import {
TOGGLE_REQUESTING_SUBTITLES
} from './actionTypes';
import {
removeTranscriptMessage,
removeTranscriptMessage, setRequestingSubtitles,
updateTranscriptMessage
} from './actions.any';
import { notifyTranscriptionChunkReceived } from './functions';
import logger from './logger';
import { ITranscriptMessage } from './types';
@@ -40,6 +44,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 +238,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 +248,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,

View File

@@ -99,7 +99,8 @@ export default function Toolbox({
const isDialogVisible = useSelector((state: IReduxState) => Boolean(state['features/base/dialog'].component));
const jwt = useSelector((state: IReduxState) => state['features/base/jwt'].jwt);
const localParticipant = useSelector(getLocalParticipant);
const jwtDisabledButtons = getJwtDisabledButtons(jwt, localParticipant?.features);
const jwtDisabledButtons = useSelector((state: IReduxState) =>
getJwtDisabledButtons(state, jwt, localParticipant?.features));
const reactionsButtonEnabled = useSelector(isReactionsButtonEnabled);
const _shouldDisplayReactionsButtons = useSelector(shouldDisplayReactionsButtons);
const toolbarVisible = useSelector(isToolboxVisible);

View File

@@ -133,6 +133,7 @@ export const TOOLBAR_BUTTONS: ToolbarButton[] = [
*/
export const VISITORS_MODE_BUTTONS: ToolbarButton[] = [
'chat',
'closedcaptions',
'hangup',
'raisehand',
'settings',

View File

@@ -1,8 +1,8 @@
import { IReduxState } from '../app/types';
import { FEATURES_TO_BUTTONS_MAPPING } from '../base/jwt/constants';
import { isJwtFeatureEnabledStateless } from '../base/jwt/functions';
import { IGUMPendingState } from '../base/media/types';
import { IParticipantFeatures } from '../base/participants/types';
import { isTranscribing } from '../transcribing/functions';
/**
* Indicates if the audio mute button is disabled or not.
@@ -20,21 +20,34 @@ export function isAudioMuteButtonDisabled(state: IReduxState) {
/**
* Returns the buttons corresponding to features disabled through jwt.
*
* @param {IReduxState} state - The state from the Redux store.
* @param {string | undefined} jwt - The jwt token.
* @param {ILocalParticipant} localParticipantFeatures - The features of the local participant.
* @returns {string[]} - The disabled by jwt buttons array.
*/
export function getJwtDisabledButtons(jwt: string | undefined, localParticipantFeatures?: IParticipantFeatures) {
return Object.keys(FEATURES_TO_BUTTONS_MAPPING).reduce((acc: string[], current: string) => {
if (!isJwtFeatureEnabledStateless({
jwt,
localParticipantFeatures,
feature: current,
ifNoToken: true
})) {
acc.push(FEATURES_TO_BUTTONS_MAPPING[current as keyof typeof FEATURES_TO_BUTTONS_MAPPING]);
}
export function getJwtDisabledButtons(
state: IReduxState,
jwt: string | undefined,
localParticipantFeatures?: IParticipantFeatures) {
const acc = [];
return acc;
}, []);
if (!isJwtFeatureEnabledStateless({
jwt,
localParticipantFeatures,
feature: 'livestreaming',
ifNoToken: true
})) {
acc.push('livestreaming');
}
if (!isTranscribing(state) && !isJwtFeatureEnabledStateless({
jwt,
localParticipantFeatures,
feature: 'transcription',
ifNoToken: true
})) {
acc.push('closedcaptions');
}
return acc;
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -1,5 +1,7 @@
-- This module is enabled under the main virtual host
local new_throttle = require "util.throttle".create;
local st = require "util.stanza";
local jid = require "util.jid";
local token_util = module:require "token/util".new(module);
local util = module:require 'util';
@@ -28,6 +30,14 @@ if token_util == nil then
return;
end
-- this is the main virtual host of the main prosody that this vnode serves
local main_domain = module:get_option_string('main_domain');
-- only the visitor prosody has main_domain setting
local is_visitor_prosody = main_domain ~= nil;
-- this is the main virtual host of this vnode
local local_domain = module:get_option_string('muc_mapper_domain_base');
local um_is_admin = require 'core.usermanager'.is_admin;
local function is_admin(jid)
return um_is_admin(jid, module.host);
@@ -44,6 +54,8 @@ load_config();
-- Header names to use to push extra data extracted from token, if any
local OUT_INITIATOR_USER_ATTR_NAME = "X-outbound-call-initiator-user";
local OUT_INITIATOR_GROUP_ATTR_NAME = "X-outbound-call-initiator-group";
local OUT_ROOM_NAME_ATTR_NAME = "JvbRoomName";
local OUTGOING_CALLS_THROTTLE_INTERVAL = 60; -- if max_number_outgoing_calls is enabled it will be
-- the max number of outgoing calls a user can try for a minute
@@ -59,30 +71,45 @@ module:hook("pre-iq/full", function(event)
local token = session.auth_token;
-- find header with attr name 'JvbRoomName' and extract its value
local headerName = 'JvbRoomName';
local roomName;
for _, child in ipairs(dial.tags) do
if (child.name == 'header'
and child.attr.name == headerName) then
roomName = child.attr.value;
break;
-- Remove any 'header' element if it already exists, so it cannot be spoofed by a client
dial:maptags(function(tag)
if tag.name == "header"
and (tag.attr.name == OUT_INITIATOR_USER_ATTR_NAME
or tag.attr.name == OUT_INITIATOR_GROUP_ATTR_NAME) then
return nil
elseif tag.name == "header" and tag.attr.name == OUT_ROOM_NAME_ATTR_NAME then
roomName = tag.attr.value;
-- we will remove it as we will add it later, modified
if is_visitor_prosody then
return nil;
end
end
return tag
end);
local room_jid = jid.bare(stanza.attr.to);
local room_real_jid = room_jid_match_rewrite(room_jid);
local room = main_muc_service.get_room_from_jid(room_real_jid);
local is_sender_in_room = room:get_occupant_jid(stanza.attr.from) ~= nil;
if not room or not is_sender_in_room then
module:log("warn", "Filtering stanza dial, stanza:%s", tostring(stanza));
session.send(st.error_reply(stanza, "auth", "forbidden"));
return true;
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);
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 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
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))
if roomName == nil
or roomName ~= room_jid
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 +126,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);
@@ -119,20 +146,6 @@ module:hook("pre-iq/full", function(event)
-- now lets insert token information if any
if session and user_id then
-- First remove any 'header' element if it already
-- exists, so it cannot be spoofed by a client
stanza:maptags(
function(tag)
if tag.name == "header"
and (tag.attr.name == OUT_INITIATOR_USER_ATTR_NAME
or tag.attr.name == OUT_INITIATOR_GROUP_ATTR_NAME) then
return nil
end
return tag
end
)
local dial = stanza:get_child('dial', 'urn:xmpp:rayo:1');
-- adds initiator user id from token
dial:tag("header", {
xmlns = "urn:xmpp:rayo:1",
@@ -149,9 +162,18 @@ module:hook("pre-iq/full", function(event)
dial:up();
end
end
-- we want to instruct jigasi to enter the main room, so send the correct main room jid
if is_visitor_prosody then
dial:tag("header", {
xmlns = "urn:xmpp:rayo:1",
name = OUT_ROOM_NAME_ATTR_NAME,
value = string.gsub(roomName, local_domain, main_domain) });
dial:up();
end
end
end
end);
end, 1); -- make sure we run before domain mapper
--- Finds and returns the number of concurrent outgoing calls for a user
-- @param context_user the user id extracted from the token
@@ -259,3 +281,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);

View File

@@ -14,6 +14,7 @@ local jid = require 'util.jid';
local st = require 'util.stanza';
local new_id = require 'util.id'.medium;
local filters = require 'util.filters';
local array = require"util.array";
local util = module:require 'util';
local ends_with = util.ends_with;
@@ -24,6 +25,11 @@ local get_focus_occupant = util.get_focus_occupant;
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
local presence_check_status = util.presence_check_status;
local PARTICIPANT_PROP_RAISE_HAND = 'jitsi_participant_raisedHand';
local PARTICIPANT_PROP_REQUEST_TRANSCRIPTION = 'jitsi_participant_requestingTranscription';
local PARTICIPANT_PROP_TRANSLATION_LANG = 'jitsi_participant_translation_language';
local TRANSCRIPT_DEFAULT_LANG = module:get_option_string('transcriptions_default_language', 'en');
-- this is the main virtual host of this vnode
local local_domain = module:get_option_string('muc_mapper_domain_base');
if not local_domain then
@@ -60,6 +66,62 @@ local function is_admin(jid)
return um_is_admin(jid, module.host);
end
local function send_transcriptions_update(room)
-- let's notify main prosody
local lang_array = array{};
local count = 0;
for k, v in pairs(room._transcription_languages) do
if not lang_array[v] then
lang_array:push(v);
end
count = count + 1;
end
local iq_id = new_id();
sent_iq_cache:set(iq_id, socket.gettime());
module:send(st.iq({
type = 'set',
to = 'visitors.'..main_domain,
from = local_domain,
id = iq_id })
:tag('visitors', { xmlns = 'jitsi:visitors',
room = jid.join(jid.node(room.jid), muc_domain_prefix..'.'..main_domain) })
:tag('transcription-languages', {
xmlns = 'jitsi:visitors',
langs = lang_array:sort():concat(','),
count = tostring(count)
}):up());
end
local function remove_transcription(room, occupant)
local send_update = false;
if room._transcription_languages then
if room._transcription_languages[occupant.jid] then
send_update = true;
end
room._transcription_languages[occupant.jid] = nil;
end
if send_update then
send_transcriptions_update(room);
end
end
-- if lang is nil we will remove it from the list
local function add_transcription(room, occupant, lang)
if not room._transcription_languages then
room._transcription_languages = {};
end
local old = room._transcription_languages[occupant.jid];
room._transcription_languages[occupant.jid] = lang or TRANSCRIPT_DEFAULT_LANG;
if old ~= room._transcription_languages[occupant.jid] then
send_transcriptions_update(room);
end
end
-- mark all occupants as visitors
module:hook('muc-occupant-pre-join', function (event)
local occupant, room, origin, stanza = event.occupant, event.room, event.origin, event.stanza;
@@ -92,7 +154,7 @@ module:hook('muc-occupant-pre-leave', function (event)
-- to main prosody
local pr = occupant:get_presence();
local raiseHand = pr:get_child_text('jitsi_participant_raisedHand');
local raiseHand = pr:get_child_text(PARTICIPANT_PROP_RAISE_HAND);
-- a promotion detected let's send it to main prosody
if raiseHand and #raiseHand > 0 then
@@ -114,6 +176,7 @@ module:hook('muc-occupant-pre-leave', function (event)
module:send(promotion_request);
end
remove_transcription(room, occupant);
end, 1); -- rate limit is 0
-- Returns the main participants count and the visitors count
@@ -201,6 +264,7 @@ end);
-- forward visitor presences to jicofo
-- detects raise hand in visitors presence, this is request for promotion
-- detects the requested transcription and its language to send updates for it
module:hook('muc-broadcast-presence', function (event)
local occupant = event.occupant;
@@ -230,7 +294,7 @@ module:hook('muc-broadcast-presence', function (event)
full_p.attr.to = focus_occupant.jid;
room:route_to_occupant(focus_occupant, full_p);
local raiseHand = full_p:get_child_text('jitsi_participant_raisedHand');
local raiseHand = full_p:get_child_text(PARTICIPANT_PROP_RAISE_HAND);
-- a promotion detected let's send it to main prosody
if raiseHand then
local user_id;
@@ -286,6 +350,18 @@ module:hook('muc-broadcast-presence', function (event)
module:send(promotion_request);
end
-- detect transcription
if full_p:get_child_text(PARTICIPANT_PROP_REQUEST_TRANSCRIPTION) then
local lang = full_p:get_child_text(PARTICIPANT_PROP_TRANSLATION_LANG);
occupant._transcription_enabled = true;
add_transcription(room, occupant, lang);
elseif occupant._transcription_enabled then
occupant._transcription_enabled = false;
remove_transcription(room, occupant, nil);
end
return;
end);

View File

@@ -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!");

View File

@@ -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);

View File

@@ -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

View File

@@ -14,6 +14,7 @@ local new_id = require 'util.id'.medium;
local util = module:require 'util';
local presence_check_status = util.presence_check_status;
local process_host_module = util.process_host_module;
local is_transcriber_jigasi = util.is_transcriber_jigasi;
local um_is_admin = require 'core.usermanager'.is_admin;
local function is_admin(jid)
@@ -185,7 +186,7 @@ process_host_module(main_muc_component_config, function(host_module, host)
-- filter focus and configured domains (used for jibri and transcribers)
if is_admin(stanza.attr.from) or visitors_nodes[room.jid] == nil
or ignore_list:contains(jid.host(occupant.bare_jid)) then
or (ignore_list:contains(jid.host(occupant.bare_jid)) and not is_transcriber_jigasi(stanza)) then
return;
end
@@ -206,7 +207,7 @@ process_host_module(main_muc_component_config, function(host_module, host)
-- ignore configured domains (jibri and transcribers)
if is_admin(occupant.bare_jid) or visitors_nodes[room.jid] == nil or visitors_nodes[room.jid].nodes == nil
or ignore_list:contains(jid.host(occupant.bare_jid)) then
or (ignore_list:contains(jid.host(occupant.bare_jid)) and not is_transcriber_jigasi(stanza)) then
return;
end
@@ -249,7 +250,7 @@ process_host_module(main_muc_component_config, function(host_module, host)
-- filter focus, ignore configured domains (jibri and transcribers)
if is_admin(stanza.attr.from) or visitors_nodes[room.jid] == nil
or ignore_list:contains(jid.host(occupant.bare_jid)) then
or (ignore_list:contains(jid.host(occupant.bare_jid)) and not is_transcriber_jigasi(stanza)) then
return;
end
@@ -268,7 +269,7 @@ process_host_module(main_muc_component_config, function(host_module, host)
local room, stanza, occupant = event.room, event.stanza, event.occupant;
-- filter sending messages from transcribers/jibris to visitors
if not visitors_nodes[room.jid] or ignore_list:contains(jid.host(occupant.bare_jid)) then
if not visitors_nodes[room.jid] then
return;
end

View File

@@ -266,6 +266,34 @@ local function stanza_handler(event)
end
end
-- request to update metadata service for jigasi languages
local transcription_languages = visitors_iq:get_child('transcription-languages');
if transcription_languages
and (transcription_languages.attr.langs or transcription_languages.attr.count) then
if not room.jitsiMetadata then
room.jitsiMetadata = {};
end
if not room.jitsiMetadata.visitors then
room.jitsiMetadata.visitors = {};
end
if room.jitsiMetadata.visitors.transcribingLanguages ~= transcription_languages then
room.jitsiMetadata.visitors.transcribingLanguages = transcription_languages.attr.langs;
processed = true;
end
if transcription_languages.attr.count then
room.jitsiMetadata.visitors.transcribingCount = transcription_languages.attr.count;
processed = true;
end
if processed then
module:context(muc_domain_prefix..'.'..muc_domain_base)
:fire_event('room-metadata-changed', { room = room; });
end
end
if not processed then
module:log('warn', 'Unknown iq received for %s: %s', module.host, stanza);
end

View File

@@ -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
@@ -471,6 +473,23 @@ function is_sip_jigasi(stanza)
return stanza:get_child('initiator', 'http://jitsi.org/protocol/jigasi');
end
function is_transcriber_jigasi(stanza)
local features = stanza:get_child('features');
if not features then
return false;
end
for i = 1, #features do
local feature = features[i];
if feature.attr and feature.attr.var and feature.attr.var == 'http://jitsi.org/protocol/transcriber' then
return true;
end
end
return false;
end
function get_sip_jibri_email_prefix(email)
if not email then
return nil;
@@ -544,6 +563,7 @@ return {
is_moderated = is_moderated;
is_sip_jibri_join = is_sip_jibri_join;
is_sip_jigasi = is_sip_jigasi;
is_transcriber_jigasi = is_transcriber_jigasi;
is_vpaas = is_vpaas;
get_focus_occupant = get_focus_occupant;
get_room_from_jid = get_room_from_jid;