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:
commit
b0c66caef9
@ -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 =>
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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])
|
||||
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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!")
|
||||
|
@ -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";
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -30,6 +30,7 @@ public interface IBbbWebApiGWApp {
|
||||
Integer endWhenNoModeratorDelayInMinutes,
|
||||
Boolean muteOnStart,
|
||||
Boolean allowModsToUnmuteUsers,
|
||||
Boolean allowModsToEjectCameras,
|
||||
Boolean keepEvents,
|
||||
BreakoutRoomsParams breakoutParams,
|
||||
LockSettingsParams lockSettingsParams,
|
||||
|
@ -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,
|
||||
|
@ -61,6 +61,7 @@ export default function addMeeting(meeting) {
|
||||
authenticatedGuest: Boolean,
|
||||
maxUsers: Number,
|
||||
allowModsToUnmuteUsers: Boolean,
|
||||
allowModsToEjectCameras: Boolean,
|
||||
meetingLayout: String,
|
||||
},
|
||||
durationProps: {
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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}`);
|
||||
}
|
||||
}
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user