Merge pull request #13915 from prlanzarin/u24-sprawling-pier

feat(webcams): add option to allow moderators to close another user's webcams
This commit is contained in:
Anton Georgiev 2021-12-15 09:24:11 -05:00 committed by GitHub
commit b0c66caef9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 173 additions and 5 deletions

View File

@ -143,6 +143,8 @@ class ReceivedJsonMsgHandlerActor(
routeGenericMsg[CamStreamUnsubscribedInSfuEvtMsg](envelope, jsonNode)
case CamBroadcastStoppedInSfuEvtMsg.NAME =>
routeGenericMsg[CamBroadcastStoppedInSfuEvtMsg](envelope, jsonNode)
case EjectUserCamerasCmdMsg.NAME =>
routeGenericMsg[EjectUserCamerasCmdMsg](envelope, jsonNode)
// Voice
case RecordingStartedVoiceConfEvtMsg.NAME =>

View File

@ -88,6 +88,7 @@ class MeetingActor(
with CamStreamSubscribedInSfuEvtMsgHdlr
with CamStreamUnsubscribedInSfuEvtMsgHdlr
with CamBroadcastStoppedInSfuEvtMsgHdlr
with EjectUserCamerasCmdMsgHdlr
with EjectUserFromVoiceCmdMsgHdlr
with EndMeetingSysCmdMsgHdlr
@ -388,6 +389,7 @@ class MeetingActor(
case m: CamStreamSubscribedInSfuEvtMsg => handleCamStreamSubscribedInSfuEvtMsg(m)
case m: CamStreamUnsubscribedInSfuEvtMsg => handleCamStreamUnsubscribedInSfuEvtMsg(m)
case m: CamBroadcastStoppedInSfuEvtMsg => handleCamBroadcastStoppedInSfuEvtMsg(m)
case m: EjectUserCamerasCmdMsg => handleEjectUserCamerasCmdMsg(m)
case m: UserJoinedVoiceConfEvtMsg => handleUserJoinedVoiceConfEvtMsg(m)
case m: LogoutAndEndMeetingCmdMsg => usersApp.handleLogoutAndEndMeetingCmdMsg(m, state)

View File

@ -0,0 +1,50 @@
package org.bigbluebutton.core2.message.handlers
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.models.Webcams
import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter }
import org.bigbluebutton.core.apps.PermissionCheck
import org.bigbluebutton.core2.message.senders.MsgBuilder
trait EjectUserCamerasCmdMsgHdlr {
this: MeetingActor =>
val outGW: OutMsgRouter
def handleEjectUserCamerasCmdMsg(msg: EjectUserCamerasCmdMsg): Unit = {
val requesterUserId = msg.header.userId
val meetingId = liveMeeting.props.meetingProp.intId
val userToEject = msg.body.userId
val ejectionDisabled = !liveMeeting.props.usersProp.allowModsToEjectCameras
val isBreakout = liveMeeting.props.meetingProp.isBreakout
val badPermission = permissionFailed(
PermissionCheck.MOD_LEVEL,
PermissionCheck.VIEWER_LEVEL,
liveMeeting.users2x,
requesterUserId
)
if (ejectionDisabled || isBreakout || badPermission) {
val reason = "No permission to eject cameras from user."
PermissionCheck.ejectUserForFailedPermission(
meetingId,
requesterUserId,
reason,
outGW,
liveMeeting
)
} else {
log.info("Ejecting user cameras. meetingId=" + meetingId
+ " userId=" + userToEject
+ " requesterUserId=" + requesterUserId)
val broadcastedWebcams = Webcams.findWebcamsForUser(liveMeeting.webcams, userToEject)
broadcastedWebcams foreach { webcam =>
// Goes to SFU and comes back through CamBroadcastStoppedInSfuEvtMsg
val event = MsgBuilder.buildCamBroadcastStopSysMsg(
meetingId, userToEject, webcam.stream.id
)
outGW.send(event)
}
}
}
}

View File

@ -40,6 +40,7 @@ trait AppsTestFixtures {
val maxUsers = 25
val guestPolicy = "ALWAYS_ASK"
val allowModsToUnmuteUsers = false
val allowModsToEjectCameras = false
val authenticatedGuest = false
val red5DeskShareIPTestFixture = "127.0.0.1"
@ -60,7 +61,7 @@ trait AppsTestFixtures {
modOnlyMessage = modOnlyMessage)
val voiceProp = VoiceProp(telVoice = voiceConfId, voiceConf = voiceConfId, dialNumber = dialNumber, muteOnStart = muteOnStart)
val usersProp = UsersProp(maxUsers = maxUsers, webcamsOnlyForModerator = webcamsOnlyForModerator,
guestPolicy = guestPolicy, allowModsToUnmuteUsers = allowModsToUnmuteUsers, authenticatedGuest = authenticatedGuest)
guestPolicy = guestPolicy, allowModsToUnmuteUsers = allowModsToUnmuteUsers, allowModsToEjectCameras = allowModsToEjectCameras, authenticatedGuest = authenticatedGuest)
val metadataProp = new MetadataProp(metadata)
val defaultProps = DefaultProps(meetingProp, breakoutProps, durationProps, password, recordProp, welcomeProp, voiceProp,

View File

@ -28,7 +28,7 @@ case class WelcomeProp(welcomeMsgTemplate: String, welcomeMsg: String, modOnlyMe
case class VoiceProp(telVoice: String, voiceConf: String, dialNumber: String, muteOnStart: Boolean)
case class UsersProp(maxUsers: Int, webcamsOnlyForModerator: Boolean, guestPolicy: String, meetingLayout: String, allowModsToUnmuteUsers: Boolean, authenticatedGuest: Boolean)
case class UsersProp(maxUsers: Int, webcamsOnlyForModerator: Boolean, guestPolicy: String, meetingLayout: String, allowModsToUnmuteUsers: Boolean, allowModsToEjectCameras: Boolean, authenticatedGuest: Boolean)
case class MetadataProp(metadata: collection.immutable.Map[String, String])

View File

@ -118,6 +118,16 @@ case class EjectUserFromSfuSysMsg(
) extends BbbCoreMsg
case class EjectUserFromSfuSysMsgBody(userId: String)
/**
* Sent by the client to eject all cameras from user #userId
*/
object EjectUserCamerasCmdMsg { val NAME = "EjectUserCamerasCmdMsg" }
case class EjectUserCamerasCmdMsg(
header: BbbClientMsgHeader,
body: EjectUserCamerasCmdMsgBody
) extends StandardMsg
case class EjectUserCamerasCmdMsgBody(userId: String)
/**
* Sent to bbb-webrtc-sfu to tear down broadcaster stream #streamId
*/

View File

@ -36,6 +36,7 @@ trait TestFixtures {
val maxUsers = 25
val muteOnStart = false
val allowModsToUnmuteUsers = false
val allowModsToEjectCameras = false
val keepEvents = false
val guestPolicy = "ALWAYS_ASK"
val authenticatedGuest = false
@ -55,7 +56,7 @@ trait TestFixtures {
modOnlyMessage = modOnlyMessage)
val voiceProp = VoiceProp(telVoice = voiceConfId, voiceConf = voiceConfId, dialNumber = dialNumber, muteOnStart = muteOnStart)
val usersProp = UsersProp(maxUsers = maxUsers, webcamsOnlyForModerator = webcamsOnlyForModerator,
guestPolicy = guestPolicy, allowModsToUnmuteUsers = allowModsToUnmuteUsers, authenticatedGuest = authenticatedGuest)
guestPolicy = guestPolicy, allowModsToUnmuteUsers = allowModsToUnmuteUsers, allowModsToEjectCameras = allowModsToEjectCameras, authenticatedGuest = authenticatedGuest)
val metadataProp = new MetadataProp(metadata)
val screenshareProps = ScreenshareProps(screenshareConf = "FixMe!", red5ScreenshareIp = "fixMe!",
red5ScreenshareApp = "fixMe!")

View File

@ -46,6 +46,7 @@ public class ApiParams {
public static final String MUTE_ON_START = "muteOnStart";
public static final String MEETING_KEEP_EVENTS = "meetingKeepEvents";
public static final String ALLOW_MODS_TO_UNMUTE_USERS = "allowModsToUnmuteUsers";
public static final String ALLOW_MODS_TO_EJECT_CAMERAS = "allowModsToEjectCameras";
public static final String NAME = "name";
public static final String PARENT_MEETING_ID = "parentMeetingID";
public static final String PASSWORD = "password";

View File

@ -416,7 +416,7 @@ public class MeetingService implements MessageListener {
m.getMeetingExpireIfNoUserJoinedInMinutes(), m.getmeetingExpireWhenLastUserLeftInMinutes(),
m.getUserInactivityInspectTimerInMinutes(), m.getUserInactivityThresholdInMinutes(),
m.getUserActivitySignResponseDelayInMinutes(), m.getEndWhenNoModerator(), m.getEndWhenNoModeratorDelayInMinutes(),
m.getMuteOnStart(), m.getAllowModsToUnmuteUsers(), m.getMeetingKeepEvents(),
m.getMuteOnStart(), m.getAllowModsToUnmuteUsers(), m.getAllowModsToEjectCameras(), m.getMeetingKeepEvents(),
m.breakoutRoomsParams,
m.lockSettingsParams, m.getHtml5InstanceId());
}

View File

@ -87,6 +87,7 @@ public class ParamsProcessorUtil {
private boolean webcamsOnlyForModerator;
private boolean defaultMuteOnStart = false;
private boolean defaultAllowModsToUnmuteUsers = false;
private boolean defaultAllowModsToEjectCameras = false;
private boolean defaultKeepEvents = false;
private Boolean useDefaultLogo;
private String defaultLogoURL;
@ -623,6 +624,12 @@ public class ParamsProcessorUtil {
}
meeting.setAllowModsToUnmuteUsers(allowModsToUnmuteUsers);
Boolean allowModsToEjectCameras = defaultAllowModsToEjectCameras;
if (!StringUtils.isEmpty(params.get(ApiParams.ALLOW_MODS_TO_EJECT_CAMERAS))) {
allowModsToEjectCameras = Boolean.parseBoolean(params.get(ApiParams.ALLOW_MODS_TO_EJECT_CAMERAS));
}
meeting.setAllowModsToEjectCameras(allowModsToEjectCameras);
return meeting;
}
@ -1074,6 +1081,14 @@ public class ParamsProcessorUtil {
return defaultAllowModsToUnmuteUsers;
}
public void setAllowModsToEjectCameras(Boolean value) {
defaultAllowModsToEjectCameras = value;
}
public Boolean getAllowModsToEjectCameras() {
return defaultAllowModsToEjectCameras;
}
public List<String> decodeIds(String encodeid) {
ArrayList<String> ids=new ArrayList<>();
try {

View File

@ -86,6 +86,7 @@ public class Meeting {
private String customCopyright = "";
private Boolean muteOnStart = false;
private Boolean allowModsToUnmuteUsers = false;
private Boolean allowModsToEjectCameras = false;
private Boolean meetingKeepEvents;
private Integer meetingExpireIfNoUserJoinedInMinutes = 5;
@ -514,6 +515,14 @@ public class Meeting {
return allowModsToUnmuteUsers;
}
public void setAllowModsToEjectCameras(Boolean value) {
allowModsToEjectCameras = value;
}
public Boolean getAllowModsToEjectCameras() {
return allowModsToEjectCameras;
}
public void userJoined(User user) {
User u = getUserById(user.getInternalUserId());
if (u != null) {

View File

@ -30,6 +30,7 @@ public interface IBbbWebApiGWApp {
Integer endWhenNoModeratorDelayInMinutes,
Boolean muteOnStart,
Boolean allowModsToUnmuteUsers,
Boolean allowModsToEjectCameras,
Boolean keepEvents,
BreakoutRoomsParams breakoutParams,
LockSettingsParams lockSettingsParams,

View File

@ -140,6 +140,7 @@ class BbbWebApiGWApp(
endWhenNoModeratorDelayInMinutes: java.lang.Integer,
muteOnStart: java.lang.Boolean,
allowModsToUnmuteUsers: java.lang.Boolean,
allowModsToEjectCameras: java.lang.Boolean,
keepEvents: java.lang.Boolean,
breakoutParams: BreakoutRoomsParams,
lockSettingsParams: LockSettingsParams,
@ -177,7 +178,9 @@ class BbbWebApiGWApp(
modOnlyMessage = modOnlyMessage)
val voiceProp = VoiceProp(telVoice = voiceBridge, voiceConf = voiceBridge, dialNumber = dialNumber, muteOnStart = muteOnStart.booleanValue())
val usersProp = UsersProp(maxUsers = maxUsers.intValue(), webcamsOnlyForModerator = webcamsOnlyForModerator.booleanValue(),
guestPolicy = guestPolicy, meetingLayout = meetingLayout, allowModsToUnmuteUsers = allowModsToUnmuteUsers.booleanValue(), authenticatedGuest = authenticatedGuest.booleanValue())
guestPolicy = guestPolicy, meetingLayout = meetingLayout, allowModsToUnmuteUsers = allowModsToUnmuteUsers.booleanValue(),
allowModsToEjectCameras = allowModsToEjectCameras.booleanValue(),
authenticatedGuest = authenticatedGuest.booleanValue())
val metadataProp = MetadataProp(mapAsScalaMap(metadata).toMap)
val screenshareProps = ScreenshareProps(
screenshareConf = voiceBridge + screenshareConfSuffix,

View File

@ -61,6 +61,7 @@ export default function addMeeting(meeting) {
authenticatedGuest: Boolean,
maxUsers: Number,
allowModsToUnmuteUsers: Boolean,
allowModsToEjectCameras: Boolean,
meetingLayout: String,
},
durationProps: {

View File

@ -1,8 +1,10 @@
import { Meteor } from 'meteor/meteor';
import userShareWebcam from './methods/userShareWebcam';
import userUnshareWebcam from './methods/userUnshareWebcam';
import ejectUserCameras from './methods/ejectUserCameras';
Meteor.methods({
userShareWebcam,
userUnshareWebcam,
ejectUserCameras,
});

View File

@ -0,0 +1,28 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import RedisPubSub from '/imports/startup/server/redis';
import { extractCredentials } from '/imports/api/common/server/helpers';
export default function ejectUserCameras(userId) {
try {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'EjectUserCamerasCmdMsg';
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(userId, String);
Logger.info(`Requesting ejection of cameras: userToEject=${userId} requesterUserId=${requesterUserId}`);
const payload = {
userId,
};
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
} catch (err) {
Logger.error(`Exception while invoking method ejectUserCameras ${err.stack}`);
}
}

View File

@ -325,6 +325,7 @@ const getUsersProp = () => {
{
fields: {
'usersProp.allowModsToUnmuteUsers': 1,
'usersProp.allowModsToEjectCameras': 1,
'usersProp.authenticatedGuest': 1,
},
},
@ -334,6 +335,7 @@ const getUsersProp = () => {
return {
allowModsToUnmuteUsers: false,
allowModsToEjectCameras: false,
authenticatedGuest: false,
};
};
@ -405,6 +407,10 @@ const getAvailableActions = (
const allowedToChangeWhiteboardAccess = amIPresenter
&& !amISubjectUser;
const allowedToEjectCameras = amIModerator
&& !amISubjectUser
&& usersProp.allowModsToEjectCameras;
return {
allowedToChatPrivately,
allowedToMuteAudio,
@ -417,6 +423,7 @@ const getAvailableActions = (
allowedToChangeStatus,
allowedToChangeUserLockStatus,
allowedToChangeWhiteboardAccess,
allowedToEjectCameras,
};
};
@ -457,6 +464,10 @@ const toggleVoice = (userId) => {
}
};
const ejectUserCameras = (userId) => {
makeCall('ejectUserCameras', userId);
};
const getEmoji = () => {
const currentUser = Users.findOne({ userId: Auth.userID },
{ fields: { emoji: 1 } });
@ -670,4 +681,5 @@ export default {
getUsersProp,
getUserCount,
sortUsersByCurrent,
ejectUserCameras,
};

View File

@ -52,6 +52,7 @@ class UserListItem extends PureComponent {
raiseHandPushAlert,
layoutContextDispatch,
isRTL,
ejectUserCameras,
} = this.props;
const contents = (
@ -90,6 +91,7 @@ class UserListItem extends PureComponent {
raiseHandPushAlert,
layoutContextDispatch,
isRTL,
ejectUserCameras,
}}
/>
);

View File

@ -33,6 +33,7 @@ export default withTracker(({ user }) => {
removeUser: UserListService.removeUser,
toggleUserLock: UserListService.toggleUserLock,
changeRole: UserListService.changeRole,
ejectUserCameras: UserListService.ejectUserCameras,
assignPresenter: UserListService.assignPresenter,
getAvailableActions: UserListService.getAvailableActions,
normalizeEmojiName: UserListService.normalizeEmojiName,

View File

@ -69,6 +69,10 @@ const messages = defineMessages({
id: 'app.userList.menu.removeWhiteboardAccess.label',
description: 'label to remove user whiteboard access',
},
ejectUserCamerasLabel: {
id: 'app.userList.menu.ejectUserCameras.label',
description: 'label to eject user cameras',
},
RemoveUserLabel: {
id: 'app.userList.menu.removeUser.label',
description: 'Forcefully remove this user from the meeting',
@ -231,6 +235,7 @@ class UserDropdown extends PureComponent {
removeUser,
toggleVoice,
changeRole,
ejectUserCameras,
lockSettingsProps,
hasPrivateChatBetweenUsers,
toggleUserLock,
@ -266,6 +271,7 @@ class UserDropdown extends PureComponent {
allowedToChangeStatus,
allowedToChangeUserLockStatus,
allowedToChangeWhiteboardAccess,
allowedToEjectCameras,
} = actionPermissions;
const { disablePrivateChat } = lockSettingsProps;
@ -483,6 +489,22 @@ class UserDropdown extends PureComponent {
});
}
if (allowedToEjectCameras
&& user.isSharingWebcam
&& isMeteorConnected
&& !meetingIsBreakout
) {
actions.push({
key: 'ejectUserCameras',
label: intl.formatMessage(messages.ejectUserCamerasLabel),
onClick: () => {
this.onActionsHide(ejectUserCameras(user.userId));
this.handleClose();
},
icon: 'video_off',
});
}
return actions;
}

View File

@ -90,6 +90,7 @@
"app.userList.menu.unmuteUserAudio.label": "Unmute user",
"app.userList.menu.giveWhiteboardAccess.label" : "Give whiteboard access",
"app.userList.menu.removeWhiteboardAccess.label": "Remove whiteboard access",
"app.userList.menu.ejectUserCameras.label": "Close cameras",
"app.userList.userAriaLabel": "{0} {1} {2} Status {3}",
"app.userList.menu.promoteUser.label": "Promote to moderator",
"app.userList.menu.demoteUser.label": "Demote to viewer",

View File

@ -262,6 +262,10 @@ muteOnStart=false
# Gives moderators permisson to unmute other users
allowModsToUnmuteUsers=false
# Eject user webcams
# Gives moderators permisson to close other users' webcams
allowModsToEjectCameras=false
# Saves meeting events even if the meeting is not recorded
defaultKeepEvents=false