feat(bbb-html5): Ban specific users from the public chat (#20585)

* Initial user lock changes

* Show lock icon
This commit is contained in:
Daniel Petri Rocha 2024-07-17 18:56:33 +02:00 committed by GitHub
parent 15525a6936
commit fa0ad14c35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 184 additions and 1 deletions

View File

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

View File

@ -5,6 +5,7 @@ import org.bigbluebutton.core.running.MeetingActor
trait UsersApp2x trait UsersApp2x
extends UserLeaveReqMsgHdlr extends UserLeaveReqMsgHdlr
with LockUserInMeetingCmdMsgHdlr with LockUserInMeetingCmdMsgHdlr
with LockUserChatInMeetingCmdMsgHdlr
with LockUsersInMeetingCmdMsgHdlr with LockUsersInMeetingCmdMsgHdlr
with GetLockSettingsReqMsgHdlr with GetLockSettingsReqMsgHdlr
with ChangeUserEmojiCmdMsgHdlr with ChangeUserEmojiCmdMsgHdlr

View File

@ -386,6 +386,8 @@ class ReceivedJsonMsgHandlerActor(
// Lock settings // Lock settings
case LockUserInMeetingCmdMsg.NAME => case LockUserInMeetingCmdMsg.NAME =>
routeGenericMsg[LockUserInMeetingCmdMsg](envelope, jsonNode) routeGenericMsg[LockUserInMeetingCmdMsg](envelope, jsonNode)
case LockUserChatInMeetingCmdMsg.NAME =>
routeGenericMsg[LockUserChatInMeetingCmdMsg](envelope, jsonNode)
case ChangeLockSettingsInMeetingCmdMsg.NAME => case ChangeLockSettingsInMeetingCmdMsg.NAME =>
routeGenericMsg[ChangeLockSettingsInMeetingCmdMsg](envelope, jsonNode) routeGenericMsg[ChangeLockSettingsInMeetingCmdMsg](envelope, jsonNode)
case LockUsersInMeetingCmdMsg.NAME => case LockUsersInMeetingCmdMsg.NAME =>

View File

@ -514,6 +514,7 @@ class MeetingActor(
handleSetLockSettings(m) handleSetLockSettings(m)
updateUserLastActivity(m.body.setBy) updateUserLastActivity(m.body.setBy)
case m: LockUserInMeetingCmdMsg => handleLockUserInMeetingCmdMsg(m) case m: LockUserInMeetingCmdMsg => handleLockUserInMeetingCmdMsg(m)
case m: LockUserChatInMeetingCmdMsg => handleLockUserChatInMeetingCmdMsg(m)
case m: LockUsersInMeetingCmdMsg => handleLockUsersInMeetingCmdMsg(m) case m: LockUsersInMeetingCmdMsg => handleLockUsersInMeetingCmdMsg(m)
case m: GetLockSettingsReqMsg => handleGetLockSettingsReqMsg(m) case m: GetLockSettingsReqMsg => handleGetLockSettingsReqMsg(m)

View File

@ -347,6 +347,20 @@ object UserLockedInMeetingEvtMsg { val NAME = "UserLockedInMeetingEvtMsg" }
case class UserLockedInMeetingEvtMsg(header: BbbClientMsgHeader, body: UserLockedInMeetingEvtMsgBody) extends BbbCoreMsg case class UserLockedInMeetingEvtMsg(header: BbbClientMsgHeader, body: UserLockedInMeetingEvtMsgBody) extends BbbCoreMsg
case class UserLockedInMeetingEvtMsgBody(userId: String, locked: Boolean, lockedBy: String) 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. * Sent by client to lock users.
*/ */

View File

@ -7,6 +7,7 @@ import handleMeetingLocksChange from './handlers/meetingLockChange';
import handleGuestPolicyChanged from './handlers/guestPolicyChanged'; import handleGuestPolicyChanged from './handlers/guestPolicyChanged';
import handleGuestLobbyMessageChanged from './handlers/guestLobbyMessageChanged'; import handleGuestLobbyMessageChanged from './handlers/guestLobbyMessageChanged';
import handleUserLockChange from './handlers/userLockChange'; import handleUserLockChange from './handlers/userLockChange';
import handleUserChatLockChange from './handlers/handleUserChatLockChange';
import handleRecordingStatusChange from './handlers/recordingStatusChange'; import handleRecordingStatusChange from './handlers/recordingStatusChange';
import handleRecordingTimerChange from './handlers/recordingTimerChange'; import handleRecordingTimerChange from './handlers/recordingTimerChange';
import handleTimeRemainingUpdate from './handlers/timeRemainingUpdate'; import handleTimeRemainingUpdate from './handlers/timeRemainingUpdate';
@ -24,6 +25,7 @@ RedisPubSub.on('MeetingEndingEvtMsg', handleMeetingEnd);
RedisPubSub.on('MeetingDestroyedEvtMsg', handleMeetingDestruction); RedisPubSub.on('MeetingDestroyedEvtMsg', handleMeetingDestruction);
RedisPubSub.on('LockSettingsInMeetingChangedEvtMsg', handleMeetingLocksChange); RedisPubSub.on('LockSettingsInMeetingChangedEvtMsg', handleMeetingLocksChange);
RedisPubSub.on('UserLockedInMeetingEvtMsg', handleUserLockChange); RedisPubSub.on('UserLockedInMeetingEvtMsg', handleUserLockChange);
RedisPubSub.on('LockUserChatInMeetingEvtMsg', handleUserChatLockChange);
RedisPubSub.on('RecordingStatusChangedEvtMsg', handleRecordingStatusChange); RedisPubSub.on('RecordingStatusChangedEvtMsg', handleRecordingStatusChange);
RedisPubSub.on('UpdateRecordingTimerEvtMsg', handleRecordingTimerChange); RedisPubSub.on('UpdateRecordingTimerEvtMsg', handleRecordingTimerChange);
RedisPubSub.on('WebcamsOnlyForModeratorChangedEvtMsg', handleChangeWebcamOnlyModerator); RedisPubSub.on('WebcamsOnlyForModeratorChangedEvtMsg', handleChangeWebcamOnlyModerator);

View File

@ -0,0 +1,5 @@
import changeUserChatLock from '../modifiers/changeUserChatLock';
export default async function handleUserChatLockChange({ body }, meetingId) {
await changeUserChatLock(meetingId, body);
}

View File

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

View File

@ -10,6 +10,7 @@ import assignPresenter from './methods/assignPresenter';
import changeRole from './methods/changeRole'; import changeRole from './methods/changeRole';
import removeUser from './methods/removeUser'; import removeUser from './methods/removeUser';
import toggleUserLock from './methods/toggleUserLock'; import toggleUserLock from './methods/toggleUserLock';
import toggleUserChatLock from './methods/toggleUserChatLock';
import setUserEffectiveConnectionType from './methods/setUserEffectiveConnectionType'; import setUserEffectiveConnectionType from './methods/setUserEffectiveConnectionType';
import userActivitySign from './methods/userActivitySign'; import userActivitySign from './methods/userActivitySign';
import userLeftMeeting from './methods/userLeftMeeting'; import userLeftMeeting from './methods/userLeftMeeting';
@ -31,6 +32,7 @@ Meteor.methods({
removeUser, removeUser,
validateAuthToken, validateAuthToken,
toggleUserLock, toggleUserLock,
toggleUserChatLock,
setUserEffectiveConnectionType, setUserEffectiveConnectionType,
userActivitySign, userActivitySign,
userLeftMeeting, userLeftMeeting,

View File

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

View File

@ -150,6 +150,8 @@ const ChatContainer = (props) => {
partnerIsLoggedOut = !!(users[Auth.meetingID][idUser]?.loggedOut partnerIsLoggedOut = !!(users[Auth.meetingID][idUser]?.loggedOut
|| users[Auth.meetingID][idUser]?.ejected); || users[Auth.meetingID][idUser]?.ejected);
isChatLocked = isChatLockedPrivate && !(users[Auth.meetingID][idUser]?.role === ROLE_MODERATOR); isChatLocked = isChatLockedPrivate && !(users[Auth.meetingID][idUser]?.role === ROLE_MODERATOR);
} else if (users[Auth.meetingID][Auth.userID]?.chatLocked === true) {
isChatLocked = true;
} else { } else {
isChatLocked = isChatLockedPublic; isChatLocked = isChatLockedPublic;
} }

View File

@ -169,6 +169,10 @@ const isChatLocked = (receiverID) => {
} }
} }
if (user.chatLocked === true && user.role !== ROLE_MODERATOR) {
return true;
}
return false; return false;
}; };

