diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/SystemConfiguration.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/SystemConfiguration.scala index 9067f3b73c..78efc0bf0a 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/SystemConfiguration.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/SystemConfiguration.scala @@ -49,7 +49,6 @@ trait SystemConfiguration { lazy val endMeetingWhenNoMoreAuthedUsers = Try(config.getBoolean("apps.endMeetingWhenNoMoreAuthedUsers")).getOrElse(false) lazy val endMeetingWhenNoMoreAuthedUsersAfterMinutes = Try(config.getInt("apps.endMeetingWhenNoMoreAuthedUsersAfterMinutes")).getOrElse(2) - lazy val multiUserWhiteboardDefault = Try(config.getBoolean("whiteboard.multiUserDefault")).getOrElse(false) // Redis server configuration lazy val redisHost = Try(config.getString("redis.host")).getOrElse("127.0.0.1") diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonActor.scala index 8963b7057b..4bab7a2477 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonActor.scala @@ -135,10 +135,6 @@ class BigBlueButtonActor( RunningMeetings.add(meetings, m) - // Send new 2x message - val msgEvent = MsgBuilder.buildMeetingCreatedEvtMsg(m.props.meetingProp.intId, msg.body.props) - m.outMsgRouter.send(msgEvent) - case Some(m) => log.info("Meeting already created. meetingID={}", msg.body.props.meetingProp.intId) // do nothing diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/ScreenshareModel.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/ScreenshareModel.scala index cae06abb02..dcf6934b67 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/ScreenshareModel.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/ScreenshareModel.scala @@ -10,6 +10,7 @@ object ScreenshareModel { status.voiceConf = "" status.screenshareConf = "" status.timestamp = "" + status.hasAudio = false } def getScreenshareStarted(status: ScreenshareModel): Boolean = { @@ -79,6 +80,14 @@ object ScreenshareModel { def getTimestamp(status: ScreenshareModel): String = { status.timestamp } + + def setHasAudio(status: ScreenshareModel, hasAudio: Boolean): Unit = { + status.hasAudio = hasAudio + } + + def getHasAudio(status: ScreenshareModel): Boolean = { + status.hasAudio + } } class ScreenshareModel { @@ -90,4 +99,5 @@ class ScreenshareModel { private var voiceConf: String = "" private var screenshareConf: String = "" private var timestamp: String = "" + private var hasAudio = false } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/WhiteboardModel.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/WhiteboardModel.scala index a8a45901fb..486041bc1d 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/WhiteboardModel.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/WhiteboardModel.scala @@ -25,7 +25,14 @@ class WhiteboardModel extends SystemConfiguration { } private def createWhiteboard(wbId: String): Whiteboard = { - new Whiteboard(wbId, multiUserWhiteboardDefault, System.currentTimeMillis(), 0, new HashMap[String, List[AnnotationVO]]()) + new Whiteboard( + wbId, + Array.empty[String], + Array.empty[String], + System.currentTimeMillis(), + 0, + new HashMap[String, List[AnnotationVO]]() + ) } private def getAnnotationsByUserId(wb: Whiteboard, id: String): List[AnnotationVO] = { @@ -184,7 +191,7 @@ class WhiteboardModel extends SystemConfiguration { if (hasWhiteboard(wbId)) { val wb = getWhiteboard(wbId) - if (wb.multiUser) { + if (wb.multiUser.contains(userId)) { if (wb.annotationsMap.contains(userId)) { val newWb = wb.copy(annotationsMap = wb.annotationsMap - userId) saveWhiteboard(newWb) @@ -205,7 +212,7 @@ class WhiteboardModel extends SystemConfiguration { var last: Option[AnnotationVO] = None val wb = getWhiteboard(wbId) - if (wb.multiUser) { + if (wb.multiUser.contains(userId)) { val usersAnnotations = getAnnotationsByUserId(wb, userId) //not empty and head id equals annotation id @@ -234,13 +241,21 @@ class WhiteboardModel extends SystemConfiguration { wb.copy(annotationsMap = newAnnotationsMap) } - def modifyWhiteboardAccess(wbId: String, multiUser: Boolean) { + def modifyWhiteboardAccess(wbId: String, multiUser: Array[String]) { val wb = getWhiteboard(wbId) - val newWb = wb.copy(multiUser = multiUser, changedModeOn = System.currentTimeMillis()) + val newWb = wb.copy(multiUser = multiUser, oldMultiUser = wb.multiUser, changedModeOn = System.currentTimeMillis()) saveWhiteboard(newWb) } - def getWhiteboardAccess(wbId: String): Boolean = getWhiteboard(wbId).multiUser + def getWhiteboardAccess(wbId: String): Array[String] = getWhiteboard(wbId).multiUser + + def hasWhiteboardAccess(wbId: String, userId: String): Boolean = { + val wb = getWhiteboard(wbId) + wb.multiUser.contains(userId) || { + val lastChange = System.currentTimeMillis() - wb.changedModeOn + wb.oldMultiUser.contains(userId) && lastChange < 5000 + } + } def getChangedModeOn(wbId: String): Long = getWhiteboard(wbId).changedModeOn diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/GetScreenshareStatusReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/GetScreenshareStatusReqMsgHdlr.scala index fbea876750..1edea1b84c 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/GetScreenshareStatusReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/GetScreenshareStatusReqMsgHdlr.scala @@ -25,9 +25,10 @@ trait GetScreenshareStatusReqMsgHdlr { val vidWidth = ScreenshareModel.getScreenshareVideoWidth(liveMeeting.screenshareModel) val vidHeight = ScreenshareModel.getScreenshareVideoHeight(liveMeeting.screenshareModel) val timestamp = ScreenshareModel.getTimestamp(liveMeeting.screenshareModel) + val hasAudio = ScreenshareModel.getHasAudio(liveMeeting.screenshareModel) val body = ScreenshareRtmpBroadcastStartedEvtMsgBody(voiceConf, screenshareConf, - stream, vidWidth, vidHeight, timestamp) + stream, vidWidth, vidHeight, timestamp, hasAudio) val event = ScreenshareRtmpBroadcastStartedEvtMsg(header, body) BbbCommonEnvCoreMsg(envelope, event) } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr.scala index e72788eb6b..60ee455a0f 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr.scala @@ -10,7 +10,7 @@ trait ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr { def handle(msg: ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { def broadcastEvent(voiceConf: String, screenshareConf: String, stream: String, vidWidth: Int, vidHeight: Int, - timestamp: String): BbbCommonEnvCoreMsg = { + timestamp: String, hasAudio: Boolean): BbbCommonEnvCoreMsg = { val routing = Routing.addMsgToClientRouting( MessageTypes.BROADCAST_TO_MEETING, @@ -23,7 +23,7 @@ trait ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr { ) val body = ScreenshareRtmpBroadcastStartedEvtMsgBody(voiceConf, screenshareConf, - stream, vidWidth, vidHeight, timestamp) + stream, vidWidth, vidHeight, timestamp, hasAudio) val event = ScreenshareRtmpBroadcastStartedEvtMsg(header, body) BbbCommonEnvCoreMsg(envelope, event) } @@ -41,12 +41,13 @@ trait ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr { ScreenshareModel.setVoiceConf(liveMeeting.screenshareModel, msg.body.voiceConf) ScreenshareModel.setScreenshareConf(liveMeeting.screenshareModel, msg.body.screenshareConf) ScreenshareModel.setTimestamp(liveMeeting.screenshareModel, msg.body.timestamp) + ScreenshareModel.setHasAudio(liveMeeting.screenshareModel, msg.body.hasAudio) log.info("START broadcast ALLOWED when isBroadcastingRTMP=false") // Notify viewers in the meeting that there's an rtmp stream to view val msgEvent = broadcastEvent(msg.body.voiceConf, msg.body.screenshareConf, msg.body.stream, - msg.body.vidWidth, msg.body.vidHeight, msg.body.timestamp) + msg.body.vidWidth, msg.body.vidHeight, msg.body.timestamp, msg.body.hasAudio) bus.outGW.send(msgEvent) } else { log.info("START broadcast NOT ALLOWED when isBroadcastingRTMP=true") diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/ClearWhiteboardPubMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/ClearWhiteboardPubMsgHdlr.scala index b5d2d686a0..d1b210498a 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/ClearWhiteboardPubMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/ClearWhiteboardPubMsgHdlr.scala @@ -21,7 +21,7 @@ trait ClearWhiteboardPubMsgHdlr extends RightsManagementTrait { bus.outGW.send(msgEvent) } - if (filterWhiteboardMessage(msg.body.whiteboardId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) { + if (filterWhiteboardMessage(msg.body.whiteboardId, msg.header.userId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) { val meetingId = liveMeeting.props.meetingProp.intId val reason = "No permission to clear the whiteboard." PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/GetWhiteboardAnnotationsReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/GetWhiteboardAnnotationsReqMsgHdlr.scala index c3f35364d3..a7cbfe1d64 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/GetWhiteboardAnnotationsReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/GetWhiteboardAnnotationsReqMsgHdlr.scala @@ -9,7 +9,7 @@ trait GetWhiteboardAnnotationsReqMsgHdlr { def handle(msg: GetWhiteboardAnnotationsReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { - def broadcastEvent(msg: GetWhiteboardAnnotationsReqMsg, history: Array[AnnotationVO], multiUser: Boolean): Unit = { + def broadcastEvent(msg: GetWhiteboardAnnotationsReqMsg, history: Array[AnnotationVO], multiUser: Array[String]): Unit = { val routing = Routing.addMsgToHtml5InstanceIdRouting(liveMeeting.props.meetingProp.intId, liveMeeting.props.systemProps.html5InstanceId.toString) val envelope = BbbCoreEnvelope(GetWhiteboardAnnotationsRespMsg.NAME, routing) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/ModifyWhiteboardAccessPubMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/ModifyWhiteboardAccessPubMsgHdlr.scala index cd515b3197..f40382e5b9 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/ModifyWhiteboardAccessPubMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/ModifyWhiteboardAccessPubMsgHdlr.scala @@ -21,7 +21,7 @@ trait ModifyWhiteboardAccessPubMsgHdlr extends RightsManagementTrait { bus.outGW.send(msgEvent) } - if (filterWhiteboardMessage(msg.body.whiteboardId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) { + if (filterWhiteboardMessage(msg.body.whiteboardId, msg.header.userId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) { val meetingId = liveMeeting.props.meetingProp.intId val reason = "No permission to modify access to the whiteboard." PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/SendCursorPositionPubMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/SendCursorPositionPubMsgHdlr.scala index 0dee90a62f..98df4e9e99 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/SendCursorPositionPubMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/SendCursorPositionPubMsgHdlr.scala @@ -21,7 +21,7 @@ trait SendCursorPositionPubMsgHdlr extends RightsManagementTrait { bus.outGW.send(msgEvent) } - if (filterWhiteboardMessage(msg.body.whiteboardId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) { + if (filterWhiteboardMessage(msg.body.whiteboardId, msg.header.userId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) { val meetingId = liveMeeting.props.meetingProp.intId val reason = "No permission to send your cursor position." // Just drop messages as these might be delayed messages from multi-user whiteboard. Don't want to diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/SendWhiteboardAnnotationPubMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/SendWhiteboardAnnotationPubMsgHdlr.scala index aa6e4ff65a..10dd993eac 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/SendWhiteboardAnnotationPubMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/SendWhiteboardAnnotationPubMsgHdlr.scala @@ -71,7 +71,7 @@ trait SendWhiteboardAnnotationPubMsgHdlr extends RightsManagementTrait { WhiteboardKeyUtil.DRAW_UPDATE_STATUS == annotation.status) } - if (!excludedWbMsg(msg.body.annotation) && filterWhiteboardMessage(msg.body.annotation.wbId, liveMeeting) && permissionFailed( + if (!excludedWbMsg(msg.body.annotation) && filterWhiteboardMessage(msg.body.annotation.wbId, msg.header.userId, liveMeeting) && permissionFailed( PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId )) { diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/UndoWhiteboardPubMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/UndoWhiteboardPubMsgHdlr.scala index 03d0e663c8..8ccbf46f8c 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/UndoWhiteboardPubMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/UndoWhiteboardPubMsgHdlr.scala @@ -21,7 +21,7 @@ trait UndoWhiteboardPubMsgHdlr extends RightsManagementTrait { bus.outGW.send(msgEvent) } - if (filterWhiteboardMessage(msg.body.whiteboardId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) { + if (filterWhiteboardMessage(msg.body.whiteboardId, msg.header.userId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) { val meetingId = liveMeeting.props.meetingProp.intId val reason = "No permission to undo an annotation." PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/WhiteboardApp2x.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/WhiteboardApp2x.scala index b6acea18a2..0f90ed1c16 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/WhiteboardApp2x.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/WhiteboardApp2x.scala @@ -5,8 +5,16 @@ import akka.event.Logging import org.bigbluebutton.core.running.LiveMeeting import org.bigbluebutton.common2.msgs.AnnotationVO import org.bigbluebutton.core.apps.WhiteboardKeyUtil +import scala.collection.immutable.{ Map, List } -case class Whiteboard(id: String, multiUser: Boolean, changedModeOn: Long, annotationCount: Int, annotationsMap: scala.collection.immutable.Map[String, scala.collection.immutable.List[AnnotationVO]]) +case class Whiteboard( + id: String, + multiUser: Array[String], + oldMultiUser: Array[String], + changedModeOn: Long, + annotationCount: Int, + annotationsMap: Map[String, List[AnnotationVO]] +) class WhiteboardApp2x(implicit val context: ActorContext) extends SendCursorPositionPubMsgHdlr @@ -56,18 +64,18 @@ class WhiteboardApp2x(implicit val context: ActorContext) liveMeeting.wbModel.undoWhiteboard(whiteboardId, requesterId) } - def getWhiteboardAccess(whiteboardId: String, liveMeeting: LiveMeeting): Boolean = { + def getWhiteboardAccess(whiteboardId: String, liveMeeting: LiveMeeting): Array[String] = { liveMeeting.wbModel.getWhiteboardAccess(whiteboardId) } - def modifyWhiteboardAccess(whiteboardId: String, multiUser: Boolean, liveMeeting: LiveMeeting) { + def modifyWhiteboardAccess(whiteboardId: String, multiUser: Array[String], liveMeeting: LiveMeeting) { liveMeeting.wbModel.modifyWhiteboardAccess(whiteboardId, multiUser) } - def filterWhiteboardMessage(whiteboardId: String, liveMeeting: LiveMeeting): Boolean = { + def filterWhiteboardMessage(whiteboardId: String, userId: String, liveMeeting: LiveMeeting): Boolean = { // Need to check if the wb mode change from multi-user to single-user. Give 5sec allowance to // allow delayed messages to be handled as clients may have been sending messages while the wb // mode was changed. (ralam nov 22, 2017) - if (!liveMeeting.wbModel.getWhiteboardAccess(whiteboardId) && liveMeeting.wbModel.getChangedModeOn(whiteboardId) > 5000) true else false + !liveMeeting.wbModel.hasWhiteboardAccess(whiteboardId, userId) } } 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 066935415b..e72b3f4bf8 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 @@ -156,6 +156,10 @@ class MeetingActor( var lastRttTestSentOn = System.currentTimeMillis() + // Send new 2x message + val msgEvent = MsgBuilder.buildMeetingCreatedEvtMsg(liveMeeting.props.meetingProp.intId, liveMeeting.props) + outGW.send(msgEvent) + // Create a default public group chat state = groupChatApp.handleCreateDefaultPublicGroupChat(state, liveMeeting, msgBus) diff --git a/akka-bbb-apps/src/universal/conf/application.conf b/akka-bbb-apps/src/universal/conf/application.conf index fd3963cf82..03186f423b 100755 --- a/akka-bbb-apps/src/universal/conf/application.conf +++ b/akka-bbb-apps/src/universal/conf/application.conf @@ -95,8 +95,3 @@ recording { # set zero to disable chapter break chapterBreakLengthInMinutes = 0 } - -whiteboard { - multiUserDefault = false -} - diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/FreeswitchConferenceEventListener.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/FreeswitchConferenceEventListener.java index f9327ec6ad..0242b524e0 100755 --- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/FreeswitchConferenceEventListener.java +++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/FreeswitchConferenceEventListener.java @@ -83,7 +83,7 @@ public class FreeswitchConferenceEventListener implements ConferenceEventListene if (((ScreenshareRTMPBroadcastEvent) event).getBroadcast()) { ScreenshareRTMPBroadcastEvent evt = (ScreenshareRTMPBroadcastEvent) event; vcs.deskShareRTMPBroadcastStarted(evt.getRoom(), evt.getBroadcastingStreamUrl(), - evt.getVideoWidth(), evt.getVideoHeight(), evt.getTimestamp()); + evt.getVideoWidth(), evt.getVideoHeight(), evt.getTimestamp(), evt.getHasAudio()); } else { ScreenshareRTMPBroadcastEvent evt = (ScreenshareRTMPBroadcastEvent) event; vcs.deskShareRTMPBroadcastStopped(evt.getRoom(), evt.getBroadcastingStreamUrl(), diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/IVoiceConferenceService.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/IVoiceConferenceService.java index 77012629d2..c60c269843 100755 --- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/IVoiceConferenceService.java +++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/IVoiceConferenceService.java @@ -55,7 +55,8 @@ public interface IVoiceConferenceService { String streamname, Integer videoWidth, Integer videoHeight, - String timestamp); + String timestamp, + boolean hasAudio); void deskShareRTMPBroadcastStopped(String room, String streamname, diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/ScreenshareRTMPBroadcastEvent.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/ScreenshareRTMPBroadcastEvent.java index ea8c826888..814a64ff1d 100755 --- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/ScreenshareRTMPBroadcastEvent.java +++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/ScreenshareRTMPBroadcastEvent.java @@ -25,6 +25,7 @@ public class ScreenshareRTMPBroadcastEvent extends VoiceConferenceEvent { private String streamUrl; private Integer vw; private Integer vh; + private boolean hasAudio; private final String SCREENSHARE_SUFFIX = "-SCREENSHARE"; @@ -46,6 +47,10 @@ public class ScreenshareRTMPBroadcastEvent extends VoiceConferenceEvent { public void setVideoHeight(Integer vh) {this.vh = vh;} + public void setHasAudio(boolean hasAudio) { + this.hasAudio = hasAudio; + } + public Integer getVideoHeight() {return vh;} public Integer getVideoWidth() {return vw;} @@ -65,4 +70,8 @@ public class ScreenshareRTMPBroadcastEvent extends VoiceConferenceEvent { public boolean getBroadcast() { return broadcast; } + + public boolean getHasAudio() { + return hasAudio; + } } diff --git a/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/VoiceConferenceService.scala b/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/VoiceConferenceService.scala index 7d63c4f116..c6da363f8c 100755 --- a/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/VoiceConferenceService.scala +++ b/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/VoiceConferenceService.scala @@ -237,13 +237,14 @@ class VoiceConferenceService(healthz: HealthzService, streamname: String, vw: java.lang.Integer, vh: java.lang.Integer, - timestamp: String + timestamp: String, + hasAudio: Boolean ) { val header = BbbCoreVoiceConfHeader(ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg.NAME, voiceConfId) val body = ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgBody(voiceConf = voiceConfId, screenshareConf = voiceConfId, stream = streamname, vidWidth = vw.intValue(), vidHeight = vh.intValue(), - timestamp) + timestamp, hasAudio) val envelope = BbbCoreEnvelope(ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg.NAME, Map("voiceConf" -> voiceConfId)) val msg = new ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg(header, body) diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala index 9916ffddeb..a6d15a7ffa 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala @@ -24,7 +24,7 @@ case class ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg( extends VoiceStandardMsg case class ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgBody(voiceConf: String, screenshareConf: String, stream: String, vidWidth: Int, vidHeight: Int, - timestamp: String) + timestamp: String, hasAudio: Boolean) /** * Sent to clients to notify them of an RTMP stream starting. @@ -37,7 +37,7 @@ case class ScreenshareRtmpBroadcastStartedEvtMsg( extends BbbCoreMsg case class ScreenshareRtmpBroadcastStartedEvtMsgBody(voiceConf: String, screenshareConf: String, stream: String, vidWidth: Int, vidHeight: Int, - timestamp: String) + timestamp: String, hasAudio: Boolean) /** * Send by FS that RTMP stream has stopped. diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala index 707fa8e66f..7e79667bf3 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala @@ -18,7 +18,7 @@ case class GetWhiteboardAnnotationsReqMsgBody(whiteboardId: String) object ModifyWhiteboardAccessPubMsg { val NAME = "ModifyWhiteboardAccessPubMsg" } case class ModifyWhiteboardAccessPubMsg(header: BbbClientMsgHeader, body: ModifyWhiteboardAccessPubMsgBody) extends StandardMsg -case class ModifyWhiteboardAccessPubMsgBody(whiteboardId: String, multiUser: Boolean) +case class ModifyWhiteboardAccessPubMsgBody(whiteboardId: String, multiUser: Array[String]) object SendCursorPositionPubMsg { val NAME = "SendCursorPositionPubMsg" } case class SendCursorPositionPubMsg(header: BbbClientMsgHeader, body: SendCursorPositionPubMsgBody) extends StandardMsg @@ -48,11 +48,11 @@ case class ClearWhiteboardEvtMsgBody(whiteboardId: String, userId: String, fullC object GetWhiteboardAnnotationsRespMsg { val NAME = "GetWhiteboardAnnotationsRespMsg" } case class GetWhiteboardAnnotationsRespMsg(header: BbbClientMsgHeader, body: GetWhiteboardAnnotationsRespMsgBody) extends BbbCoreMsg -case class GetWhiteboardAnnotationsRespMsgBody(whiteboardId: String, annotations: Array[AnnotationVO], multiUser: Boolean) +case class GetWhiteboardAnnotationsRespMsgBody(whiteboardId: String, annotations: Array[AnnotationVO], multiUser: Array[String]) object ModifyWhiteboardAccessEvtMsg { val NAME = "ModifyWhiteboardAccessEvtMsg" } case class ModifyWhiteboardAccessEvtMsg(header: BbbClientMsgHeader, body: ModifyWhiteboardAccessEvtMsgBody) extends BbbCoreMsg -case class ModifyWhiteboardAccessEvtMsgBody(whiteboardId: String, multiUser: Boolean) +case class ModifyWhiteboardAccessEvtMsgBody(whiteboardId: String, multiUser: Array[String]) object SendCursorPositionEvtMsg { val NAME = "SendCursorPositionEvtMsg" } case class SendCursorPositionEvtMsg(header: BbbClientMsgHeader, body: SendCursorPositionEvtMsgBody) extends BbbCoreMsg diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java index 94e835588a..40b81a2828 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java @@ -43,6 +43,7 @@ public class ApiParams { public static final String MODERATOR_ONLY_MESSAGE = "moderatorOnlyMessage"; public static final String MODERATOR_PW = "moderatorPW"; 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 NAME = "name"; public static final String PARENT_MEETING_ID = "parentMeetingID"; diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java index fad6f60344..bf495f2b8d 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java @@ -120,7 +120,6 @@ public class MeetingService implements MessageListener { private RedisStorageService storeService; private CallbackUrlService callbackUrlService; private HTML5LoadBalancingService html5LoadBalancingService; - private boolean keepEvents; private long usersTimeout; private long enteredUsersTimeout; @@ -356,7 +355,7 @@ public class MeetingService implements MessageListener { } private boolean storeEvents(Meeting m) { - return m.isRecord() || keepEvents; + return m.isRecord() || m.getMeetingKeepEvents(); } private void handleCreateMeeting(Meeting m) { @@ -404,6 +403,8 @@ public class MeetingService implements MessageListener { logData.put("logCode", "create_meeting"); logData.put("description", "Create meeting."); + logData.put("meetingKeepEvents", m.getMeetingKeepEvents()); + Gson gson = new Gson(); String logStr = gson.toJson(logData); @@ -417,7 +418,7 @@ public class MeetingService implements MessageListener { m.getDialNumber(), m.getMaxUsers(), m.getMeetingExpireIfNoUserJoinedInMinutes(), m.getmeetingExpireWhenLastUserLeftInMinutes(), m.getUserInactivityInspectTimerInMinutes(), m.getUserInactivityThresholdInMinutes(), - m.getUserActivitySignResponseDelayInMinutes(), m.getMuteOnStart(), m.getAllowModsToUnmuteUsers(), keepEvents, + m.getUserActivitySignResponseDelayInMinutes(), m.getMuteOnStart(), m.getAllowModsToUnmuteUsers(), m.getMeetingKeepEvents(), m.breakoutRoomsParams, m.lockSettingsParams, m.getHtml5InstanceId()); } @@ -697,7 +698,7 @@ public class MeetingService implements MessageListener { if (m != null) { m.setForciblyEnded(true); processRecording(m); - if (keepEvents) { + if (m.getMeetingKeepEvents()) { // The creation of the ended tag must occur after the creation of the // recorded tag to avoid concurrency issues at the recording scripts recordingService.markAsEnded(m.getInternalId()); @@ -1233,10 +1234,6 @@ public class MeetingService implements MessageListener { stunTurnService = s; } - public void setKeepEvents(boolean value) { - keepEvents = value; - } - public void setUsersTimeout(long value) { usersTimeout = value; } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java index 172265ded7..741d4099e0 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java @@ -87,6 +87,7 @@ public class ParamsProcessorUtil { private boolean webcamsOnlyForModerator; private boolean defaultMuteOnStart = false; private boolean defaultAllowModsToUnmuteUsers = false; + private boolean defaultKeepEvents = false; private boolean defaultBreakoutRoomsEnabled; private boolean defaultBreakoutRoomsRecord; @@ -544,6 +545,12 @@ public class ParamsProcessorUtil { meeting.setMuteOnStart(muteOnStart); + Boolean meetingKeepEvents = defaultKeepEvents; + if (!StringUtils.isEmpty(params.get(ApiParams.MEETING_KEEP_EVENTS))) { + meetingKeepEvents = Boolean.parseBoolean(params.get(ApiParams.MEETING_KEEP_EVENTS)); + } + meeting.setMeetingKeepEvents(meetingKeepEvents); + Boolean allowModsToUnmuteUsers = defaultAllowModsToUnmuteUsers; if (!StringUtils.isEmpty(params.get(ApiParams.ALLOW_MODS_TO_UNMUTE_USERS))) { allowModsToUnmuteUsers = Boolean.parseBoolean(params.get(ApiParams.ALLOW_MODS_TO_UNMUTE_USERS)); @@ -1026,6 +1033,10 @@ public class ParamsProcessorUtil { return defaultMuteOnStart; } + public void setDefaultKeepEvents(Boolean mke) { + defaultKeepEvents = mke; + } + public void setAllowModsToUnmuteUsers(Boolean value) { defaultAllowModsToUnmuteUsers = value; } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java index e2884db344..b17b07c7fb 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java @@ -84,6 +84,7 @@ public class Meeting { private String customCopyright = ""; private Boolean muteOnStart = false; private Boolean allowModsToUnmuteUsers = false; + private Boolean meetingKeepEvents; private Integer meetingExpireIfNoUserJoinedInMinutes = 5; private Integer meetingExpireWhenLastUserLeftInMinutes = 1; @@ -503,6 +504,14 @@ public class Meeting { return muteOnStart; } + public void setMeetingKeepEvents(Boolean mke) { + meetingKeepEvents = mke; + } + + public Boolean getMeetingKeepEvents() { + return meetingKeepEvents; + } + public void setAllowModsToUnmuteUsers(Boolean value) { allowModsToUnmuteUsers = value; } diff --git a/bigbluebutton-config/bigbluebutton-release b/bigbluebutton-config/bigbluebutton-release index f4dbef9194..dbabfda03e 100644 --- a/bigbluebutton-config/bigbluebutton-release +++ b/bigbluebutton-config/bigbluebutton-release @@ -1,2 +1,2 @@ -BIGBLUEBUTTON_RELEASE=2.3.0-alpha8 +BIGBLUEBUTTON_RELEASE=2.3.0-beta-1 diff --git a/bigbluebutton-config/bin/apply-lib.sh b/bigbluebutton-config/bin/apply-lib.sh index 605248b3a9..6cfb2cb427 100644 --- a/bigbluebutton-config/bin/apply-lib.sh +++ b/bigbluebutton-config/bin/apply-lib.sh @@ -23,15 +23,17 @@ else SERVLET_DIR=/var/lib/tomcat7/webapps/bigbluebutton fi +BBB_WEB_ETC_CONFIG=/etc/bigbluebutton/bbb-web.properties + PROTOCOL=http if [ -f $SERVLET_DIR/WEB-INF/classes/bigbluebutton.properties ]; then - SERVER_URL=$(cat $SERVLET_DIR/WEB-INF/classes/bigbluebutton.properties | sed -n '/^bigbluebutton.web.serverURL/{s/.*\///;p}') - if cat $SERVLET_DIR/WEB-INF/classes/bigbluebutton.properties | grep bigbluebutton.web.serverURL | grep -q https; then + SERVER_URL=$(cat $SERVLET_DIR/WEB-INF/classes/bigbluebutton.properties $BBB_WEB_ETC_CONFIG | grep -v '#' | sed -n '/^bigbluebutton.web.serverURL/{s/.*\///;p}' | tail -n 1) + if cat $SERVLET_DIR/WEB-INF/classes/bigbluebutton.properties $BBB_WEB_ETC_CONFIG | grep -v '#' | grep ^bigbluebutton.web.serverURL | tail -n 1 | grep -q https; then PROTOCOL=https fi fi -HOST=$(cat $SERVLET_DIR/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | sed -n '/^bigbluebutton.web.serverURL/{s/.*\///;p}') +HOST=$(cat $SERVLET_DIR/WEB-INF/classes/bigbluebutton.properties $BBB_WEB_ETC_CONFIG | grep -v '#' | sed -n '/^bigbluebutton.web.serverURL/{s/.*\///;p}' | tail -n 1) HTML5_CONFIG=/usr/share/meteor/bundle/programs/server/assets/app/config/settings.yml BBB_WEB_CONFIG=$SERVLET_DIR/WEB-INF/classes/bigbluebutton.properties diff --git a/bigbluebutton-html5/client/main.html b/bigbluebutton-html5/client/main.html index 00c5bc98cf..09a8a2af05 100755 --- a/bigbluebutton-html5/client/main.html +++ b/bigbluebutton-html5/client/main.html @@ -82,7 +82,6 @@ with BigBlueButton; if not, see . - diff --git a/bigbluebutton-html5/imports/api/annotations/server/handlers/whiteboardAnnotations.js b/bigbluebutton-html5/imports/api/annotations/server/handlers/whiteboardAnnotations.js index 8aaf807894..2a1390002a 100755 --- a/bigbluebutton-html5/imports/api/annotations/server/handlers/whiteboardAnnotations.js +++ b/bigbluebutton-html5/imports/api/annotations/server/handlers/whiteboardAnnotations.js @@ -15,7 +15,7 @@ export default function handleWhiteboardAnnotations({ header, body }, meetingId) check(annotations, Array); check(whiteboardId, String); - check(multiUser, Boolean); + check(multiUser, Array); clearAnnotations(meetingId, whiteboardId); diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods/clearWhiteboard.js b/bigbluebutton-html5/imports/api/annotations/server/methods/clearWhiteboard.js index 5c146c7c8e..2358f05014 100644 --- a/bigbluebutton-html5/imports/api/annotations/server/methods/clearWhiteboard.js +++ b/bigbluebutton-html5/imports/api/annotations/server/methods/clearWhiteboard.js @@ -10,6 +10,8 @@ export default function clearWhiteboard(whiteboardId) { const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); check(whiteboardId, String); const payload = { diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods/sendAnnotation.js b/bigbluebutton-html5/imports/api/annotations/server/methods/sendAnnotation.js index daa01d8f24..4ba53872db 100755 --- a/bigbluebutton-html5/imports/api/annotations/server/methods/sendAnnotation.js +++ b/bigbluebutton-html5/imports/api/annotations/server/methods/sendAnnotation.js @@ -1,8 +1,12 @@ +import { check } from 'meteor/check'; import sendAnnotationHelper from './sendAnnotationHelper'; import { extractCredentials } from '/imports/api/common/server/helpers'; export default function sendAnnotation(annotation) { const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + sendAnnotationHelper(annotation, meetingId, requesterUserId); } diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods/sendBulkAnnotations.js b/bigbluebutton-html5/imports/api/annotations/server/methods/sendBulkAnnotations.js index fd4530e744..ba8b0c1428 100644 --- a/bigbluebutton-html5/imports/api/annotations/server/methods/sendBulkAnnotations.js +++ b/bigbluebutton-html5/imports/api/annotations/server/methods/sendBulkAnnotations.js @@ -1,8 +1,12 @@ import { extractCredentials } from '/imports/api/common/server/helpers'; import sendAnnotationHelper from './sendAnnotationHelper'; +import { check } from 'meteor/check'; export default function sendBulkAnnotations(payload) { const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + payload.forEach(annotation => sendAnnotationHelper(annotation, meetingId, requesterUserId)); } diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods/undoAnnotation.js b/bigbluebutton-html5/imports/api/annotations/server/methods/undoAnnotation.js index bcab1c2b35..b3398e5061 100644 --- a/bigbluebutton-html5/imports/api/annotations/server/methods/undoAnnotation.js +++ b/bigbluebutton-html5/imports/api/annotations/server/methods/undoAnnotation.js @@ -10,6 +10,8 @@ export default function undoAnnotation(whiteboardId) { const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); check(whiteboardId, String); const payload = { diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js index 84ede61437..b573f8d4b0 100755 --- a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js +++ b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js @@ -39,6 +39,7 @@ const WEBSOCKET_KEEP_ALIVE_DEBOUNCE = MEDIA.websocketKeepAliveDebounce || 10; const TRACE_SIP = MEDIA.traceSip || false; const AUDIO_MICROPHONE_CONSTRAINTS = Meteor.settings.public.app.defaultSettings .application.microphoneConstraints; +const SDP_SEMANTICS = MEDIA.sdpSemantics; const getAudioSessionNumber = () => { let currItem = parseInt(sessionStorage.getItem(AUDIO_SESSION_NUM_KEY), 10); @@ -388,7 +389,7 @@ class SIPSession { sessionDescriptionHandlerFactoryOptions: { peerConnectionConfiguration: { iceServers, - sdpSemantics: 'plan-b', + sdpSemantics: SDP_SEMANTICS, }, }, displayName: callerIdName, diff --git a/bigbluebutton-html5/imports/api/breakouts/server/methods/createBreakout.js b/bigbluebutton-html5/imports/api/breakouts/server/methods/createBreakout.js index 25b7af0db8..697be09077 100644 --- a/bigbluebutton-html5/imports/api/breakouts/server/methods/createBreakout.js +++ b/bigbluebutton-html5/imports/api/breakouts/server/methods/createBreakout.js @@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor'; import RedisPubSub from '/imports/startup/server/redis'; import Logger from '/imports/startup/server/logger'; import { extractCredentials } from '/imports/api/common/server/helpers'; +import { check } from 'meteor/check'; export default function createBreakoutRoom(rooms, durationInMinutes, record = false) { const REDIS_CONFIG = Meteor.settings.private.redis; @@ -12,6 +13,9 @@ export default function createBreakoutRoom(rooms, durationInMinutes, record = fa const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + const eventName = 'CreateBreakoutRoomsCmdMsg'; if (rooms.length > MAX_BREAKOUT_ROOMS) { Logger.info(`Attempt to create breakout rooms with invalid number of rooms in meeting id=${meetingId}`); diff --git a/bigbluebutton-html5/imports/api/breakouts/server/methods/requestJoinURL.js b/bigbluebutton-html5/imports/api/breakouts/server/methods/requestJoinURL.js index ace15db4b8..71903a00ff 100755 --- a/bigbluebutton-html5/imports/api/breakouts/server/methods/requestJoinURL.js +++ b/bigbluebutton-html5/imports/api/breakouts/server/methods/requestJoinURL.js @@ -1,6 +1,7 @@ import { Meteor } from 'meteor/meteor'; import RedisPubSub from '/imports/startup/server/redis'; import { extractCredentials } from '/imports/api/common/server/helpers'; +import { check } from 'meteor/check'; export default function requestJoinURL({ breakoutId, userId: userIdToInvite }) { const REDIS_CONFIG = Meteor.settings.private.redis; @@ -8,6 +9,9 @@ export default function requestJoinURL({ breakoutId, userId: userIdToInvite }) { const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + const userId = userIdToInvite || requesterUserId; const eventName = 'RequestBreakoutJoinURLReqMsg'; diff --git a/bigbluebutton-html5/imports/api/external-videos/server/methods/emitExternalVideoEvent.js b/bigbluebutton-html5/imports/api/external-videos/server/methods/emitExternalVideoEvent.js index 29123dbd86..bf22f2eca2 100644 --- a/bigbluebutton-html5/imports/api/external-videos/server/methods/emitExternalVideoEvent.js +++ b/bigbluebutton-html5/imports/api/external-videos/server/methods/emitExternalVideoEvent.js @@ -11,12 +11,14 @@ export default function emitExternalVideoEvent(options) { const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + const { status, playerStatus } = options; - const user = Users.findOne({ meetingId, userId: requesterUserId }) + const user = Users.findOne({ meetingId, userId: requesterUserId }); if (user && user.presenter) { - check(status, String); check(playerStatus, { rate: Match.Maybe(Number), @@ -24,13 +26,14 @@ export default function emitExternalVideoEvent(options) { state: Match.Maybe(Boolean), }); - let rate = playerStatus.rate || 0; - let time = playerStatus.time || 0; - let state = playerStatus.state || 0; - const payload = { status, rate, time, state }; + const rate = playerStatus.rate || 0; + const time = playerStatus.time || 0; + const state = playerStatus.state || 0; + const payload = { + status, rate, time, state, + }; Logger.debug(`User id=${requesterUserId} sending ${EVENT_NAME} event:${state} for meeting ${meetingId}`); return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); - } } diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/chatMessageBeforeJoinCounter.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/chatMessageBeforeJoinCounter.js index f1e9d1141f..175eee0cdb 100644 --- a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/chatMessageBeforeJoinCounter.js +++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/chatMessageBeforeJoinCounter.js @@ -10,6 +10,10 @@ const PUBLIC_CHAT_TYPE = CHAT_CONFIG.type_public; export default function chatMessageBeforeJoinCounter() { const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterUserId, String); + const groupChats = GroupChat.find({ $or: [ { meetingId, access: PUBLIC_CHAT_TYPE }, diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/clearPublicChatHistory.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/clearPublicChatHistory.js index df88282d41..f0d9259234 100644 --- a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/clearPublicChatHistory.js +++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/clearPublicChatHistory.js @@ -1,6 +1,7 @@ import { Meteor } from 'meteor/meteor'; import RedisPubSub from '/imports/startup/server/redis'; import { extractCredentials } from '/imports/api/common/server/helpers'; +import { check } from 'meteor/check'; export default function clearPublicChatHistory() { const REDIS_CONFIG = Meteor.settings.private.redis; @@ -11,6 +12,9 @@ export default function clearPublicChatHistory() { const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + const payload = { chatId: PUBLIC_GROUP_CHAT_ID, }; diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/fetchMessagePerPage.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/fetchMessagePerPage.js index 8a00d3d593..32549e3d9e 100644 --- a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/fetchMessagePerPage.js +++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/fetchMessagePerPage.js @@ -1,14 +1,18 @@ import { Meteor } from 'meteor/meteor'; -import GroupChat from '/imports/api/group-chat'; import { GroupChatMsg } from '/imports/api/group-chat-msg'; import Users from '/imports/api/users'; import { extractCredentials } from '/imports/api/common/server/helpers'; +import { check } from 'meteor/check'; const CHAT_CONFIG = Meteor.settings.public.chat; const ITENS_PER_PAGE = CHAT_CONFIG.itemsPerPage; export default function fetchMessagePerPage(chatId, page) { const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterUserId, String); + const User = Users.findOne({ userId: requesterUserId, meetingId }); const messages = GroupChatMsg.find({ chatId, meetingId, timestamp: { $lt: User.authTokenValidatedTime } }, diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/sendGroupChatMsg.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/sendGroupChatMsg.js index f486cdfe6c..d83853d207 100644 --- a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/sendGroupChatMsg.js +++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/sendGroupChatMsg.js @@ -34,6 +34,8 @@ export default function sendGroupChatMsg(chatId, message) { const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); check(message, Object); const parsedMessage = parseMessage(message.message); diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/startUserTyping.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/startUserTyping.js index b46288d80e..f58126a9af 100644 --- a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/startUserTyping.js +++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/startUserTyping.js @@ -11,6 +11,8 @@ export default function startUserTyping(chatId) { const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id; const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); check(chatId, String); const payload = { diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/stopUserTyping.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/stopUserTyping.js index 619ad4963a..35bd4c12fe 100644 --- a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/stopUserTyping.js +++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/stopUserTyping.js @@ -1,10 +1,14 @@ import { UsersTyping } from '/imports/api/group-chat-msg'; import stopTyping from '../modifiers/stopTyping'; import { extractCredentials } from '/imports/api/common/server/helpers'; +import { check } from 'meteor/check'; export default function stopUserTyping() { const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + const userTyping = UsersTyping.findOne({ meetingId, userId: requesterUserId, diff --git a/bigbluebutton-html5/imports/api/group-chat/server/methods/createGroupChat.js b/bigbluebutton-html5/imports/api/group-chat/server/methods/createGroupChat.js index 2244289cc8..11d8904e4e 100644 --- a/bigbluebutton-html5/imports/api/group-chat/server/methods/createGroupChat.js +++ b/bigbluebutton-html5/imports/api/group-chat/server/methods/createGroupChat.js @@ -11,6 +11,8 @@ export default function createGroupChat(receiver) { const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); check(receiver, Object); const payload = { diff --git a/bigbluebutton-html5/imports/api/group-chat/server/methods/destroyGroupChat.js b/bigbluebutton-html5/imports/api/group-chat/server/methods/destroyGroupChat.js index bc0b859f95..5c418c1f7a 100644 --- a/bigbluebutton-html5/imports/api/group-chat/server/methods/destroyGroupChat.js +++ b/bigbluebutton-html5/imports/api/group-chat/server/methods/destroyGroupChat.js @@ -1,12 +1,16 @@ import { Meteor } from 'meteor/meteor'; import RedisPubSub from '/imports/startup/server/redis'; import { extractCredentials } from '/imports/api/common/server/helpers'; +import { check } from 'meteor/check'; export default function destroyGroupChat() { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + const eventName = 'DestroyGroupChatReqMsg'; const payload = { diff --git a/bigbluebutton-html5/imports/api/guest-users/server/methods/allowPendingUsers.js b/bigbluebutton-html5/imports/api/guest-users/server/methods/allowPendingUsers.js index 55837d56c2..d65a11a95e 100644 --- a/bigbluebutton-html5/imports/api/guest-users/server/methods/allowPendingUsers.js +++ b/bigbluebutton-html5/imports/api/guest-users/server/methods/allowPendingUsers.js @@ -11,6 +11,8 @@ const EVENT_NAME = 'GuestsWaitingApprovedMsg'; export default function allowPendingUsers(guests, status) { const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); check(guests, Array); const mappedGuests = guests.map(guest => ({ status, guest: guest.intId })); diff --git a/bigbluebutton-html5/imports/api/guest-users/server/methods/changeGuestPolicy.js b/bigbluebutton-html5/imports/api/guest-users/server/methods/changeGuestPolicy.js index d0f7e140e5..873b1a6eae 100644 --- a/bigbluebutton-html5/imports/api/guest-users/server/methods/changeGuestPolicy.js +++ b/bigbluebutton-html5/imports/api/guest-users/server/methods/changeGuestPolicy.js @@ -11,6 +11,8 @@ const EVENT_NAME = 'SetGuestPolicyCmdMsg'; export default function changeGuestPolicy(policyRule) { const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); check(policyRule, String); const payload = { diff --git a/bigbluebutton-html5/imports/api/local-settings/server/methods/userChangedLocalSettings.js b/bigbluebutton-html5/imports/api/local-settings/server/methods/userChangedLocalSettings.js index 4cb6a5d561..ab611118f0 100644 --- a/bigbluebutton-html5/imports/api/local-settings/server/methods/userChangedLocalSettings.js +++ b/bigbluebutton-html5/imports/api/local-settings/server/methods/userChangedLocalSettings.js @@ -10,6 +10,8 @@ export default function userChangedLocalSettings(settings) { if (!meetingId || !requesterUserId) return; check(settings, Object); + check(meetingId, String); + check(requesterUserId, String); const userLocalSettings = LocalSettings .findOne({ meetingId, userId: requesterUserId }, diff --git a/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js b/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js index e13e7c5f2d..041b4ae23b 100644 --- a/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js +++ b/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js @@ -4,6 +4,7 @@ import handleGetAllMeetings from './handlers/getAllMeetings'; import handleMeetingEnd from './handlers/meetingEnd'; import handleMeetingDestruction from './handlers/meetingDestruction'; import handleMeetingLocksChange from './handlers/meetingLockChange'; +import handleGuestPolicyChanged from './handlers/guestPolicyChanged'; import handleGuestLobbyMessageChanged from './handlers/guestLobbyMessageChanged'; import handleUserLockChange from './handlers/userLockChange'; import handleRecordingStatusChange from './handlers/recordingStatusChange'; @@ -22,6 +23,7 @@ RedisPubSub.on('RecordingStatusChangedEvtMsg', handleRecordingStatusChange); RedisPubSub.on('UpdateRecordingTimerEvtMsg', handleRecordingTimerChange); RedisPubSub.on('WebcamsOnlyForModeratorChangedEvtMsg', handleChangeWebcamOnlyModerator); RedisPubSub.on('GetLockSettingsRespMsg', handleMeetingLocksChange); +RedisPubSub.on('GuestPolicyChangedEvtMsg', handleGuestPolicyChanged); RedisPubSub.on('GuestLobbyMessageChangedEvtMsg', handleGuestLobbyMessageChanged); RedisPubSub.on('MeetingTimeRemainingUpdateEvtMsg', handleTimeRemainingUpdate); RedisPubSub.on('SelectRandomViewerRespMsg', handleSelectRandomViewer); diff --git a/bigbluebutton-html5/imports/api/meetings/server/handlers/guestPolicyChanged.js b/bigbluebutton-html5/imports/api/meetings/server/handlers/guestPolicyChanged.js new file mode 100644 index 0000000000..b7e42aeb9e --- /dev/null +++ b/bigbluebutton-html5/imports/api/meetings/server/handlers/guestPolicyChanged.js @@ -0,0 +1,12 @@ +import setGuestPolicy from '../modifiers/setGuestPolicy'; +import { check } from 'meteor/check'; + +export default function handleGuestPolicyChanged({ body }, meetingId) { + const { policy } = body; + + check(meetingId, String); + check(policy, String); + + + return setGuestPolicy(meetingId, policy); +} diff --git a/bigbluebutton-html5/imports/api/meetings/server/methods/clearRandomlySelectedUser.js b/bigbluebutton-html5/imports/api/meetings/server/methods/clearRandomlySelectedUser.js index 1cd5303db8..c7d46f4d75 100644 --- a/bigbluebutton-html5/imports/api/meetings/server/methods/clearRandomlySelectedUser.js +++ b/bigbluebutton-html5/imports/api/meetings/server/methods/clearRandomlySelectedUser.js @@ -1,10 +1,14 @@ import Logger from '/imports/startup/server/logger'; import Meetings from '/imports/api/meetings'; import { extractCredentials } from '/imports/api/common/server/helpers'; +import { check } from 'meteor/check'; export default function clearRandomlySelectedUser() { const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + const selector = { meetingId, }; diff --git a/bigbluebutton-html5/imports/api/meetings/server/methods/endMeeting.js b/bigbluebutton-html5/imports/api/meetings/server/methods/endMeeting.js index b77bc4ce31..d73c09c670 100644 --- a/bigbluebutton-html5/imports/api/meetings/server/methods/endMeeting.js +++ b/bigbluebutton-html5/imports/api/meetings/server/methods/endMeeting.js @@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor'; import RedisPubSub from '/imports/startup/server/redis'; import Logger from '/imports/startup/server/logger'; import { extractCredentials } from '/imports/api/common/server/helpers'; +import { check } from 'meteor/check'; export default function endMeeting() { const REDIS_CONFIG = Meteor.settings.private.redis; @@ -9,6 +10,9 @@ export default function endMeeting() { const EVENT_NAME = 'LogoutAndEndMeetingCmdMsg'; const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + const payload = { userId: requesterUserId, }; diff --git a/bigbluebutton-html5/imports/api/meetings/server/methods/toggleLockSettings.js b/bigbluebutton-html5/imports/api/meetings/server/methods/toggleLockSettings.js index 152d603947..cbb945774f 100755 --- a/bigbluebutton-html5/imports/api/meetings/server/methods/toggleLockSettings.js +++ b/bigbluebutton-html5/imports/api/meetings/server/methods/toggleLockSettings.js @@ -9,6 +9,8 @@ export default function toggleLockSettings(lockSettingsProps) { const EVENT_NAME = 'ChangeLockSettingsInMeetingCmdMsg'; const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); check(lockSettingsProps, { disableCam: Boolean, disableMic: Boolean, diff --git a/bigbluebutton-html5/imports/api/meetings/server/methods/toggleRecording.js b/bigbluebutton-html5/imports/api/meetings/server/methods/toggleRecording.js index 0bf20e5211..0ba3a5d034 100644 --- a/bigbluebutton-html5/imports/api/meetings/server/methods/toggleRecording.js +++ b/bigbluebutton-html5/imports/api/meetings/server/methods/toggleRecording.js @@ -4,6 +4,7 @@ import RedisPubSub from '/imports/startup/server/redis'; import { RecordMeetings } from '/imports/api/meetings'; import Users from '/imports/api/users'; import { extractCredentials } from '/imports/api/common/server/helpers'; +import { check } from 'meteor/check'; export default function toggleRecording() { const REDIS_CONFIG = Meteor.settings.private.redis; @@ -11,6 +12,10 @@ export default function toggleRecording() { const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator; const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterUserId, String); + const EVENT_NAME = 'SetRecordingStatusCmdMsg'; let meetingRecorded; diff --git a/bigbluebutton-html5/imports/api/meetings/server/methods/toggleWebcamsOnlyForModerator.js b/bigbluebutton-html5/imports/api/meetings/server/methods/toggleWebcamsOnlyForModerator.js index 93553bb128..303b9b4972 100755 --- a/bigbluebutton-html5/imports/api/meetings/server/methods/toggleWebcamsOnlyForModerator.js +++ b/bigbluebutton-html5/imports/api/meetings/server/methods/toggleWebcamsOnlyForModerator.js @@ -9,6 +9,9 @@ export default function toggleWebcamsOnlyForModerator(webcamsOnlyForModerator) { const EVENT_NAME = 'UpdateWebcamsOnlyForModeratorCmdMsg'; const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterUserId, String); check(webcamsOnlyForModerator, Boolean); const payload = { diff --git a/bigbluebutton-html5/imports/api/meetings/server/methods/transferUser.js b/bigbluebutton-html5/imports/api/meetings/server/methods/transferUser.js index 1bc56d624f..deca48c848 100644 --- a/bigbluebutton-html5/imports/api/meetings/server/methods/transferUser.js +++ b/bigbluebutton-html5/imports/api/meetings/server/methods/transferUser.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import RedisPubSub from '/imports/startup/server/redis'; import Logger from '/imports/startup/server/logger'; import { extractCredentials } from '/imports/api/common/server/helpers'; - +import { check } from 'meteor/check'; export default function transferUser(fromMeetingId, toMeetingId) { const REDIS_CONFIG = Meteor.settings.private.redis; @@ -11,6 +11,9 @@ export default function transferUser(fromMeetingId, toMeetingId) { const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + const payload = { fromMeetingId, toMeetingId, diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/setGuestPolicy.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/setGuestPolicy.js new file mode 100644 index 0000000000..bc6ae4e1bb --- /dev/null +++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/setGuestPolicy.js @@ -0,0 +1,28 @@ +import Meetings from '/imports/api/meetings'; +import Logger from '/imports/startup/server/logger'; +import { check } from 'meteor/check'; + +export default function setGuestPolicy(meetingId, guestPolicy) { + check(meetingId, String); + check(guestPolicy, String); + + const selector = { + meetingId, + }; + + const modifier = { + $set: { + 'usersProp.guestPolicy': guestPolicy, + }, + }; + + try { + const { numberAffected } = Meetings.upsert(selector, modifier); + + if (numberAffected) { + Logger.verbose(`Set guest policy meetingId=${meetingId} guestPolicy=${guestPolicy}`); + } + } catch (err) { + Logger.error(`Setting guest policy: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/network-information/server/methods/userInstabilityDetected.js b/bigbluebutton-html5/imports/api/network-information/server/methods/userInstabilityDetected.js index 07db47ba63..bb4ecc43ae 100644 --- a/bigbluebutton-html5/imports/api/network-information/server/methods/userInstabilityDetected.js +++ b/bigbluebutton-html5/imports/api/network-information/server/methods/userInstabilityDetected.js @@ -5,6 +5,9 @@ import { extractCredentials } from '/imports/api/common/server/helpers'; export default function userInstabilityDetected(sender) { const { meetingId, requesterUserId: receiver } = extractCredentials(this.userId); + + check(meetingId, String); + check(receiver, String); check(sender, String); const payload = { diff --git a/bigbluebutton-html5/imports/api/polls/server/handlers/sendPollChatMsg.js b/bigbluebutton-html5/imports/api/polls/server/handlers/sendPollChatMsg.js index 686dc31ec5..8870fdb41d 100644 --- a/bigbluebutton-html5/imports/api/polls/server/handlers/sendPollChatMsg.js +++ b/bigbluebutton-html5/imports/api/polls/server/handlers/sendPollChatMsg.js @@ -11,12 +11,28 @@ export default function sendPollChatMsg({ body }, meetingId) { const { answers, numRespondents } = poll; + const caseInsensitiveReducer = (acc, item) => { + const index = acc.findIndex(ans => ans.key.toLowerCase() === item.key.toLowerCase()); + if(index !== -1) { + if(acc[index].numVotes >= item.numVotes) acc[index].numVotes += item.numVotes; + else { + const tempVotes = acc[index].numVotes; + acc[index] = item; + acc[index].numVotes += tempVotes; + } + } else { + acc.push(item); + } + return acc; + }; + let responded = 0; let resultString = 'bbb-published-poll-\n'; answers.map((item) => { responded += item.numVotes; return item; - }).map((item) => { + }).reduce(caseInsensitiveReducer, []).map((item) => { + item.key = item.key.split('
').join(''); const numResponded = responded === numRespondents ? numRespondents : responded; const pct = Math.round(item.numVotes / numResponded * 100); const pctFotmatted = `${Number.isNaN(pct) ? 0 : pct}%`; diff --git a/bigbluebutton-html5/imports/api/polls/server/methods/publishPoll.js b/bigbluebutton-html5/imports/api/polls/server/methods/publishPoll.js index 701bea3b5c..738c043712 100644 --- a/bigbluebutton-html5/imports/api/polls/server/methods/publishPoll.js +++ b/bigbluebutton-html5/imports/api/polls/server/methods/publishPoll.js @@ -2,6 +2,7 @@ import RedisPubSub from '/imports/startup/server/redis'; import Polls from '/imports/api/polls'; import Logger from '/imports/startup/server/logger'; import { extractCredentials } from '/imports/api/common/server/helpers'; +import { check } from 'meteor/check'; export default function publishPoll() { const REDIS_CONFIG = Meteor.settings.private.redis; @@ -9,6 +10,10 @@ export default function publishPoll() { const EVENT_NAME = 'ShowPollResultReqMsg'; const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterUserId, String); + const poll = Polls.findOne({ meetingId }); // TODO--send pollid from client if (!poll) { Logger.error(`Attempted to publish inexisting poll for meetingId: ${meetingId}`); diff --git a/bigbluebutton-html5/imports/api/polls/server/methods/publishTypedVote.js b/bigbluebutton-html5/imports/api/polls/server/methods/publishTypedVote.js index ffd29cc537..2e7ec5ee79 100644 --- a/bigbluebutton-html5/imports/api/polls/server/methods/publishTypedVote.js +++ b/bigbluebutton-html5/imports/api/polls/server/methods/publishTypedVote.js @@ -10,6 +10,8 @@ export default function publishTypedVote(id, pollAnswer) { const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); check(pollAnswer, String); check(id, String); diff --git a/bigbluebutton-html5/imports/api/polls/server/methods/publishVote.js b/bigbluebutton-html5/imports/api/polls/server/methods/publishVote.js index 6aabfe10c6..a09be1043f 100644 --- a/bigbluebutton-html5/imports/api/polls/server/methods/publishVote.js +++ b/bigbluebutton-html5/imports/api/polls/server/methods/publishVote.js @@ -10,6 +10,8 @@ export default function publishVote(pollId, pollAnswerId) { const EVENT_NAME = 'RespondToPollReqMsg'; const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); check(pollAnswerId, Number); check(pollId, String); diff --git a/bigbluebutton-html5/imports/api/polls/server/methods/startPoll.js b/bigbluebutton-html5/imports/api/polls/server/methods/startPoll.js index 2fe305dd48..b3b67ff5f5 100644 --- a/bigbluebutton-html5/imports/api/polls/server/methods/startPoll.js +++ b/bigbluebutton-html5/imports/api/polls/server/methods/startPoll.js @@ -10,6 +10,8 @@ export default function startPoll(pollType, pollId, question, answers) { const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); check(pollId, String); check(pollType, String); diff --git a/bigbluebutton-html5/imports/api/polls/server/methods/stopPoll.js b/bigbluebutton-html5/imports/api/polls/server/methods/stopPoll.js index af0c50dd00..7a40397638 100644 --- a/bigbluebutton-html5/imports/api/polls/server/methods/stopPoll.js +++ b/bigbluebutton-html5/imports/api/polls/server/methods/stopPoll.js @@ -1,8 +1,13 @@ import RedisPubSub from '/imports/startup/server/redis'; import { extractCredentials } from '/imports/api/common/server/helpers'; +import { check } from 'meteor/check'; export default function stopPoll() { const { meetingId, requesterUserId: requesterId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterId, String); + const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'StopPollReqMsg'; diff --git a/bigbluebutton-html5/imports/api/presentation-upload-token/server/methods/requestPresentationUploadToken.js b/bigbluebutton-html5/imports/api/presentation-upload-token/server/methods/requestPresentationUploadToken.js index b01937dea0..f7331c15b7 100644 --- a/bigbluebutton-html5/imports/api/presentation-upload-token/server/methods/requestPresentationUploadToken.js +++ b/bigbluebutton-html5/imports/api/presentation-upload-token/server/methods/requestPresentationUploadToken.js @@ -8,6 +8,9 @@ export default function requestPresentationUploadToken(podId, filename) { const EVENT_NAME = 'PresentationUploadTokenReqMsg'; const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterUserId, String); check(podId, String); check(filename, String); diff --git a/bigbluebutton-html5/imports/api/presentation-upload-token/server/methods/setUsedToken.js b/bigbluebutton-html5/imports/api/presentation-upload-token/server/methods/setUsedToken.js index 6efd96aa48..413aeaafa7 100644 --- a/bigbluebutton-html5/imports/api/presentation-upload-token/server/methods/setUsedToken.js +++ b/bigbluebutton-html5/imports/api/presentation-upload-token/server/methods/setUsedToken.js @@ -1,10 +1,14 @@ import PresentationUploadToken from '/imports/api/presentation-upload-token'; import Logger from '/imports/startup/server/logger'; import { extractCredentials } from '/imports/api/common/server/helpers'; +import { check } from 'meteor/check'; export default function setUsedToken(authzToken) { const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + const payload = { $set: { used: true, diff --git a/bigbluebutton-html5/imports/api/presentations/server/methods/removePresentation.js b/bigbluebutton-html5/imports/api/presentations/server/methods/removePresentation.js index a4169aa0f3..1bf2c92345 100644 --- a/bigbluebutton-html5/imports/api/presentations/server/methods/removePresentation.js +++ b/bigbluebutton-html5/imports/api/presentations/server/methods/removePresentation.js @@ -9,6 +9,8 @@ export default function removePresentation(presentationId, podId) { const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); check(presentationId, String); check(podId, String); diff --git a/bigbluebutton-html5/imports/api/presentations/server/methods/setPresentation.js b/bigbluebutton-html5/imports/api/presentations/server/methods/setPresentation.js index ebe15cbe0d..8d277b7f41 100644 --- a/bigbluebutton-html5/imports/api/presentations/server/methods/setPresentation.js +++ b/bigbluebutton-html5/imports/api/presentations/server/methods/setPresentation.js @@ -8,6 +8,8 @@ export default function setPresentation(presentationId, podId) { const EVENT_NAME = 'SetCurrentPresentationPubMsg'; const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); check(presentationId, String); check(podId, String); diff --git a/bigbluebutton-html5/imports/api/presentations/server/methods/setPresentationDownloadable.js b/bigbluebutton-html5/imports/api/presentations/server/methods/setPresentationDownloadable.js index 5d51af14f0..d8604fde26 100644 --- a/bigbluebutton-html5/imports/api/presentations/server/methods/setPresentationDownloadable.js +++ b/bigbluebutton-html5/imports/api/presentations/server/methods/setPresentationDownloadable.js @@ -9,6 +9,8 @@ export default function setPresentationDownloadable(presentationId, downloadable const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); check(downloadable, Boolean); check(presentationId, String); diff --git a/bigbluebutton-html5/imports/api/screenshare/client/bridge/errors.js b/bigbluebutton-html5/imports/api/screenshare/client/bridge/errors.js new file mode 100644 index 0000000000..0adbe86643 --- /dev/null +++ b/bigbluebutton-html5/imports/api/screenshare/client/bridge/errors.js @@ -0,0 +1,61 @@ +import { + SFU_CLIENT_SIDE_ERRORS, + SFU_SERVER_SIDE_ERRORS +} from '/imports/ui/services/bbb-webrtc-sfu/broker-base-errors'; + +// Mapped getDisplayMedia errors. These are bridge agnostic +// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia +const GDM_ERRORS = { + // Fallback error: 1130 + 1130: 'GetDisplayMediaGenericError', + 1131: 'AbortError', + 1132: 'InvalidStateError', + 1133: 'OverconstrainedError', + 1134: 'TypeError', + 1135: 'NotFoundError', + 1136: 'NotAllowedError', + 1137: 'NotSupportedError', + 1138: 'NotReadableError', +}; + +// Import as many bridge specific errors you want in this utilitary and shove +// them into the error class slots down below. +const CLIENT_SIDE_ERRORS = { + 1101: "SIGNALLING_TRANSPORT_DISCONNECTED", + 1102: "SIGNALLING_TRANSPORT_CONNECTION_FAILED", + 1104: "SCREENSHARE_PLAY_FAILED", + 1105: "PEER_NEGOTIATION_FAILED", + 1107: "ICE_STATE_FAILED", + 1120: "MEDIA_TIMEOUT", + 1121: "UNKNOWN_ERROR", +}; + +const SERVER_SIDE_ERRORS = { + ...SFU_SERVER_SIDE_ERRORS, +} + +const AGGREGATED_ERRORS = { + ...CLIENT_SIDE_ERRORS, + ...SERVER_SIDE_ERRORS, + ...GDM_ERRORS, +} + +const expandErrors = () => { + const expandedErrors = Object.keys(AGGREGATED_ERRORS).reduce((map, key) => { + map[AGGREGATED_ERRORS[key]] = { errorCode: key, errorMessage: AGGREGATED_ERRORS[key] }; + return map; + }, {}); + + return { ...AGGREGATED_ERRORS, ...expandedErrors }; +} + +const SCREENSHARING_ERRORS = expandErrors(); + +export { + GDM_ERRORS, + BRIDGE_SERVER_SIDE_ERRORS, + BRIDGE_CLIENT_SIDE_ERRORS, + // All errors, [code]: [message] + // Expanded errors. It's AGGREGATED + message: { errorCode, errorMessage } + SCREENSHARING_ERRORS, +} diff --git a/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js index d3ffdff3b8..d16adb4c2c 100755 --- a/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js +++ b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js @@ -1,236 +1,297 @@ import Auth from '/imports/ui/services/auth'; -import BridgeService from './service'; -import { fetchWebRTCMappedStunTurnServers, getMappedFallbackStun } from '/imports/utils/fetchStunTurnServers'; -import playAndRetry from '/imports/utils/mediaElementPlayRetry'; import logger from '/imports/startup/client/logger'; +import BridgeService from './service'; +import ScreenshareBroker from '/imports/ui/services/bbb-webrtc-sfu/screenshare-broker'; +import { setSharingScreen, screenShareEndAlert } from '/imports/ui/components/screenshare/service'; +import { SCREENSHARING_ERRORS } from './errors'; const SFU_CONFIG = Meteor.settings.public.kurento; const SFU_URL = SFU_CONFIG.wsUrl; -const CHROME_DEFAULT_EXTENSION_KEY = SFU_CONFIG.chromeDefaultExtensionKey; -const CHROME_CUSTOM_EXTENSION_KEY = SFU_CONFIG.chromeExtensionKey; -const CHROME_SCREENSHARE_SOURCES = SFU_CONFIG.screenshare.chromeScreenshareSources; -const FIREFOX_SCREENSHARE_SOURCE = SFU_CONFIG.screenshare.firefoxScreenshareSource; + +const BRIDGE_NAME = 'kurento' const SCREENSHARE_VIDEO_TAG = 'screenshareVideo'; +const SEND_ROLE = 'send'; +const RECV_ROLE = 'recv'; -const CHROME_EXTENSION_KEY = CHROME_CUSTOM_EXTENSION_KEY === 'KEY' ? CHROME_DEFAULT_EXTENSION_KEY : CHROME_CUSTOM_EXTENSION_KEY; +// the error-code mapping is bridge specific; that's why it's not in the errors util +const ERROR_MAP = { + 1301: SCREENSHARING_ERRORS.SIGNALLING_TRANSPORT_DISCONNECTED, + 1302: SCREENSHARING_ERRORS.SIGNALLING_TRANSPORT_CONNECTION_FAILED, + 1305: SCREENSHARING_ERRORS.PEER_NEGOTIATION_FAILED, + 1307: SCREENSHARING_ERRORS.ICE_STATE_FAILED, +} -const getUserId = () => Auth.userID; +const mapErrorCode = (error) => { + const { errorCode } = error; + const mappedError = ERROR_MAP[errorCode]; -const getMeetingId = () => Auth.meetingID; + if (errorCode == null || mappedError == null) return error; + error.errorCode = mappedError.errorCode; + error.errorMessage = mappedError.errorMessage; + error.message = mappedError.errorMessage; -const getUsername = () => Auth.fullname; - -const getSessionToken = () => Auth.sessionToken; + return error; +} export default class KurentoScreenshareBridge { - static normalizeError(error = {}) { - const errorMessage = error.name || error.message || error.reason || 'Unknown error'; - const errorCode = error.code || undefined; - const errorReason = error.reason || error.id || 'Undefined reason'; - - return { errorMessage, errorCode, errorReason }; + constructor() { + this.role; + this.broker; + this._gdmStream; + this.hasAudio = false; + this.connectionAttempts = 0; + this.reconnecting = false; + this.reconnectionTimeout; + this.restartIntervalMs = BridgeService.BASE_MEDIA_TIMEOUT; } - static handlePresenterFailure(error, started = false) { - const normalizedError = KurentoScreenshareBridge.normalizeError(error); - if (!started) { - logger.error({ - logCode: 'screenshare_presenter_error_failed_to_connect', - extraInfo: { ...normalizedError }, - }, `Screenshare presenter failed when trying to start due to ${normalizedError.errorMessage}`); - } else { - logger.error({ - logCode: 'screenshare_presenter_error_failed_after_success', - extraInfo: { ...normalizedError }, - }, `Screenshare presenter failed during working session due to ${normalizedError.errorMessage}`); + get gdmStream() { + return this._gdmStream; + } + + set gdmStream(stream) { + this._gdmStream = stream; + } + + outboundStreamReconnect() { + const currentRestartIntervalMs = this.restartIntervalMs; + const stream = this.gdmStream; + + logger.warn({ + logCode: 'screenshare_presenter_reconnect', + extraInfo: { + reconnecting: this.reconnecting, + role: this.role, + bridge: BRIDGE_NAME + }, + }, `Screenshare presenter session is reconnecting`); + + this.stop(); + this.restartIntervalMs = BridgeService.getNextReconnectionInterval(currentRestartIntervalMs); + this.share(stream, this.onerror).then(() => { + this.clearReconnectionTimeout(); + }).catch(error => { + // Error handling is a no-op because it will be "handled" in handlePresenterFailure + logger.debug({ + logCode: 'screenshare_reconnect_failed', + extraInfo: { + errorCode: error.errorCode, + errorMessage: error.errorMessage, + reconnecting: this.reconnecting, + role: this.role, + bridge: BRIDGE_NAME + }, + }, 'Screensharing reconnect failed'); + }); + } + + inboundStreamReconnect() { + const currentRestartIntervalMs = this.restartIntervalMs; + + logger.warn({ + logCode: 'screenshare_viewer_reconnect', + extraInfo: { + reconnecting: this.reconnecting, + role: this.role, + bridge: BRIDGE_NAME + }, + }, `Screenshare viewer session is reconnecting`); + + // Cleanly stop everything before triggering a reconnect + this.stop(); + // Create new reconnect interval time + this.restartIntervalMs = BridgeService.getNextReconnectionInterval(currentRestartIntervalMs); + this.view(this.hasAudio).then(() => { + this.clearReconnectionTimeout(); + }).catch(error => { + // Error handling is a no-op because it will be "handled" in handleViewerFailure + logger.debug({ + logCode: 'screenshare_reconnect_failed', + extraInfo: { + errorCode: error.errorCode, + errorMessage: error.errorMessage, + reconnecting: this.reconnecting, + role: this.role, + bridge: BRIDGE_NAME + }, + }, 'Screensharing reconnect failed'); + }); + } + + handleConnectionTimeoutExpiry() { + this.reconnecting = true; + + switch (this.role) { + case RECV_ROLE: + return this.inboundStreamReconnect(); + case SEND_ROLE: + return this.outboundStreamReconnect(); + default: + this.reconnecting = false; + logger.error({ + logCode: 'screenshare_invalid_role' + }, 'Screen sharing with invalid role, wont reconnect'); + break; } - return normalizedError; } - static handleViewerFailure(error, started = false) { - const normalizedError = KurentoScreenshareBridge.normalizeError(error); - if (!started) { - logger.error({ - logCode: 'screenshare_viewer_error_failed_to_connect', - extraInfo: { ...normalizedError }, - }, `Screenshare viewer failed when trying to start due to ${normalizedError.errorMessage}`); - } else { - logger.error({ - logCode: 'screenshare_viewer_error_failed_after_success', - extraInfo: { ...normalizedError }, - }, `Screenshare viewer failed during working session due to ${normalizedError.errorMessage}`); + maxConnectionAttemptsReached () { + return this.connectionAttempts > BridgeService.MAX_CONN_ATTEMPTS; + } + + scheduleReconnect () { + if (this.reconnectionTimeout == null) { + this.reconnectionTimeout = setTimeout( + this.handleConnectionTimeoutExpiry.bind(this), + this.restartIntervalMs + ); } - return normalizedError; } - static playElement(screenshareMediaElement) { - const mediaTagPlayed = () => { - logger.info({ - logCode: 'screenshare_media_play_success', - }, 'Screenshare media played successfully'); + clearReconnectionTimeout () { + this.reconnecting = false; + this.restartIntervalMs = BridgeService.BASE_MEDIA_TIMEOUT; + + if (this.reconnectionTimeout) { + clearTimeout(this.reconnectionTimeout); + this.reconnectionTimeout = null; + } + } + + handleViewerStart() { + const mediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG); + + if (mediaElement && this.broker && this.broker.webRtcPeer) { + const stream = this.broker.webRtcPeer.getRemoteStream(); + BridgeService.screenshareLoadAndPlayMediaStream(stream, mediaElement, !this.broker.hasAudio); + } + + this.clearReconnectionTimeout(); + } + + handleBrokerFailure(error) { + mapErrorCode(error); + const { errorMessage, errorCode } = error; + + logger.error({ + logCode: 'screenshare_broker_failure', + extraInfo: { + errorCode, errorMessage, + role: this.broker.role, + started: this.broker.started, + reconnecting: this.reconnecting, + bridge: BRIDGE_NAME + }, + }, 'Screenshare broker failure'); + + // Screensharing was already successfully negotiated and error occurred during + // during call; schedule a reconnect + // If the session has not yet started, a reconnect should already be scheduled + if (this.broker.started) { + this.scheduleReconnect(); + } + + return error; + } + + async view(hasAudio = false) { + this.hasAudio = hasAudio; + this.role = RECV_ROLE; + const iceServers = await BridgeService.getIceServers(Auth.sessionToken); + const options = { + iceServers, + userName: Auth.fullname, + hasAudio, }; - if (screenshareMediaElement.paused) { - // Tag isn't playing yet. Play it. - screenshareMediaElement.play() - .then(mediaTagPlayed) - .catch((error) => { - // NotAllowedError equals autoplay issues, fire autoplay handling event. - // This will be handled in the screenshare react component. - if (error.name === 'NotAllowedError') { - logger.error({ - logCode: 'screenshare_error_autoplay', - extraInfo: { errorName: error.name }, - }, 'Screenshare play failed due to autoplay error'); - const tagFailedEvent = new CustomEvent('screensharePlayFailed', - { detail: { mediaElement: screenshareMediaElement } }); - window.dispatchEvent(tagFailedEvent); - } else { - // Tag failed for reasons other than autoplay. Log the error and - // try playing again a few times until it works or fails for good - const played = playAndRetry(screenshareMediaElement); - if (!played) { - logger.error({ - logCode: 'screenshare_error_media_play_failed', - extraInfo: { errorName: error.name }, - }, `Screenshare media play failed due to ${error.name}`); - } else { - mediaTagPlayed(); - } - } - }); - } else { - // Media tag is already playing, so log a success. This is really a - // logging fallback for a case that shouldn't happen. But if it does - // (ie someone re-enables the autoPlay prop in the element), then it - // means the stream is playing properly and it'll be logged. - mediaTagPlayed(); - } + this.broker = new ScreenshareBroker( + Auth.authenticateURL(SFU_URL), + BridgeService.getConferenceBridge(), + Auth.userID, + Auth.meetingID, + this.role, + options, + ); + + this.broker.onstart = this.handleViewerStart.bind(this); + this.broker.onerror = this.handleBrokerFailure.bind(this); + this.broker.onended = this.handleEnded.bind(this); + + return this.broker.view().finally(this.scheduleReconnect.bind(this)); } - static screenshareElementLoadAndPlay(stream, element, muted) { - element.muted = muted; - element.pause(); - element.srcObject = stream; - KurentoScreenshareBridge.playElement(element); + handlePresenterStart() { + logger.info({ + logCode: 'screenshare_presenter_start_success', + }, 'Screenshare presenter started succesfully'); + this.clearReconnectionTimeout(); + this.reconnecting = false; + this.connectionAttempts = 0; } - kurentoViewLocalPreview() { - const screenshareMediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG); - const { webRtcPeer } = window.kurentoManager.kurentoScreenshare; - - if (webRtcPeer) { - const stream = webRtcPeer.getLocalStream(); - KurentoScreenshareBridge.screenshareElementLoadAndPlay(stream, screenshareMediaElement, true); - } + handleEnded() { + screenShareEndAlert(); } - async kurentoViewScreen() { - const screenshareMediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG); - let iceServers = []; - let started = false; + share(stream, onFailure) { + return new Promise(async (resolve, reject) => { + this.onerror = onFailure; + this.connectionAttempts += 1; + this.role = SEND_ROLE; + this.hasAudio = BridgeService.streamHasAudioTrack(stream); + this.gdmStream = stream; - try { - iceServers = await fetchWebRTCMappedStunTurnServers(getSessionToken()); - } catch (error) { - logger.error({ - logCode: 'screenshare_viewer_fetchstunturninfo_error', - extraInfo: { error }, - }, 'Screenshare bridge failed to fetch STUN/TURN info, using default'); - iceServers = getMappedFallbackStun(); - } finally { - const options = { - wsUrl: Auth.authenticateURL(SFU_URL), - iceServers, - logger, - userName: getUsername(), - }; + const onerror = (error) => { + const normalizedError = this.handleBrokerFailure(error); + if (this.maxConnectionAttemptsReached()) { + this.clearReconnectionTimeout(); + this.connectionAttempts = 0; + onFailure(SCREENSHARING_ERRORS.MEDIA_TIMEOUT); - const onFail = (error) => { - KurentoScreenshareBridge.handleViewerFailure(error, started); - }; - - // Callback for the kurento-extension.js script. It's called when the whole - // negotiation with SFU is successful. This will load the stream into the - // screenshare media element and play it manually. - const onSuccess = () => { - started = true; - const { webRtcPeer } = window.kurentoManager.kurentoVideo; - if (webRtcPeer) { - const stream = webRtcPeer.getRemoteStream(); - KurentoScreenshareBridge.screenshareElementLoadAndPlay( - stream, - screenshareMediaElement, - true, - ); + return reject(SCREENSHARING_ERRORS.MEDIA_TIMEOUT); } }; - window.kurentoWatchVideo( - SCREENSHARE_VIDEO_TAG, - BridgeService.getConferenceBridge(), - getUserId(), - getMeetingId(), - onFail, - onSuccess, - options, - ); - } - } - - kurentoExitVideo() { - window.kurentoExitVideo(); - } - - async kurentoShareScreen(onFail, stream) { - let iceServers = []; - try { - iceServers = await fetchWebRTCMappedStunTurnServers(getSessionToken()); - } catch (error) { - logger.error({ logCode: 'screenshare_presenter_fetchstunturninfo_error' }, - - 'Screenshare bridge failed to fetch STUN/TURN info, using default'); - iceServers = getMappedFallbackStun(); - } finally { + const iceServers = await BridgeService.getIceServers(Auth.sessionToken); const options = { - wsUrl: Auth.authenticateURL(SFU_URL), - chromeExtension: CHROME_EXTENSION_KEY, - chromeScreenshareSources: CHROME_SCREENSHARE_SOURCES, - firefoxScreenshareSource: FIREFOX_SCREENSHARE_SOURCE, iceServers, - logger, - userName: getUsername(), + userName: Auth.fullname, + stream, + hasAudio: this.hasAudio, }; - let started = false; - - const failureCallback = (error) => { - const normalizedError = KurentoScreenshareBridge.handlePresenterFailure(error, started); - onFail(normalizedError); - }; - - const successCallback = () => { - started = true; - logger.info({ - logCode: 'screenshare_presenter_start_success', - }, 'Screenshare presenter started succesfully'); - }; - - options.stream = stream || undefined; - - window.kurentoShareScreen( - SCREENSHARE_VIDEO_TAG, + this.broker = new ScreenshareBroker( + Auth.authenticateURL(SFU_URL), BridgeService.getConferenceBridge(), - getUserId(), - getMeetingId(), - failureCallback, - successCallback, + Auth.userID, + Auth.meetingID, + this.role, options, ); - } - } - kurentoExitScreenShare() { - window.kurentoExitScreenShare(); + this.broker.onerror = onerror.bind(this); + this.broker.onstreamended = this.stop.bind(this); + this.broker.onstart = this.handlePresenterStart.bind(this); + this.broker.onended = this.handleEnded.bind(this); + + this.broker.share().then(() => { + this.scheduleReconnect(); + return resolve(); + }).catch(reject); + }); + }; + + stop() { + if (this.broker) { + this.broker.stop(); + // Checks if this session is a sharer and if it's not reconnecting + // If that's the case, clear the local sharing state in screen sharing UI + // component tracker to be extra sure we won't have any client-side state + // inconsistency - prlanzarin + if (this.broker.role === SEND_ROLE && !this.reconnecting) setSharingScreen(false); + this.broker = null; + } + this.gdmStream = null; + this.clearReconnectionTimeout(); } } diff --git a/bigbluebutton-html5/imports/api/screenshare/client/bridge/service.js b/bigbluebutton-html5/imports/api/screenshare/client/bridge/service.js index a8c0c90175..202fc80a05 100644 --- a/bigbluebutton-html5/imports/api/screenshare/client/bridge/service.js +++ b/bigbluebutton-html5/imports/api/screenshare/client/bridge/service.js @@ -1,37 +1,66 @@ import Meetings from '/imports/api/meetings'; import logger from '/imports/startup/client/logger'; +import { fetchWebRTCMappedStunTurnServers, getMappedFallbackStun } from '/imports/utils/fetchStunTurnServers'; +import loadAndPlayMediaStream from '/imports/ui/services/bbb-webrtc-sfu/load-play'; +import { SCREENSHARING_ERRORS } from './errors'; const { constraints: GDM_CONSTRAINTS, + mediaTimeouts: MEDIA_TIMEOUTS, } = Meteor.settings.public.kurento.screenshare; +const { + baseTimeout: BASE_MEDIA_TIMEOUT, + maxTimeout: MAX_MEDIA_TIMEOUT, + maxConnectionAttempts: MAX_CONN_ATTEMPTS, + timeoutIncreaseFactor: TIMEOUT_INCREASE_FACTOR, +} = MEDIA_TIMEOUTS; -const hasDisplayMedia = (typeof navigator.getDisplayMedia === 'function' +const HAS_DISPLAY_MEDIA = (typeof navigator.getDisplayMedia === 'function' || (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function')); const getConferenceBridge = () => Meetings.findOne().voiceProp.voiceConf; +const normalizeGetDisplayMediaError = (error) => { + return SCREENSHARING_ERRORS[error.name] || SCREENSHARING_ERRORS.GetDisplayMediaGenericError; +}; + +const getBoundGDM = () => { + if (typeof navigator.getDisplayMedia === 'function') { + return navigator.getDisplayMedia.bind(navigator); + } else if (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function') { + return navigator.mediaDevices.getDisplayMedia.bind(navigator.mediaDevices); + } +} + const getScreenStream = async () => { const gDMCallback = (stream) => { + // Some older Chromium variants choke on gDM when audio: true by NOT generating + // a promise rejection AND not generating a valid input screen stream, need to + // work around that manually for now - prlanzarin + if (stream == null) { + return Promise.reject(SCREENSHARING_ERRORS.NotSupportedError); + } + if (typeof stream.getVideoTracks === 'function' - && typeof constraints.video === 'object') { - stream.getVideoTracks().forEach((track) => { - if (typeof track.applyConstraints === 'function') { - track.applyConstraints(constraints.video).catch((error) => { + && typeof GDM_CONSTRAINTS.video === 'object') { + stream.getVideoTracks().forEach(track => { + if (typeof track.applyConstraints === 'function') { + track.applyConstraints(GDM_CONSTRAINTS.video).catch(error => { logger.warn({ logCode: 'screenshare_videoconstraint_failed', extraInfo: { errorName: error.name, errorCode: error.code }, }, - 'Error applying screenshare video constraint'); + 'Error applying screenshare video constraint'); }); } }); } if (typeof stream.getAudioTracks === 'function' - && typeof constraints.audio === 'object') { - stream.getAudioTracks().forEach((track) => { - if (typeof track.applyConstraints === 'function') { - track.applyConstraints(constraints.audio).catch((error) => { + && typeof GDM_CONSTRAINTS.audio === 'object') { + stream.getAudioTracks().forEach(track => { + if (typeof track.applyConstraints === 'function') { + track.applyConstraints(GDM_CONSTRAINTS.audio).catch(error => { logger.warn({ logCode: 'screenshare_audioconstraint_failed', extraInfo: { errorName: error.name, errorCode: error.code }, @@ -44,39 +73,81 @@ const getScreenStream = async () => { return Promise.resolve(stream); }; - const constraints = hasDisplayMedia ? GDM_CONSTRAINTS : null; + const getDisplayMedia = getBoundGDM(); - // getDisplayMedia isn't supported, generate no stream and let the legacy - // constraint fetcher work its way on kurento-extension.js - if (constraints == null) { - return Promise.resolve(); - } - if (typeof navigator.getDisplayMedia === 'function') { - return navigator.getDisplayMedia(constraints) + if (typeof getDisplayMedia === 'function') { + return getDisplayMedia(GDM_CONSTRAINTS) .then(gDMCallback) - .catch((error) => { + .catch(error => { + const normalizedError = normalizeGetDisplayMediaError(error); logger.error({ logCode: 'screenshare_getdisplaymedia_failed', - extraInfo: { errorName: error.name, errorCode: error.code }, + extraInfo: { errorCode: normalizedError.errorCode, errorMessage: normalizedError.errorMessage }, }, 'getDisplayMedia call failed'); - return Promise.resolve(); - }); - } if (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function') { - return navigator.mediaDevices.getDisplayMedia(constraints) - .then(gDMCallback) - .catch((error) => { - logger.error({ - logCode: 'screenshare_getdisplaymedia_failed', - extraInfo: { errorName: error.name, errorCode: error.code }, - }, 'getDisplayMedia call failed'); - return Promise.resolve(); + return Promise.reject(normalizedError); }); + } else { + // getDisplayMedia isn't supported, error its way out + return Promise.reject(SCREENSHARING_ERRORS.NotSupportedError); } }; +const getIceServers = (sessionToken) => { + return fetchWebRTCMappedStunTurnServers(sessionToken).catch(error => { + logger.error({ + logCode: 'screenshare_fetchstunturninfo_error', + extraInfo: { error } + }, 'Screenshare bridge failed to fetch STUN/TURN info'); + return getMappedFallbackStun(); + }); +} + +const getNextReconnectionInterval = (oldInterval) => { + return Math.min( + TIMEOUT_INCREASE_FACTOR * oldInterval, + MAX_MEDIA_TIMEOUT, + ); +} + +const streamHasAudioTrack = (stream) => { + return stream + && typeof stream.getAudioTracks === 'function' + && stream.getAudioTracks().length >= 1; +} + +const dispatchAutoplayHandlingEvent = (mediaElement) => { + const tagFailedEvent = new CustomEvent('screensharePlayFailed', + { detail: { mediaElement } }); + window.dispatchEvent(tagFailedEvent); +} + +const screenshareLoadAndPlayMediaStream = (stream, mediaElement, muted) => { + return loadAndPlayMediaStream(stream, mediaElement, muted).catch(error => { + // NotAllowedError equals autoplay issues, fire autoplay handling event. + // This will be handled in the screenshare react component. + if (error.name === 'NotAllowedError') { + logger.error({ + logCode: 'screenshare_error_autoplay', + extraInfo: { errorName: error.name }, + }, 'Screen share media play failed: autoplay error'); + dispatchAutoplayHandlingEvent(mediaElement); + } else { + throw { + errorCode: SCREENSHARING_ERRORS.SCREENSHARE_PLAY_FAILED.errorCode, + errorMessage: error.message || SCREENSHARING_ERRORS.SCREENSHARE_PLAY_FAILED.errorMessage, + }; + } + }); +} export default { - hasDisplayMedia, + HAS_DISPLAY_MEDIA, getConferenceBridge, getScreenStream, + getIceServers, + getNextReconnectionInterval, + streamHasAudioTrack, + screenshareLoadAndPlayMediaStream, + BASE_MEDIA_TIMEOUT, + MAX_CONN_ATTEMPTS, }; diff --git a/bigbluebutton-html5/imports/api/slides/server/methods/switchSlide.js b/bigbluebutton-html5/imports/api/slides/server/methods/switchSlide.js index 2251095ced..3e57c7f264 100755 --- a/bigbluebutton-html5/imports/api/slides/server/methods/switchSlide.js +++ b/bigbluebutton-html5/imports/api/slides/server/methods/switchSlide.js @@ -11,6 +11,9 @@ export default function switchSlide(slideNumber, podId) { // TODO-- send present const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'SetCurrentPagePubMsg'; const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterUserId, String); check(slideNumber, Number); const selector = { diff --git a/bigbluebutton-html5/imports/api/slides/server/methods/zoomSlide.js b/bigbluebutton-html5/imports/api/slides/server/methods/zoomSlide.js index 3d247742f3..10b949fc5e 100755 --- a/bigbluebutton-html5/imports/api/slides/server/methods/zoomSlide.js +++ b/bigbluebutton-html5/imports/api/slides/server/methods/zoomSlide.js @@ -3,6 +3,7 @@ import { Slides } from '/imports/api/slides'; import { Meteor } from 'meteor/meteor'; import RedisPubSub from '/imports/startup/server/redis'; import { extractCredentials } from '/imports/api/common/server/helpers'; +import { check } from 'meteor/check'; export default function zoomSlide(slideNumber, podId, widthRatio, heightRatio, x, y) { // TODO-- send presentationId and SlideId const REDIS_CONFIG = Meteor.settings.private.redis; @@ -11,6 +12,9 @@ export default function zoomSlide(slideNumber, podId, widthRatio, heightRatio, x const EVENT_NAME = 'ResizeAndMovePagePubMsg'; const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + const selector = { meetingId, podId, diff --git a/bigbluebutton-html5/imports/api/users-infos/server/methods/removeUserInformation.js b/bigbluebutton-html5/imports/api/users-infos/server/methods/removeUserInformation.js index 4e35bbbb45..d297498521 100644 --- a/bigbluebutton-html5/imports/api/users-infos/server/methods/removeUserInformation.js +++ b/bigbluebutton-html5/imports/api/users-infos/server/methods/removeUserInformation.js @@ -1,9 +1,14 @@ import UserInfos from '/imports/api/users-infos'; import Logger from '/imports/startup/server/logger'; import { extractCredentials } from '/imports/api/common/server/helpers'; +import { check } from 'meteor/check'; export default function removeUserInformation() { const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterUserId, String); + const selector = { meetingId, requesterUserId, diff --git a/bigbluebutton-html5/imports/api/users-infos/server/methods/requestUserInformation.js b/bigbluebutton-html5/imports/api/users-infos/server/methods/requestUserInformation.js index ed89eedd8e..cff6199fed 100644 --- a/bigbluebutton-html5/imports/api/users-infos/server/methods/requestUserInformation.js +++ b/bigbluebutton-html5/imports/api/users-infos/server/methods/requestUserInformation.js @@ -9,6 +9,9 @@ export default function getUserInformation(externalUserId) { const EVENT_NAME = 'LookUpUserReqMsg'; const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterUserId, String); check(externalUserId, String); const payload = { diff --git a/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js b/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js index edb96f1924..eb5d12c6c6 100644 --- a/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js +++ b/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js @@ -85,6 +85,9 @@ export default function addUserSettings(settings) { const { meetingId, requesterUserId: userId } = extractCredentials(this.userId); + check(meetingId, String); + check(userId, String); + let parameters = {}; settings.forEach((el) => { diff --git a/bigbluebutton-html5/imports/api/users/server/methods/assignPresenter.js b/bigbluebutton-html5/imports/api/users/server/methods/assignPresenter.js index 7e6c3b67a5..6cf198ec48 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods/assignPresenter.js +++ b/bigbluebutton-html5/imports/api/users/server/methods/assignPresenter.js @@ -11,6 +11,9 @@ export default function assignPresenter(userId) { // TODO-- send username from c const EVENT_NAME = 'AssignPresenterReqMsg'; const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterUserId, String); check(userId, String); const User = Users.findOne({ diff --git a/bigbluebutton-html5/imports/api/users/server/methods/changeRole.js b/bigbluebutton-html5/imports/api/users/server/methods/changeRole.js index bbb6bd3d31..78705e80d7 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods/changeRole.js +++ b/bigbluebutton-html5/imports/api/users/server/methods/changeRole.js @@ -11,6 +11,8 @@ export default function changeRole(userId, role) { const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); check(userId, String); check(role, String); diff --git a/bigbluebutton-html5/imports/api/users/server/methods/removeUser.js b/bigbluebutton-html5/imports/api/users/server/methods/removeUser.js index 4219cfad24..0720559fbc 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods/removeUser.js +++ b/bigbluebutton-html5/imports/api/users/server/methods/removeUser.js @@ -12,6 +12,8 @@ export default function removeUser(userId, banUser) { const { meetingId, requesterUserId: ejectedBy } = extractCredentials(this.userId); + check(meetingId, String); + check(ejectedBy, String); check(userId, String); const payload = { diff --git a/bigbluebutton-html5/imports/api/users/server/methods/setEmojiStatus.js b/bigbluebutton-html5/imports/api/users/server/methods/setEmojiStatus.js index dea4d7edb7..d96cd78d96 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods/setEmojiStatus.js +++ b/bigbluebutton-html5/imports/api/users/server/methods/setEmojiStatus.js @@ -11,6 +11,8 @@ export default function setEmojiStatus(userId, status) { const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); check(userId, String); const payload = { diff --git a/bigbluebutton-html5/imports/api/users/server/methods/setRandomUser.js b/bigbluebutton-html5/imports/api/users/server/methods/setRandomUser.js index a0da24a993..34ed860118 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods/setRandomUser.js +++ b/bigbluebutton-html5/imports/api/users/server/methods/setRandomUser.js @@ -1,6 +1,7 @@ import { Meteor } from 'meteor/meteor'; import RedisPubSub from '/imports/startup/server/redis'; import { extractCredentials } from '/imports/api/common/server/helpers'; +import { check } from 'meteor/check'; export default function setRandomUser() { const REDIS_CONFIG = Meteor.settings.private.redis; @@ -9,6 +10,9 @@ export default function setRandomUser() { const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + const payload = { requestedBy: requesterUserId, }; diff --git a/bigbluebutton-html5/imports/api/users/server/methods/setUserEffectiveConnectionType.js b/bigbluebutton-html5/imports/api/users/server/methods/setUserEffectiveConnectionType.js index 289e0f1259..f2bfaa949e 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods/setUserEffectiveConnectionType.js +++ b/bigbluebutton-html5/imports/api/users/server/methods/setUserEffectiveConnectionType.js @@ -11,6 +11,8 @@ export default function setUserEffectiveConnectionType(effectiveConnectionType) const EVENT_NAME = 'ChangeUserEffectiveConnectionMsg'; const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); check(effectiveConnectionType, String); const payload = { diff --git a/bigbluebutton-html5/imports/api/users/server/methods/toggleUserLock.js b/bigbluebutton-html5/imports/api/users/server/methods/toggleUserLock.js index 3ece68b50c..5e34f4dc61 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods/toggleUserLock.js +++ b/bigbluebutton-html5/imports/api/users/server/methods/toggleUserLock.js @@ -11,6 +11,7 @@ export default function toggleUserLock(userId, lock) { const { meetingId, requesterUserId: lockedBy } = extractCredentials(this.userId); + check(meetingId, String); check(lockedBy, String); check(userId, String); check(lock, Boolean); diff --git a/bigbluebutton-html5/imports/api/users/server/methods/userActivitySign.js b/bigbluebutton-html5/imports/api/users/server/methods/userActivitySign.js index 16642f732b..7bf68cd519 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods/userActivitySign.js +++ b/bigbluebutton-html5/imports/api/users/server/methods/userActivitySign.js @@ -3,12 +3,17 @@ import Users from '/imports/api/users'; import RedisPubSub from '/imports/startup/server/redis'; import Logger from '/imports/startup/server/logger'; import { extractCredentials } from '/imports/api/common/server/helpers'; +import { check } from 'meteor/check'; export default function userActivitySign() { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'UserActivitySignCmdMsg'; const { meetingId, requesterUserId: userId } = extractCredentials(this.userId); + + check(meetingId, String); + check(userId, String); + const payload = { userId, }; diff --git a/bigbluebutton-html5/imports/api/users/server/methods/userLeftMeeting.js b/bigbluebutton-html5/imports/api/users/server/methods/userLeftMeeting.js index 6f28a6066a..dabec39187 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods/userLeftMeeting.js +++ b/bigbluebutton-html5/imports/api/users/server/methods/userLeftMeeting.js @@ -2,11 +2,15 @@ import Logger from '/imports/startup/server/logger'; import Users from '/imports/api/users'; import { extractCredentials } from '/imports/api/common/server/helpers'; import ClientConnections from '/imports/startup/server/ClientConnections'; +import { check } from 'meteor/check'; export default function userLeftMeeting() { // TODO-- spread the code to method/modifier/handler // so we don't update the db in a method const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + const selector = { meetingId, userId: requesterUserId, diff --git a/bigbluebutton-html5/imports/api/users/server/publishers.js b/bigbluebutton-html5/imports/api/users/server/publishers.js index 3cdba628ad..1935728a22 100644 --- a/bigbluebutton-html5/imports/api/users/server/publishers.js +++ b/bigbluebutton-html5/imports/api/users/server/publishers.js @@ -3,6 +3,7 @@ import { Meteor } from 'meteor/meteor'; import Logger from '/imports/startup/server/logger'; import AuthTokenValidation, { ValidationStates } from '/imports/api/auth-token-validation'; import { extractCredentials } from '/imports/api/common/server/helpers'; +import { check } from 'meteor/check'; const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator; @@ -18,7 +19,7 @@ function currentUser() { const selector = { meetingId, userId: requesterUserId, - intId: { $exists: true } + intId: { $exists: true }, }; const options = { @@ -57,7 +58,7 @@ function users(role) { $or: [ { meetingId }, ], - intId: { $exists: true } + intId: { $exists: true }, }; const User = Users.findOne({ userId, meetingId }, { fields: { role: 1 } }); diff --git a/bigbluebutton-html5/imports/api/video-streams/server/methods/userShareWebcam.js b/bigbluebutton-html5/imports/api/video-streams/server/methods/userShareWebcam.js index 4b67929371..8d21a6182a 100644 --- a/bigbluebutton-html5/imports/api/video-streams/server/methods/userShareWebcam.js +++ b/bigbluebutton-html5/imports/api/video-streams/server/methods/userShareWebcam.js @@ -10,9 +10,12 @@ export default function userShareWebcam(stream) { const EVENT_NAME = 'UserBroadcastCamStartMsg'; const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + check(stream, String); + Logger.info(`user sharing webcam: ${meetingId} ${requesterUserId}`); - check(stream, String); // const actionName = 'joinVideo'; /* TODO throw an error if user has no permission to share webcam diff --git a/bigbluebutton-html5/imports/api/video-streams/server/methods/userUnshareWebcam.js b/bigbluebutton-html5/imports/api/video-streams/server/methods/userUnshareWebcam.js index c04d863efd..dd2400bee5 100644 --- a/bigbluebutton-html5/imports/api/video-streams/server/methods/userUnshareWebcam.js +++ b/bigbluebutton-html5/imports/api/video-streams/server/methods/userUnshareWebcam.js @@ -10,10 +10,12 @@ export default function userUnshareWebcam(stream) { const EVENT_NAME = 'UserBroadcastCamStopMsg'; const { meetingId, requesterUserId } = extractCredentials(this.userId); - Logger.info(`user unsharing webcam: ${meetingId} ${requesterUserId}`); - + check(meetingId, String); + check(requesterUserId, String); check(stream, String); + Logger.info(`user unsharing webcam: ${meetingId} ${requesterUserId}`); + // const actionName = 'joinVideo'; /* TODO throw an error if user has no permission to share webcam if (!isAllowedTo(actionName, credentials)) { diff --git a/bigbluebutton-html5/imports/api/voice-users/server/methods/ejectUserFromVoice.js b/bigbluebutton-html5/imports/api/voice-users/server/methods/ejectUserFromVoice.js index 1039a83d92..bbf44dd489 100644 --- a/bigbluebutton-html5/imports/api/voice-users/server/methods/ejectUserFromVoice.js +++ b/bigbluebutton-html5/imports/api/voice-users/server/methods/ejectUserFromVoice.js @@ -9,6 +9,9 @@ export default function ejectUserFromVoice(userId) { const EVENT_NAME = 'EjectUserFromVoiceCmdMsg'; const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterUserId, String); check(userId, String); const payload = { diff --git a/bigbluebutton-html5/imports/api/voice-users/server/methods/muteAllExceptPresenterToggle.js b/bigbluebutton-html5/imports/api/voice-users/server/methods/muteAllExceptPresenterToggle.js index c82e280d50..0ee7b2ff9b 100755 --- a/bigbluebutton-html5/imports/api/voice-users/server/methods/muteAllExceptPresenterToggle.js +++ b/bigbluebutton-html5/imports/api/voice-users/server/methods/muteAllExceptPresenterToggle.js @@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor'; import RedisPubSub from '/imports/startup/server/redis'; import Meetings from '/imports/api/meetings'; import { extractCredentials } from '/imports/api/common/server/helpers'; +import { check } from 'meteor/check'; export default function muteAllExceptPresenterToggle() { const REDIS_CONFIG = Meteor.settings.private.redis; @@ -10,6 +11,9 @@ export default function muteAllExceptPresenterToggle() { const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + const meeting = Meetings.findOne({ meetingId }); const toggleMeetingMuted = !meeting.voiceProp.muteOnStart; diff --git a/bigbluebutton-html5/imports/api/voice-users/server/methods/muteAllToggle.js b/bigbluebutton-html5/imports/api/voice-users/server/methods/muteAllToggle.js index ecfd3433f9..d818ebcce4 100755 --- a/bigbluebutton-html5/imports/api/voice-users/server/methods/muteAllToggle.js +++ b/bigbluebutton-html5/imports/api/voice-users/server/methods/muteAllToggle.js @@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor'; import RedisPubSub from '/imports/startup/server/redis'; import Meetings from '/imports/api/meetings'; import { extractCredentials } from '/imports/api/common/server/helpers'; +import { check } from 'meteor/check'; export default function muteAllToggle() { const REDIS_CONFIG = Meteor.settings.private.redis; @@ -10,6 +11,9 @@ export default function muteAllToggle() { const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + const meeting = Meetings.findOne({ meetingId }); const toggleMeetingMuted = !meeting.voiceProp.muteOnStart; diff --git a/bigbluebutton-html5/imports/api/voice-users/server/methods/muteToggle.js b/bigbluebutton-html5/imports/api/voice-users/server/methods/muteToggle.js index 3f89e94ee9..f232bf5890 100644 --- a/bigbluebutton-html5/imports/api/voice-users/server/methods/muteToggle.js +++ b/bigbluebutton-html5/imports/api/voice-users/server/methods/muteToggle.js @@ -5,6 +5,7 @@ import Users from '/imports/api/users'; import VoiceUsers from '/imports/api/voice-users'; import Meetings from '/imports/api/meetings'; import Logger from '/imports/startup/server/logger'; +import { check } from 'meteor/check'; export default function muteToggle(uId, toggle) { const REDIS_CONFIG = Meteor.settings.private.redis; @@ -12,6 +13,10 @@ export default function muteToggle(uId, toggle) { const EVENT_NAME = 'MuteUserCmdMsg'; const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterUserId, String); + const userToMute = uId || requesterUserId; const requester = Users.findOne({ diff --git a/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/handlers/modifyWhiteboardAccess.js b/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/handlers/modifyWhiteboardAccess.js index 0e21d79c9b..73c01a39cd 100644 --- a/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/handlers/modifyWhiteboardAccess.js +++ b/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/handlers/modifyWhiteboardAccess.js @@ -4,7 +4,7 @@ import modifyWhiteboardAccess from '../modifiers/modifyWhiteboardAccess'; export default function handleModifyWhiteboardAccess({ body }, meetingId) { const { multiUser, whiteboardId } = body; - check(multiUser, Boolean); + check(multiUser, Array); check(whiteboardId, String); check(meetingId, String); diff --git a/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/helpers.js b/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/helpers.js new file mode 100644 index 0000000000..377403bc3c --- /dev/null +++ b/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/helpers.js @@ -0,0 +1,31 @@ +import Users from '/imports/api/users'; +import WhiteboardMultiUser from '/imports/api/whiteboard-multi-user/'; + +const getMultiUser = (meetingId, whiteboardId) => { + const data = WhiteboardMultiUser.findOne( + { + meetingId, + whiteboardId, + }, { fields: { multiUser: 1 } }, + ); + + if (!data || !data.multiUser || !Array.isArray(data.multiUser)) return []; + + return data.multiUser; +}; + +const getUsers = (meetingId) => { + const data = Users.find( + { meetingId }, + { fields: { userId: 1 } }, + ).fetch(); + + if (!data) return []; + + return data.map(user => user.userId); +}; + +export { + getMultiUser, + getUsers, +}; diff --git a/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/methods.js b/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/methods.js index 6371e1d91c..4400535e31 100644 --- a/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/methods.js +++ b/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/methods.js @@ -1,6 +1,12 @@ import { Meteor } from 'meteor/meteor'; -import changeWhiteboardAccess from './methods/changeWhiteboardAccess'; +import addGlobalAccess from './methods/addGlobalAccess'; +import addIndividualAccess from './methods/addIndividualAccess'; +import removeGlobalAccess from './methods/removeGlobalAccess'; +import removeIndividualAccess from './methods/removeIndividualAccess'; Meteor.methods({ - changeWhiteboardAccess, + addGlobalAccess, + addIndividualAccess, + removeGlobalAccess, + removeIndividualAccess, }); diff --git a/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/methods/addGlobalAccess.js b/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/methods/addGlobalAccess.js new file mode 100644 index 0000000000..deb3a81f27 --- /dev/null +++ b/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/methods/addGlobalAccess.js @@ -0,0 +1,27 @@ +import RedisPubSub from '/imports/startup/server/redis'; +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import { getUsers } from '/imports/api/whiteboard-multi-user/server/helpers'; +import { extractCredentials } from '/imports/api/common/server/helpers'; + +export default function addGlobalAccess(whiteboardId) { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'ModifyWhiteboardAccessPubMsg'; + + check(whiteboardId, String); + + const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterUserId, String); + + const multiUser = getUsers(meetingId); + + const payload = { + multiUser, + whiteboardId, + }; + + return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); +} diff --git a/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/methods/addIndividualAccess.js b/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/methods/addIndividualAccess.js new file mode 100644 index 0000000000..9dd55a3e5a --- /dev/null +++ b/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/methods/addIndividualAccess.js @@ -0,0 +1,32 @@ +import RedisPubSub from '/imports/startup/server/redis'; +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import { getMultiUser } from '/imports/api/whiteboard-multi-user/server/helpers'; +import { extractCredentials } from '/imports/api/common/server/helpers'; + +export default function addIndividualAccess(whiteboardId, userId) { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'ModifyWhiteboardAccessPubMsg'; + + check(whiteboardId, String); + check(userId, String); + + const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterUserId, String); + + const multiUser = getMultiUser(meetingId, whiteboardId); + + if (!multiUser.includes(userId)) { + multiUser.push(userId); + + const payload = { + multiUser, + whiteboardId, + }; + + return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); + } +} diff --git a/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/methods/changeWhiteboardAccess.js b/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/methods/removeGlobalAccess.js similarity index 81% rename from bigbluebutton-html5/imports/api/whiteboard-multi-user/server/methods/changeWhiteboardAccess.js rename to bigbluebutton-html5/imports/api/whiteboard-multi-user/server/methods/removeGlobalAccess.js index ffd3b16626..4c4fe8a7fb 100644 --- a/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/methods/changeWhiteboardAccess.js +++ b/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/methods/removeGlobalAccess.js @@ -3,18 +3,20 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function changeWhiteboardAccess(multiUser, whiteboardId) { +export default function removeGlobalAccess(whiteboardId) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'ModifyWhiteboardAccessPubMsg'; - const { meetingId, requesterUserId } = extractCredentials(this.userId); - - check(multiUser, Boolean); check(whiteboardId, String); + const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterUserId, String); + const payload = { - multiUser, + multiUser: [], whiteboardId, }; diff --git a/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/methods/removeIndividualAccess.js b/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/methods/removeIndividualAccess.js new file mode 100644 index 0000000000..367c471d69 --- /dev/null +++ b/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/methods/removeIndividualAccess.js @@ -0,0 +1,30 @@ +import RedisPubSub from '/imports/startup/server/redis'; +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import { getMultiUser } from '/imports/api/whiteboard-multi-user/server/helpers'; +import { extractCredentials } from '/imports/api/common/server/helpers'; + +export default function removeIndividualAccess(whiteboardId, userId) { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'ModifyWhiteboardAccessPubMsg'; + + check(whiteboardId, String); + check(userId, String); + + const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterUserId, String); + + const multiUser = getMultiUser(meetingId, whiteboardId); + + if (multiUser.includes(userId)) { + const payload = { + multiUser: multiUser.filter(id => id !== userId), + whiteboardId, + }; + + return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); + } +} diff --git a/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/modifiers/modifyWhiteboardAccess.js b/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/modifiers/modifyWhiteboardAccess.js index 5d1944426a..7d28c0e8d8 100644 --- a/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/modifiers/modifyWhiteboardAccess.js +++ b/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/modifiers/modifyWhiteboardAccess.js @@ -5,7 +5,7 @@ import WhiteboardMultiUser from '/imports/api/whiteboard-multi-user/'; export default function modifyWhiteboardAccess(meetingId, whiteboardId, multiUser) { check(meetingId, String); check(whiteboardId, String); - check(multiUser, Boolean); + check(multiUser, Array); const selector = { meetingId, diff --git a/bigbluebutton-html5/imports/startup/client/base.jsx b/bigbluebutton-html5/imports/startup/client/base.jsx index 4dd0041992..b83d3a5e58 100755 --- a/bigbluebutton-html5/imports/startup/client/base.jsx +++ b/bigbluebutton-html5/imports/startup/client/base.jsx @@ -34,6 +34,7 @@ const BREAKOUT_END_NOTIFY_DELAY = 50; const HTML = document.getElementsByTagName('html')[0]; let breakoutNotified = false; +let checkedUserSettings = false; const propTypes = { subscriptionsReady: PropTypes.bool, @@ -400,15 +401,22 @@ const BaseContainer = withTracker(() => { }, }); - if (getFromUserSettings('bbb_show_participants_on_login', Meteor.settings.public.layout.showParticipantsOnLogin) && !deviceInfo.type().isPhone) { - if (CHAT_ENABLED && getFromUserSettings('bbb_show_public_chat_on_login', !Meteor.settings.public.chat.startClosed)) { - Session.set('openPanel', 'chat'); - Session.set('idChatOpen', PUBLIC_CHAT_ID); - } else { - Session.set('openPanel', 'userlist'); + if (Session.equals('openPanel', undefined) || Session.equals('subscriptionsReady', true)) { + if (!checkedUserSettings) { + if (getFromUserSettings('bbb_show_participants_on_login', Meteor.settings.public.layout.showParticipantsOnLogin) && !deviceInfo.type().isPhone) { + if (CHAT_ENABLED && getFromUserSettings('bbb_show_public_chat_on_login', !Meteor.settings.public.chat.startClosed)) { + Session.set('openPanel', 'chat'); + Session.set('idChatOpen', PUBLIC_CHAT_ID); + } else { + Session.set('openPanel', 'userlist'); + } + } else { + Session.set('openPanel', ''); + } + if ( Session.equals('subscriptionsReady', true )) { + checkedUserSettings = true; + } } - } else { - Session.set('openPanel', ''); } const codeError = Session.get('codeError'); diff --git a/bigbluebutton-html5/imports/startup/client/intl.jsx b/bigbluebutton-html5/imports/startup/client/intl.jsx index c56fac96ba..235671f081 100644 --- a/bigbluebutton-html5/imports/startup/client/intl.jsx +++ b/bigbluebutton-html5/imports/startup/client/intl.jsx @@ -69,75 +69,72 @@ class IntlStartup extends Component { fetch(url) .then((response) => { if (!response.ok) { - return Promise.reject(); + return false; } - return response.json(); }) .then(({ normalizedLocale, regionDefaultLocale }) => { - fetch(`${localesPath}/${DEFAULT_LANGUAGE}.json`) - .then((response) => { - if (!response.ok) { - return Promise.reject(); - } - return response.json(); - }) - .then((messages) => { - if (regionDefaultLocale !== '') { - fetch(`${localesPath}/${regionDefaultLocale}.json`) - .then((response) => { - if (!response.ok) { - return Promise.resolve(); - } - return response.json(); - }) - .then((regionDefaultMessages) => { - messages = Object.assign(messages, regionDefaultMessages); - return messages; - }); + const fetchFallbackMessages = new Promise((resolve, reject) => { + fetch(`${localesPath}/${DEFAULT_LANGUAGE}.json`) + .then((response) => { + if (!response.ok) { + return reject(); + } + return resolve(response.json()); + }); + }); + + const fetchRegionMessages = new Promise((resolve) => { + if (!regionDefaultLocale) { + return resolve(false); + } + fetch(`${localesPath}/${regionDefaultLocale}.json`) + .then((response) => { + if (!response.ok) { + return resolve(false); + } + return resolve(response.json()); + }); + }); + + const fetchSpecificMessages = new Promise((resolve) => { + if (!normalizedLocale || normalizedLocale === DEFAULT_LANGUAGE || normalizedLocale === regionDefaultLocale) { + return resolve(false); + } + fetch(`${localesPath}/${normalizedLocale}.json`) + .then((response) => { + if (!response.ok) { + return resolve(false); + } + return resolve(response.json()); + }); + }); + + Promise.all([fetchFallbackMessages, fetchRegionMessages, fetchSpecificMessages]) + .then((values) => { + let mergedMessages = Object.assign({}, values[0]); + + if (!values[1] && !values[2]) { + normalizedLocale = DEFAULT_LANGUAGE; + } else { + if (values[1]) { + mergedMessages = Object.assign(mergedMessages, values[1]); + } + if (values[2]) { + mergedMessages = Object.assign(mergedMessages, values[2]); + } } - if (normalizedLocale !== DEFAULT_LANGUAGE && normalizedLocale !== regionDefaultLocale) { - fetch(`${localesPath}/${normalizedLocale}.json`) - .then((response) => { - if (!response.ok) { - return Promise.reject(); - } - return response.json(); - }) - .then((localeMessages) => { - messages = Object.assign(messages, localeMessages); - return messages; - }) - .catch(() => { - normalizedLocale = (regionDefaultLocale) || DEFAULT_LANGUAGE; - const dasherizedLocale = normalizedLocale.replace('_', '-'); - this.setState({ messages, fetching: false, normalizedLocale: dasherizedLocale }, () => { - IntlStartup.saveLocale(normalizedLocale); - }); - }); - } - - return messages; - }) - .then((messages) => { const dasherizedLocale = normalizedLocale.replace('_', '-'); - this.setState({ messages, fetching: false, normalizedLocale: dasherizedLocale }, () => { + this.setState({ messages: mergedMessages, fetching: false, normalizedLocale: dasherizedLocale }, () => { IntlStartup.saveLocale(dasherizedLocale); }); }) .catch(() => { - normalizedLocale = DEFAULT_LANGUAGE; - const dasherizedLocale = normalizedLocale.replace('_', '-'); - this.setState({ fetching: false, normalizedLocale: dasherizedLocale }, () => { - IntlStartup.saveLocale(normalizedLocale); + this.setState({ fetching: false, normalizedLocale: null }, () => { + IntlStartup.saveLocale(DEFAULT_LANGUAGE); }); }); - }) - .catch(() => { - this.setState({ fetching: false, normalizedLocale: null }, () => { - IntlStartup.saveLocale(DEFAULT_LANGUAGE); - }); }); }); } diff --git a/bigbluebutton-html5/imports/startup/server/ClientConnections.js b/bigbluebutton-html5/imports/startup/server/ClientConnections.js index 9bb837f4ae..a8da598c50 100644 --- a/bigbluebutton-html5/imports/startup/server/ClientConnections.js +++ b/bigbluebutton-html5/imports/startup/server/ClientConnections.js @@ -3,6 +3,7 @@ import userLeaving from '/imports/api/users/server/methods/userLeaving'; import { extractCredentials } from '/imports/api/common/server/helpers'; import AuthTokenValidation from '/imports/api/auth-token-validation'; import Users from '/imports/api/users'; +import { check } from 'meteor/check'; const { enabled, syncInterval } = Meteor.settings.public.syncUsersWithConnectionManager; @@ -38,6 +39,9 @@ class ClientConnections { const { meetingId, requesterUserId: userId } = extractCredentials(sessionId); + check(meetingId, String); + check(userId, String); + if (!meetingId) { Logger.error('Error on add new client connection. sessionId=${sessionId} connection=${connection.id}', { logCode: 'client_connections_add_error_meeting_id_null', extraInfo: { meetingId, userId } } @@ -88,6 +92,9 @@ class ClientConnections { getConnectionsForClient(sessionId) { const { meetingId, requesterUserId: userId } = extractCredentials(sessionId); + check(meetingId, String); + check(userId, String); + return this.connections.get(meetingId)?.get(userId); } @@ -108,6 +115,9 @@ class ClientConnections { Logger.info(`Removing connectionId for user. sessionId=${sessionId} connectionId=${connectionId}`); const { meetingId, requesterUserId: userId } = extractCredentials(sessionId); + check(meetingId, String); + check(userId, String); + const meetingConnections = this.connections.get(meetingId); if (meetingConnections?.has(userId)) { diff --git a/bigbluebutton-html5/imports/startup/server/redis.js b/bigbluebutton-html5/imports/startup/server/redis.js index a862cfb308..793a398829 100755 --- a/bigbluebutton-html5/imports/startup/server/redis.js +++ b/bigbluebutton-html5/imports/startup/server/redis.js @@ -298,7 +298,7 @@ class RedisPubSub { }); } } - // I ignore + // ignore } } else { // add to existing queue diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx index 1a84ba7a5a..85bd4aa972 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx @@ -117,6 +117,7 @@ class ActionsDropdown extends PureComponent { handleTakePresenter, isSharingVideo, isPollingEnabled, + isSelectRandomUserEnabled, stopExternalVideoShare, mountModal, } = this.props; @@ -188,7 +189,7 @@ class ActionsDropdown extends PureComponent { /> ) : null), - (amIPresenter + (amIPresenter && isSelectRandomUserEnabled ? ( ) : null} -
+ { +