diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/SetRecordingStatusCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/SetRecordingStatusCmdMsgHdlr.scala index 2e9e5f51ba..f7cda39d01 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/SetRecordingStatusCmdMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/SetRecordingStatusCmdMsgHdlr.scala @@ -9,6 +9,7 @@ import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait } import org.bigbluebutton.core2.message.senders.MsgBuilder import org.bigbluebutton.core.apps.voice.VoiceApp import org.bigbluebutton.core.db.{ MeetingRecordingDAO, NotificationDAO } +import org.bigbluebutton.core.models.{ Users2x } trait SetRecordingStatusCmdMsgHdlr extends RightsManagementTrait { this: UsersApp => @@ -29,9 +30,19 @@ trait SetRecordingStatusCmdMsgHdlr extends RightsManagementTrait { BbbCommonEnvCoreMsg(envelope, event) } - if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) { + // Retrieve custom record permission from metadata + val customRecordPermission: Option[Boolean] = Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId).flatMap { user => + user.userMetadata.get("bbb_record_permission").map(_.toBoolean) + } + + // Determine final permission using metadata or fallback + val hasPermission: Boolean = customRecordPermission.getOrElse { + !permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) + } + + if (!hasPermission) { val meetingId = liveMeeting.props.meetingProp.intId - val reason = "No permission to clear chat in meeting." + val reason = "No permission to set recording status in meeting." PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting) state } else { diff --git a/bbb-graphql-actions/src/actions/meetingRecordingSetStatus.ts b/bbb-graphql-actions/src/actions/meetingRecordingSetStatus.ts index 62a12d6a5b..dbc8e81157 100644 --- a/bbb-graphql-actions/src/actions/meetingRecordingSetStatus.ts +++ b/bbb-graphql-actions/src/actions/meetingRecordingSetStatus.ts @@ -1,12 +1,11 @@ import { RedisMessage } from '../types'; -import {throwErrorIfInvalidInput, throwErrorIfNotModerator} from "../imports/validation"; +import { throwErrorIfInvalidInput } from "../imports/validation"; export default function buildRedisMessage(sessionVariables: Record, input: Record): RedisMessage { - throwErrorIfNotModerator(sessionVariables); throwErrorIfInvalidInput(input, - [ - {name: 'recording', type: 'boolean', required: true}, - ] + [ + { name: 'recording', type: 'boolean', required: true }, + ] ) const eventName = 'SetRecordingStatusCmdMsg'; diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/nav-bar-graphql/recording-indicator/component.tsx b/bigbluebutton-html5/imports/ui/components/nav-bar/nav-bar-graphql/recording-indicator/component.tsx index 0350903004..935fe113d9 100644 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/nav-bar-graphql/recording-indicator/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/nav-bar-graphql/recording-indicator/component.tsx @@ -6,6 +6,7 @@ import React, { } from 'react'; import useMeeting from '/imports/ui/core/hooks/useMeeting'; import deviceInfo, { isMobile } from '/imports/utils/deviceInfo'; +import { defineMessages, useIntl } from 'react-intl'; import { GET_MEETING_RECORDING_DATA, GET_MEETING_RECORDING_POLICIES, @@ -20,7 +21,6 @@ import humanizeSeconds from '/imports/utils/humanizeSeconds'; import { notify } from '/imports/ui/services/notification'; import Styled from './styles'; import { User } from '/imports/ui/Types/user'; -import { defineMessages, useIntl } from 'react-intl'; import useTimeSync from '/imports/ui/core/local-states/useTimeSync'; import RecordingNotify from './notify/component'; import RecordingContainer from '/imports/ui/components/recording/container'; @@ -28,6 +28,7 @@ import useDeduplicatedSubscription from '/imports/ui/core/hooks/useDeduplicatedS import { getSettingsSingletonInstance } from '/imports/ui/services/settings'; import logger from '/imports/startup/client/logger'; import SvgIcon from '/imports/ui/components/common/icon-svg/component'; +import Service from './service'; const intlMessages = defineMessages({ notificationRecordingStart: { @@ -233,7 +234,9 @@ const RecordingIndicator: React.FC = ({ } const recordingButton = recording ? recordMeetingButtonWithTooltip : recordMeetingButton; - const showButton = isModerator && allowStartStopRecording; + const showButton = Service.mayIRecord(isModerator, allowStartStopRecording); + const defaultRecordTooltip = intl.formatMessage(intlMessages.notificationRecordingStop); + const customRecordTooltip = Service.getCustomRecordTooltip(defaultRecordTooltip); if (!record) return null; return ( <> @@ -249,18 +252,14 @@ const RecordingIndicator: React.FC = ({ {showButton ? recordingButton : null} {showButton ? null : ( {recordingIndicatorIcon} diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/nav-bar-graphql/recording-indicator/notify/service.tsx b/bigbluebutton-html5/imports/ui/components/nav-bar/nav-bar-graphql/recording-indicator/notify/service.tsx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/nav-bar-graphql/recording-indicator/service.tsx b/bigbluebutton-html5/imports/ui/components/nav-bar/nav-bar-graphql/recording-indicator/service.tsx new file mode 100644 index 0000000000..6f2307a9f5 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/nav-bar-graphql/recording-indicator/service.tsx @@ -0,0 +1,15 @@ +import getFromUserSettings from '/imports/ui/services/users-settings'; + +const mayIRecord = (amIModerator: boolean, allowStartStopRecording: boolean) => { + const customRecord = getFromUserSettings('bbb_record_permission', undefined); + if (!allowStartStopRecording) return false; + if (customRecord !== undefined) { + return customRecord; + } + return amIModerator; +}; + +export default { + mayIRecord, + getCustomRecordTooltip: (value: string) => getFromUserSettings('bbb_record_permission_tooltip', value), +}; diff --git a/bigbluebutton-html5/imports/ui/components/recording/component.tsx b/bigbluebutton-html5/imports/ui/components/recording/component.tsx index 520ee37eca..4a4e1756e6 100755 --- a/bigbluebutton-html5/imports/ui/components/recording/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/recording/component.tsx @@ -46,7 +46,6 @@ const intlMessages = defineMessages({ }); interface RecordingComponentProps { - amIModerator: boolean; connected: boolean; isOpen: boolean; recordingStatus: boolean, @@ -59,7 +58,6 @@ interface RecordingComponentProps { const RecordingComponent: React.FC = (props) => { const { - amIModerator, connected, isOpen, recordingStatus, @@ -76,8 +74,6 @@ const RecordingComponent: React.FC = (props) => { let description; let cancelButtonLabel; - if (!amIModerator) return null; - if (recordingStatus) { description = intl.formatMessage(intlMessages.stopDescription); title = intl.formatMessage(intlMessages.stopTitle); diff --git a/bigbluebutton-html5/imports/ui/components/recording/container.tsx b/bigbluebutton-html5/imports/ui/components/recording/container.tsx index 8ce64d0941..6dd19b517a 100755 --- a/bigbluebutton-html5/imports/ui/components/recording/container.tsx +++ b/bigbluebutton-html5/imports/ui/components/recording/container.tsx @@ -2,10 +2,11 @@ import React from 'react'; import { useMutation, useReactiveVar } from '@apollo/client'; import RecordingComponent from './component'; import { SET_RECORDING_STATUS } from './mutations'; -import { GetRecordingResponse } from './queries'; +import { GetRecordingResponse, GetRecordingPoliciesResponse } from './queries'; import useDeduplicatedSubscription from '/imports/ui/core/hooks/useDeduplicatedSubscription'; import ConnectionStatus from '/imports/ui/core/graphql/singletons/connectionStatus'; -import { GET_MEETING_RECORDING_DATA } from '/imports/ui/components/nav-bar/nav-bar-graphql/recording-indicator/queries'; +import { GET_MEETING_RECORDING_DATA, GET_MEETING_RECORDING_POLICIES } from '/imports/ui/components/nav-bar/nav-bar-graphql/recording-indicator/queries'; +import Service from '/imports/ui/components/nav-bar/nav-bar-graphql/recording-indicator/service'; interface RecordingContainerProps { setIsOpen: React.Dispatch>; @@ -24,9 +25,13 @@ const RecordingContainer: React.FC = (props) => { const { data: recordingData, } = useDeduplicatedSubscription(GET_MEETING_RECORDING_DATA); + const { + data: recordingPoliciesData, + } = useDeduplicatedSubscription(GET_MEETING_RECORDING_POLICIES); const recording = recordingData?.meeting_recording[0]?.isRecording ?? false; const time = recordingData?.meeting_recording[0]?.previousRecordedTimeInSeconds ?? 0; + const allowStartStopRecording = recordingPoliciesData?.meeting_recordingPolicies[0]?.allowStartStopRecording ?? false; const toggleRecording = () => { setRecordingStatus({ @@ -37,10 +42,13 @@ const RecordingContainer: React.FC = (props) => { setIsOpen(false); }; + const mayIRecord = Service.mayIRecord(amIModerator, allowStartStopRecording); + + if (!mayIRecord) return null; + return (