View File

@ -674,6 +674,10 @@ const getGroupChatPrivate = (senderUserId, receiver) => {
} }
}; };
const toggleUserChatLock = (userId, isLocked) => {
makeCall('toggleUserChatLock', userId, isLocked);
}
const toggleUserLock = (userId, lockStatus) => { const toggleUserLock = (userId, lockStatus) => {
makeCall('toggleUserLock', userId, lockStatus); makeCall('toggleUserLock', userId, lockStatus);
}; };
@ -820,6 +824,7 @@ export default {
roving, roving,
getCustomLogoUrl, getCustomLogoUrl,
getGroupChatPrivate, getGroupChatPrivate,
toggleUserChatLock,
hasBreakoutRoom, hasBreakoutRoom,
getEmojiList: () => EMOJI_STATUSES, getEmojiList: () => EMOJI_STATUSES,
getEmoji, getEmoji,

View File

@ -47,6 +47,14 @@ const messages = defineMessages({
id: 'app.audio.backLabel', id: 'app.audio.backLabel',
description: 'label for option to hide emoji menu', 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: { StartPrivateChat: {
id: 'app.userList.menu.chat.label', id: 'app.userList.menu.chat.label',
description: 'label for option to start a new private chat', description: 'label for option to start a new private chat',
@ -175,6 +183,7 @@ const propTypes = {
normalizeEmojiName: PropTypes.func.isRequired, normalizeEmojiName: PropTypes.func.isRequired,
getScrollContainerRef: PropTypes.func.isRequired, getScrollContainerRef: PropTypes.func.isRequired,
toggleUserLock: PropTypes.func.isRequired, toggleUserLock: PropTypes.func.isRequired,
toggleUserChatLock: PropTypes.func.isRequired,
isMeteorConnected: PropTypes.bool.isRequired, isMeteorConnected: PropTypes.bool.isRequired,
isMe: PropTypes.func.isRequired, isMe: PropTypes.func.isRequired,
}; };
@ -286,6 +295,7 @@ class UserListItem extends PureComponent {
voiceUser, voiceUser,
getAvailableActions, getAvailableActions,
getGroupChatPrivate, getGroupChatPrivate,
toggleUserChatLock,
getEmojiList, getEmojiList,
setEmojiStatus, setEmojiStatus,
setUserAway, setUserAway,
@ -345,6 +355,7 @@ class UserListItem extends PureComponent {
const { allowUserLookup } = Meteor.settings.public.app; const { allowUserLookup } = Meteor.settings.public.app;
const userLocked = user.locked && user.role !== ROLE_MODERATOR; const userLocked = user.locked && user.role !== ROLE_MODERATOR;
const userChatLocked = user.chatLocked;
const availableActions = [ const availableActions = [
{ {
@ -406,6 +417,29 @@ class UserListItem extends PureComponent {
icon: 'chat', icon: 'chat',
dataTest: 'startPrivateChat', 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 allowed: allowedToResetStatus
&& user.emoji !== 'none' && 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( userNameSub.push(
<span key={uniqueId('lock-')}> <span key={uniqueId('lock-')}>
<Icon iconName="lock" /> <Icon iconName="lock" />

View File

@ -14,6 +14,7 @@ const UserListItemContainer = (props) => {
toggleVoice, toggleVoice,
removeUser, removeUser,
toggleUserLock, toggleUserLock,
toggleUserChatLock,
changeRole, changeRole,
ejectUserCameras, ejectUserCameras,
assignPresenter, assignPresenter,
@ -29,6 +30,7 @@ const UserListItemContainer = (props) => {
toggleVoice, toggleVoice,
removeUser, removeUser,
toggleUserLock, toggleUserLock,
toggleUserChatLock,
changeRole, changeRole,
ejectUserCameras, ejectUserCameras,
assignPresenter, assignPresenter,

View File

@ -151,6 +151,8 @@
"app.userList.menu.away": "Set yourself as away", "app.userList.menu.away": "Set yourself as away",
"app.userList.menu.notAway": "Set yourself as active", "app.userList.menu.notAway": "Set yourself as active",
"app.userList.menu.chat.label": "Start a private chat", "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.clearStatus.label": "Clear status",
"app.userList.menu.removeUser.label": "Remove user", "app.userList.menu.removeUser.label": "Remove user",
"app.userList.menu.removeConfirmation.label": "Remove user ({0})", "app.userList.menu.removeConfirmation.label": "Remove user ({0})",