From fa0ad14c3554c99d61a7f32493329de93e2f4541 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha <33319791+danielpetri1@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:56:33 +0200 Subject: [PATCH] feat(bbb-html5): Ban specific users from the public chat (#20585) * Initial user lock changes * Show lock icon --- .../LockUserChatInMeetingCmdMsgHdlr.scala | 36 ++++++++++++++++++ .../core/apps/users/UsersApp2x.scala | 1 + .../senders/ReceivedJsonMsgHandlerActor.scala | 2 + .../core/running/MeetingActor.scala | 1 + .../common2/msgs/UsersMsgs.scala | 14 +++++++ .../api/meetings/server/eventHandlers.js | 2 + .../handlers/handleUserChatLockChange.js | 5 +++ .../server/modifiers/changeUserChatLock.js | 37 +++++++++++++++++++ .../imports/api/users/server/methods.js | 2 + .../server/methods/toggleUserChatLock.js | 34 +++++++++++++++++ .../imports/ui/components/chat/container.jsx | 2 + .../imports/ui/components/chat/service.js | 4 ++ .../ui/components/user-list/service.js | 5 +++ .../user-list-item/component.jsx | 36 +++++++++++++++++- .../user-list-item/container.jsx | 2 + bigbluebutton-html5/public/locales/en.json | 2 + 16 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/LockUserChatInMeetingCmdMsgHdlr.scala create mode 100644 bigbluebutton-html5/imports/api/meetings/server/handlers/handleUserChatLockChange.js create mode 100644 bigbluebutton-html5/imports/api/meetings/server/modifiers/changeUserChatLock.js create mode 100644 bigbluebutton-html5/imports/api/users/server/methods/toggleUserChatLock.js diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/LockUserChatInMeetingCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/LockUserChatInMeetingCmdMsgHdlr.scala new file mode 100644 index 0000000000..37e3b5bbf7 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/LockUserChatInMeetingCmdMsgHdlr.scala @@ -0,0 +1,36 @@ +package org.bigbluebutton.core.apps.users + +import org.bigbluebutton.LockSettingsUtil +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.models.{ Users2x, VoiceUsers } +import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter } +import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait } + +trait LockUserChatInMeetingCmdMsgHdlr extends RightsManagementTrait { + this: MeetingActor => + + val outGW: OutMsgRouter + + def handleLockUserChatInMeetingCmdMsg(msg: LockUserChatInMeetingCmdMsg) { + + def build(meetingId: String, userId: String, isLocked: Boolean): BbbCommonEnvCoreMsg = { + val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId) + val envelope = BbbCoreEnvelope(LockUserChatInMeetingEvtMsg.NAME, routing) + val body = LockUserChatInMeetingEvtMsgBody(userId, isLocked) + val header = BbbClientMsgHeader(LockUserChatInMeetingEvtMsg.NAME, meetingId, userId) + val event = LockUserChatInMeetingEvtMsg(header, body) + + BbbCommonEnvCoreMsg(envelope, event) + } + + if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) { + val meetingId = liveMeeting.props.meetingProp.intId + val reason = "No permission to lock user chat in meeting." + PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting) + } else { + log.info("Lock user chat. meetingId=" + props.meetingProp.intId + " userId=" + msg.body.userId + " isLocked=" + msg.body.isLocked) + val event = build(props.meetingProp.intId, msg.body.userId, msg.body.isLocked) + outGW.send(event) + } + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp2x.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp2x.scala index f2a4fe4f53..b6efc392a1 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp2x.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp2x.scala @@ -5,6 +5,7 @@ import org.bigbluebutton.core.running.MeetingActor trait UsersApp2x extends UserLeaveReqMsgHdlr with LockUserInMeetingCmdMsgHdlr + with LockUserChatInMeetingCmdMsgHdlr with LockUsersInMeetingCmdMsgHdlr with GetLockSettingsReqMsgHdlr with ChangeUserEmojiCmdMsgHdlr diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala index 76e0995b3d..212be53fc7 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala @@ -386,6 +386,8 @@ class ReceivedJsonMsgHandlerActor( // Lock settings case LockUserInMeetingCmdMsg.NAME => routeGenericMsg[LockUserInMeetingCmdMsg](envelope, jsonNode) + case LockUserChatInMeetingCmdMsg.NAME => + routeGenericMsg[LockUserChatInMeetingCmdMsg](envelope, jsonNode) case ChangeLockSettingsInMeetingCmdMsg.NAME => routeGenericMsg[ChangeLockSettingsInMeetingCmdMsg](envelope, jsonNode) case LockUsersInMeetingCmdMsg.NAME => diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index df253bdc3d..07b18cf958 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -514,6 +514,7 @@ class MeetingActor( handleSetLockSettings(m) updateUserLastActivity(m.body.setBy) case m: LockUserInMeetingCmdMsg => handleLockUserInMeetingCmdMsg(m) + case m: LockUserChatInMeetingCmdMsg => handleLockUserChatInMeetingCmdMsg(m) case m: LockUsersInMeetingCmdMsg => handleLockUsersInMeetingCmdMsg(m) case m: GetLockSettingsReqMsg => handleGetLockSettingsReqMsg(m) diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UsersMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UsersMsgs.scala index a72b8806ed..4a61f738c4 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UsersMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UsersMsgs.scala @@ -347,6 +347,20 @@ object UserLockedInMeetingEvtMsg { val NAME = "UserLockedInMeetingEvtMsg" } case class UserLockedInMeetingEvtMsg(header: BbbClientMsgHeader, body: UserLockedInMeetingEvtMsgBody) extends BbbCoreMsg case class UserLockedInMeetingEvtMsgBody(userId: String, locked: Boolean, lockedBy: String) +/** + * Sent from client to lock user in meeting. + */ +object LockUserChatInMeetingCmdMsg { val NAME = "LockUserChatInMeetingCmdMsg" } +case class LockUserChatInMeetingCmdMsg(header: BbbClientMsgHeader, body: LockUserChatInMeetingCmdMsgBody) extends StandardMsg +case class LockUserChatInMeetingCmdMsgBody(userId: String, isLocked: Boolean, lockedBy: String) + +/** + * Send to client that user has been locked. + */ +object LockUserChatInMeetingEvtMsg { val NAME = "LockUserChatInMeetingEvtMsg" } +case class LockUserChatInMeetingEvtMsg(header: BbbClientMsgHeader, body: LockUserChatInMeetingEvtMsgBody) extends BbbCoreMsg +case class LockUserChatInMeetingEvtMsgBody(userId: String, isLocked: Boolean) + /** * Sent by client to lock users. */ diff --git a/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js b/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js index 777b53fdd1..57ebfe8d8f 100644 --- a/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js +++ b/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js @@ -7,6 +7,7 @@ import handleMeetingLocksChange from './handlers/meetingLockChange'; import handleGuestPolicyChanged from './handlers/guestPolicyChanged'; import handleGuestLobbyMessageChanged from './handlers/guestLobbyMessageChanged'; import handleUserLockChange from './handlers/userLockChange'; +import handleUserChatLockChange from './handlers/handleUserChatLockChange'; import handleRecordingStatusChange from './handlers/recordingStatusChange'; import handleRecordingTimerChange from './handlers/recordingTimerChange'; import handleTimeRemainingUpdate from './handlers/timeRemainingUpdate'; @@ -24,6 +25,7 @@ RedisPubSub.on('MeetingEndingEvtMsg', handleMeetingEnd); RedisPubSub.on('MeetingDestroyedEvtMsg', handleMeetingDestruction); RedisPubSub.on('LockSettingsInMeetingChangedEvtMsg', handleMeetingLocksChange); RedisPubSub.on('UserLockedInMeetingEvtMsg', handleUserLockChange); +RedisPubSub.on('LockUserChatInMeetingEvtMsg', handleUserChatLockChange); RedisPubSub.on('RecordingStatusChangedEvtMsg', handleRecordingStatusChange); RedisPubSub.on('UpdateRecordingTimerEvtMsg', handleRecordingTimerChange); RedisPubSub.on('WebcamsOnlyForModeratorChangedEvtMsg', handleChangeWebcamOnlyModerator); diff --git a/bigbluebutton-html5/imports/api/meetings/server/handlers/handleUserChatLockChange.js b/bigbluebutton-html5/imports/api/meetings/server/handlers/handleUserChatLockChange.js new file mode 100644 index 0000000000..6255995169 --- /dev/null +++ b/bigbluebutton-html5/imports/api/meetings/server/handlers/handleUserChatLockChange.js @@ -0,0 +1,5 @@ +import changeUserChatLock from '../modifiers/changeUserChatLock'; + +export default async function handleUserChatLockChange({ body }, meetingId) { + await changeUserChatLock(meetingId, body); +} diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/changeUserChatLock.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/changeUserChatLock.js new file mode 100644 index 0000000000..e5a5f7efa1 --- /dev/null +++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/changeUserChatLock.js @@ -0,0 +1,37 @@ +import Logger from '/imports/startup/server/logger'; +import Users from '/imports/api/users'; +import Meetings from '/imports/api/meetings'; +import { check } from 'meteor/check'; + +export default async function changeUserChatLock(meetingId, payload) { + check(meetingId, String); + check(payload, { + userId: String, + isLocked: Boolean, + }); + + const { userId, isLocked } = payload; + + const userSelector = { + meetingId, + userId, + }; + + const userModifier = { + $set: { + chatLocked: isLocked, + }, + }; + + try { + const { numberAffected } = await Users.upsertAsync(userSelector, userModifier); + + if ( numberAffected ) { + Logger.info(`Updated lock settings in meeting ${meetingId}: disablePublicChat=${isLocked}`); + } else { + Logger.info(`Kept lock settings in meeting ${meetingId}`); + } + } catch (err) { + Logger.error(`Changing user chat lock setting: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/users/server/methods.js b/bigbluebutton-html5/imports/api/users/server/methods.js index 1d491951d1..f6f7303fbc 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods.js +++ b/bigbluebutton-html5/imports/api/users/server/methods.js @@ -10,6 +10,7 @@ import assignPresenter from './methods/assignPresenter'; import changeRole from './methods/changeRole'; import removeUser from './methods/removeUser'; import toggleUserLock from './methods/toggleUserLock'; +import toggleUserChatLock from './methods/toggleUserChatLock'; import setUserEffectiveConnectionType from './methods/setUserEffectiveConnectionType'; import userActivitySign from './methods/userActivitySign'; import userLeftMeeting from './methods/userLeftMeeting'; @@ -31,6 +32,7 @@ Meteor.methods({ removeUser, validateAuthToken, toggleUserLock, + toggleUserChatLock, setUserEffectiveConnectionType, userActivitySign, userLeftMeeting, diff --git a/bigbluebutton-html5/imports/api/users/server/methods/toggleUserChatLock.js b/bigbluebutton-html5/imports/api/users/server/methods/toggleUserChatLock.js new file mode 100644 index 0000000000..65942109e4 --- /dev/null +++ b/bigbluebutton-html5/imports/api/users/server/methods/toggleUserChatLock.js @@ -0,0 +1,34 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import RedisPubSub from '/imports/startup/server/redis'; +import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; + +export default function toggleUserChatLock(userId, isLocked) { + try { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'LockUserChatInMeetingCmdMsg'; + + const { meetingId, requesterUserId: lockedBy } = extractCredentials(this.userId); + + check(meetingId, String); + check(lockedBy, String); + check(userId, String); + check(isLocked, Boolean); + + const payload = { + lockedBy, + userId, + isLocked, + }; + + Logger.verbose('Updated chat lock status for user', { + meetingId, userId, isLocked, lockedBy, + }); + + RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, lockedBy, payload); + } catch (err) { + Logger.error(`Exception while invoking method toggleUserChatLock ${err.stack}`); + } +} diff --git a/bigbluebutton-html5/imports/ui/components/chat/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/container.jsx index 06c4e23a14..e3374ea425 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/container.jsx @@ -150,6 +150,8 @@ const ChatContainer = (props) => { partnerIsLoggedOut = !!(users[Auth.meetingID][idUser]?.loggedOut || users[Auth.meetingID][idUser]?.ejected); isChatLocked = isChatLockedPrivate && !(users[Auth.meetingID][idUser]?.role === ROLE_MODERATOR); + } else if (users[Auth.meetingID][Auth.userID]?.chatLocked === true) { + isChatLocked = true; } else { isChatLocked = isChatLockedPublic; } diff --git a/bigbluebutton-html5/imports/ui/components/chat/service.js b/bigbluebutton-html5/imports/ui/components/chat/service.js index 71fe57125a..d667cfa07f 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/service.js +++ b/bigbluebutton-html5/imports/ui/components/chat/service.js @@ -169,6 +169,10 @@ const isChatLocked = (receiverID) => { } } + if (user.chatLocked === true && user.role !== ROLE_MODERATOR) { + return true; + } + return false; }; diff --git a/bigbluebutton-html5/imports/ui/components/user-list/service.js b/bigbluebutton-html5/imports/ui/components/user-list/service.js index b8ad75d7d6..1f3bdd0ee1 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/service.js +++ b/bigbluebutton-html5/imports/ui/components/user-list/service.js @@ -674,6 +674,10 @@ const getGroupChatPrivate = (senderUserId, receiver) => { } }; +const toggleUserChatLock = (userId, isLocked) => { + makeCall('toggleUserChatLock', userId, isLocked); +} + const toggleUserLock = (userId, lockStatus) => { makeCall('toggleUserLock', userId, lockStatus); }; @@ -820,6 +824,7 @@ export default { roving, getCustomLogoUrl, getGroupChatPrivate, + toggleUserChatLock, hasBreakoutRoom, getEmojiList: () => EMOJI_STATUSES, getEmoji, diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx index d3e135fb6b..91fe487dbc 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx @@ -47,6 +47,14 @@ const messages = defineMessages({ id: 'app.audio.backLabel', description: 'label for option to hide emoji menu', }, + lockPublicChat: { + id: 'app.userList.menu.lockPublicChat.label', + description: 'label for option to lock user\'s public chat', + }, + unlockPublicChat: { + id: 'app.userList.menu.unlockPublicChat.label', + description: 'label for option to lock user\'s public chat', + }, StartPrivateChat: { id: 'app.userList.menu.chat.label', description: 'label for option to start a new private chat', @@ -175,6 +183,7 @@ const propTypes = { normalizeEmojiName: PropTypes.func.isRequired, getScrollContainerRef: PropTypes.func.isRequired, toggleUserLock: PropTypes.func.isRequired, + toggleUserChatLock: PropTypes.func.isRequired, isMeteorConnected: PropTypes.bool.isRequired, isMe: PropTypes.func.isRequired, }; @@ -286,6 +295,7 @@ class UserListItem extends PureComponent { voiceUser, getAvailableActions, getGroupChatPrivate, + toggleUserChatLock, getEmojiList, setEmojiStatus, setUserAway, @@ -345,6 +355,7 @@ class UserListItem extends PureComponent { const { allowUserLookup } = Meteor.settings.public.app; const userLocked = user.locked && user.role !== ROLE_MODERATOR; + const userChatLocked = user.chatLocked; const availableActions = [ { @@ -406,6 +417,29 @@ class UserListItem extends PureComponent { icon: 'chat', dataTest: 'startPrivateChat', }, + { + allowed: isChatEnabled() + && user.role !== ROLE_MODERATOR + && currentUser.role === ROLE_MODERATOR + && !isDialInUser + && isMeteorConnected, + key: 'lockChat', + label: userChatLocked ? intl.formatMessage(messages.unlockPublicChat) : intl.formatMessage(messages.lockPublicChat), + onClick: () => { + this.handleClose(); + toggleUserChatLock(user.userId, !userChatLocked); + layoutContextDispatch({ + type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN, + value: true, + }); + layoutContextDispatch({ + type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL, + value: PANELS.CHAT, + }); + }, + icon: userChatLocked ? 'unlock' : 'lock', + dataTest: 'togglePublicChat', + }, { allowed: allowedToResetStatus && user.emoji !== 'none' @@ -786,7 +820,7 @@ class UserListItem extends PureComponent { ); } - if (isThisMeetingLocked && user.locked && user.role !== ROLE_MODERATOR) { + if (((isThisMeetingLocked && user.locked) || user.chatLocked) && user.role !== ROLE_MODERATOR) { userNameSub.push( diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/container.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/container.jsx index 0644eb124a..63850c3014 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/container.jsx @@ -14,6 +14,7 @@ const UserListItemContainer = (props) => { toggleVoice, removeUser, toggleUserLock, + toggleUserChatLock, changeRole, ejectUserCameras, assignPresenter, @@ -29,6 +30,7 @@ const UserListItemContainer = (props) => { toggleVoice, removeUser, toggleUserLock, + toggleUserChatLock, changeRole, ejectUserCameras, assignPresenter, diff --git a/bigbluebutton-html5/public/locales/en.json b/bigbluebutton-html5/public/locales/en.json index d45ac7aa91..daa23288f9 100755 --- a/bigbluebutton-html5/public/locales/en.json +++ b/bigbluebutton-html5/public/locales/en.json @@ -151,6 +151,8 @@ "app.userList.menu.away": "Set yourself as away", "app.userList.menu.notAway": "Set yourself as active", "app.userList.menu.chat.label": "Start a private chat", + "app.userList.menu.lockPublicChat.label": "Lock public chat", + "app.userList.menu.unlockPublicChat.label": "Unlock public chat", "app.userList.menu.clearStatus.label": "Clear status", "app.userList.menu.removeUser.label": "Remove user", "app.userList.menu.removeConfirmation.label": "Remove user ({0})",