Merge pull request #1 from gustavotrott/breakout-updated

Move user among brekout rooms - Backend part
This commit is contained in:
João Victor Nunes 2022-04-12 14:25:45 -03:00 committed by GitHub
commit 9ec06177c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
158 changed files with 1613 additions and 706 deletions

View File

@ -48,4 +48,5 @@ lib_managed/
.cache
bin/
src/main/resources/
.bsp/

13
akka-bbb-apps/deploy.sh Executable file
View File

@ -0,0 +1,13 @@
#!/usr/bin/env bash
cd "$(dirname "$0")"
sudo service bbb-apps-akka stop
sbt debian:packageBin
sudo dpkg -i target/bbb-apps-akka_*.deb
echo ''
echo ''
echo '----------------'
echo 'bbb-web updated'
sudo service bbb-apps-akka start
echo 'starting service bbb-web'

View File

@ -1,5 +1,14 @@
#!/usr/bin/env bash
sudo service bbb-apps-akka stop
rm -rf src/main/resources
cp -R src/universal/conf src/main/resources
exec sbt update run
#Set correct sharedSecret and bbbWebAPI
sudo sed -i "s/sharedSecret = \"changeme\"/sharedSecret = \"$(sudo bbb-conf --salt | grep Secret: | cut -d ' ' -f 6)\"/g" src/main/resources/application.conf
sudo sed -i "s/bbbWebAPI = \"https:\/\/192.168.23.33\/bigbluebutton\/api\"/bbbWebAPI = \"https:\/\/$(hostname -f)\/bigbluebutton\/api\"/g" src/main/resources/application.conf
#sbt update - Resolves and retrieves external dependencies, more details in https://www.scala-sbt.org/1.x/docs/Command-Line-Reference.html
#sbt ~reStart (instead of run) - run with "triggered restart" mode, more details in #https://github.com/spray/sbt-revolver
exec sbt update ~reStart

View File

@ -11,6 +11,7 @@ trait BreakoutApp2x extends BreakoutRoomCreatedMsgHdlr
with CreateBreakoutRoomsCmdMsgHdlr
with EndAllBreakoutRoomsMsgHdlr
with UpdateBreakoutRoomsTimeMsgHdlr
with ChangeUserBreakoutReqMsgHdlr
with SendMessageToAllBreakoutRoomsMsgHdlr
with SendMessageToBreakoutRoomInternalMsgHdlr
with RequestBreakoutJoinURLReqMsgHdlr

View File

@ -82,6 +82,29 @@ object BreakoutHdlrHelpers extends SystemConfiguration {
}
def sendChangeUserBreakoutMsg(
outGW: OutMsgRouter,
meetingId: String,
userId: String,
fromBreakoutId: String,
toBreakoutId: String,
redirectToHtml5JoinURL: String
): Unit = {
def build(meetingId: String, userId: String, fromBreakoutId: String, toBreakoutId: String, redirectToHtml5JoinURL: String): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, userId)
val envelope = BbbCoreEnvelope(ChangeUserBreakoutEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(ChangeUserBreakoutEvtMsg.NAME, meetingId, userId)
val body = ChangeUserBreakoutEvtMsgBody(meetingId, userId, fromBreakoutId, toBreakoutId, redirectToHtml5JoinURL)
val event = ChangeUserBreakoutEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
val msgEvent = build(meetingId, userId, fromBreakoutId, toBreakoutId, redirectToHtml5JoinURL)
outGW.send(msgEvent)
}
def updateParentMeetingWithUsers(
liveMeeting: LiveMeeting,
eventBus: InternalEventBus

View File

@ -0,0 +1,81 @@
package org.bigbluebutton.core.apps.breakout
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.api.EjectUserFromBreakoutInternalMsg
import org.bigbluebutton.core.apps.breakout.BreakoutHdlrHelpers.{ getRedirectUrls }
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
import org.bigbluebutton.core.bus.BigBlueButtonEvent
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.models.{ EjectReasonCode }
import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter }
import org.bigbluebutton.core2.message.senders.{ MsgBuilder }
trait ChangeUserBreakoutReqMsgHdlr extends RightsManagementTrait {
this: MeetingActor =>
val outGW: OutMsgRouter
def handleChangeUserBreakoutReqMsg(msg: ChangeUserBreakoutReqMsg, state: MeetingState2x): MeetingState2x = {
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to move user among breakout rooms."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)
state
} else {
val meetingId = liveMeeting.props.meetingProp.intId
for {
breakoutModel <- state.breakout
} yield {
//Eject user from room From
for {
roomFrom <- breakoutModel.rooms.get(msg.body.fromBreakoutId)
} yield {
roomFrom.users.filter(u => u.id == msg.body.userId + "-" + roomFrom.sequence).foreach(user => {
eventBus.publish(BigBlueButtonEvent(roomFrom.id, EjectUserFromBreakoutInternalMsg(meetingId, roomFrom.id, user.id, msg.header.userId, "User moved to another room", EjectReasonCode.EJECT_USER, false)))
})
}
//Get join URL for room To
val redirectToHtml5JoinURL = (
for {
roomTo <- breakoutModel.rooms.get(msg.body.toBreakoutId)
(redirectToHtml5JoinURL, redirectJoinURL) <- getRedirectUrls(liveMeeting, msg.body.userId, roomTo.externalId, roomTo.sequence.toString())
} yield redirectToHtml5JoinURL
).getOrElse("")
BreakoutHdlrHelpers.sendChangeUserBreakoutMsg(
outGW,
meetingId,
msg.body.userId,
msg.body.fromBreakoutId,
msg.body.toBreakoutId,
redirectToHtml5JoinURL,
)
//Send notification to moved User
for {
roomFrom <- breakoutModel.rooms.get(msg.body.fromBreakoutId)
roomTo <- breakoutModel.rooms.get(msg.body.toBreakoutId)
} yield {
val notifyUserEvent = MsgBuilder.buildNotifyUserInMeetingEvtMsg(
msg.body.userId,
liveMeeting.props.meetingProp.intId,
"info",
"promote",
"app.updateBreakoutRoom.userChangeRoomNotification",
"Notification to warn user was moved to another room",
Vector(roomTo.shortName)
)
outGW.send(notifyUserEvent)
}
}
state
}
}
}

View File

@ -30,6 +30,9 @@ trait EjectUserFromBreakoutInternalMsgHdlr {
// send a system message to force disconnection
Sender.sendDisconnectClientSysMsg(msg.breakoutId, registeredUser.id, msg.ejectedBy, msg.reasonCode, outGW)
//send users update to parent meeting
BreakoutHdlrHelpers.updateParentMeetingWithUsers(liveMeeting, eventBus)
log.info("Eject user {} id={} in breakoutId {}", registeredUser.name, registeredUser.id, msg.breakoutId)
}

View File

@ -15,12 +15,13 @@ trait PresentationConversionCompletedSysPubMsgHdlr {
): MeetingState2x = {
val meetingId = liveMeeting.props.meetingProp.intId
val temporaryPresentationId = msg.body.presentation.temporaryPresentationId
val newState = for {
pod <- PresentationPodsApp.getPresentationPod(state, msg.body.podId)
pres <- pod.getPresentation(msg.body.presentation.id)
} yield {
val presVO = PresentationPodsApp.translatePresentationToPresentationVO(pres)
val presVO = PresentationPodsApp.translatePresentationToPresentationVO(pres, temporaryPresentationId)
PresentationSender.broadcastPresentationConversionCompletedEvtMsg(
bus,

View File

@ -58,7 +58,7 @@ object PresentationPodsApp {
)
}
PresentationVO(p.id, p.name, p.current,
PresentationVO(p.id, "", p.name, p.current,
pages.toVector, p.downloadable, p.removable)
}
@ -74,7 +74,7 @@ object PresentationPodsApp {
state.update(podManager)
}
def translatePresentationToPresentationVO(pres: PresentationInPod): PresentationVO = {
def translatePresentationToPresentationVO(pres: PresentationInPod, temporaryPresentationId: String): PresentationVO = {
val pages = pres.pages.values.map { page =>
PageVO(
id = page.id,
@ -90,7 +90,7 @@ object PresentationPodsApp {
heightRatio = page.heightRatio
)
}
PresentationVO(pres.id, pres.name, pres.current, pages.toVector, pres.downloadable, pres.removable)
PresentationVO(pres.id, temporaryPresentationId, pres.name, pres.current, pages.toVector, pres.downloadable, pres.removable)
}
def setCurrentPresentationInPod(state: MeetingState2x, podId: String, nextCurrentPresId: String): Option[PresentationPod] = {

View File

@ -19,7 +19,7 @@ trait PresentationUploadTokenReqMsgHdlr extends RightsManagementTrait {
val envelope = BbbCoreEnvelope(PresentationUploadTokenPassRespMsg.NAME, routing)
val header = BbbClientMsgHeader(PresentationUploadTokenPassRespMsg.NAME, liveMeeting.props.meetingProp.intId, msg.header.userId)
val body = PresentationUploadTokenPassRespMsgBody(msg.body.podId, token, msg.body.filename)
val body = PresentationUploadTokenPassRespMsgBody(msg.body.podId, token, msg.body.filename, msg.body.tmpPresId)
val event = PresentationUploadTokenPassRespMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
bus.outGW.send(msgEvent)

View File

@ -50,7 +50,6 @@ trait SelectRandomViewerReqMsgHdlr extends RightsManagementTrait {
Users2x.setUserExempted(liveMeeting.users2x, pickedUser, true)
}
}
val userIds = users.map { case (v) => v.intId }
broadcastEvent(msg, userIds, pickedUser)
}

View File

@ -15,12 +15,12 @@ trait UserJoinMeetingAfterReconnectReqMsgHdlr extends HandlerHelpers with UserJo
def handleUserJoinMeetingAfterReconnectReqMsg(msg: UserJoinMeetingAfterReconnectReqMsg, state: MeetingState2x): MeetingState2x = {
log.info("Received user joined after reconnecting. user {} meetingId={}", msg.body.userId, msg.header.meetingId)
Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId) match {
case Some(reconnectingUser) =>
if (reconnectingUser.userLeftFlag.left) {
log.info("Resetting flag that user left meeting. user {}", msg.body.userId)
// User has reconnected. Just reset it's flag. ralam Oct 23, 2018
sendUserLeftFlagUpdatedEvtMsg(outGW, liveMeeting, msg.body.userId, false)
Users2x.resetUserLeftFlag(liveMeeting.users2x, msg.body.userId)
}
state

View File

@ -2,8 +2,8 @@ package org.bigbluebutton.core.apps.users
import org.bigbluebutton.common2.msgs.UserJoinMeetingReqMsg
import org.bigbluebutton.core.apps.breakout.BreakoutHdlrHelpers
import org.bigbluebutton.core.models.{ Users2x, VoiceUsers }
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.models.{ Users2x, VoiceUsers }
import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting, MeetingActor, OutMsgRouter }
trait UserJoinMeetingReqMsgHdlr extends HandlerHelpers {
@ -20,8 +20,10 @@ trait UserJoinMeetingReqMsgHdlr extends HandlerHelpers {
if (reconnectingUser.userLeftFlag.left) {
log.info("Resetting flag that user left meeting. user {}", msg.body.userId)
// User has reconnected. Just reset it's flag. ralam Oct 23, 2018
sendUserLeftFlagUpdatedEvtMsg(outGW, liveMeeting, msg.body.userId, false)
Users2x.resetUserLeftFlag(liveMeeting.users2x, msg.body.userId)
}
state
case None =>
val newState = userJoinMeeting(outGW, msg.body.authToken, msg.body.clientType, liveMeeting, state)

View File

@ -3,9 +3,9 @@ package org.bigbluebutton.core.apps.users
import org.bigbluebutton.common2.msgs.UserLeaveReqMsg
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.models.{ RegisteredUsers, Users2x }
import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter }
import org.bigbluebutton.core.running.{ HandlerHelpers, MeetingActor, OutMsgRouter }
trait UserLeaveReqMsgHdlr {
trait UserLeaveReqMsgHdlr extends HandlerHelpers {
this: MeetingActor =>
val outGW: OutMsgRouter
@ -19,6 +19,8 @@ trait UserLeaveReqMsgHdlr {
// Just flag that user has left as the user might be reconnecting.
// An audit will remove this user if it hasn't rejoined after a certain period of time.
// ralam oct 23, 2018
sendUserLeftFlagUpdatedEvtMsg(outGW, liveMeeting, msg.body.userId, true)
Users2x.setUserLeftFlag(liveMeeting.users2x, msg.body.userId)
}
if (msg.body.loggedOut) {

View File

@ -76,20 +76,10 @@ object UsersApp {
outGW.send(event)
}
def sendUserEjectedMessageToClient(outGW: OutMsgRouter, meetingId: String,
userId: String, ejectedBy: String,
reason: String, reasonCode: String): Unit = {
// send a message to client
Sender.sendUserEjectedFromMeetingClientEvtMsg(
meetingId,
userId, ejectedBy, reason, reasonCode, outGW
)
}
def sendUserLeftMeetingToAllClients(outGW: OutMsgRouter, meetingId: String,
userId: String): Unit = {
userId: String, eject: Boolean = false, ejectedBy: String = "", reason: String = "", reasonCode: String = ""): Unit = {
// send a user left event for the clients to update
val userLeftMeetingEvent = MsgBuilder.buildUserLeftMeetingEvtMsg(meetingId, userId)
val userLeftMeetingEvent = MsgBuilder.buildUserLeftMeetingEvtMsg(meetingId, userId, eject, ejectedBy, reason, reasonCode)
outGW.send(userLeftMeetingEvent)
}
@ -128,8 +118,7 @@ object UsersApp {
for {
user <- Users2x.ejectFromMeeting(liveMeeting.users2x, userId)
} yield {
sendUserEjectedMessageToClient(outGW, meetingId, userId, ejectedBy, reason, reasonCode)
sendUserLeftMeetingToAllClients(outGW, meetingId, userId)
sendUserLeftMeetingToAllClients(outGW, meetingId, userId, true, ejectedBy, reason, reasonCode)
sendEjectUserFromSfuSysMsg(outGW, meetingId, userId)
if (user.presenter) {
// println(s"ejectUserFromMeeting will cause a automaticallyAssignPresenter for user=${user}")

View File

@ -2,6 +2,7 @@ package org.bigbluebutton.core.models
import com.softwaremill.quicklens._
import org.bigbluebutton.core.util.TimeUtil
import org.bigbluebutton.core2.message.senders.MsgBuilder
object Users2x {
def findWithIntId(users: Users2x, intId: String): Option[UserState] = {

View File

@ -231,6 +231,8 @@ class ReceivedJsonMsgHandlerActor(
routeGenericMsg[UpdateBreakoutRoomsTimeReqMsg](envelope, jsonNode)
case SendMessageToAllBreakoutRoomsReqMsg.NAME =>
routeGenericMsg[SendMessageToAllBreakoutRoomsReqMsg](envelope, jsonNode)
case ChangeUserBreakoutReqMsg.NAME =>
routeGenericMsg[ChangeUserBreakoutReqMsg](envelope, jsonNode)
// Layout
case GetCurrentLayoutReqMsg.NAME =>

View File

@ -25,6 +25,20 @@ trait HandlerHelpers extends SystemConfiguration {
}
}
def sendUserLeftFlagUpdatedEvtMsg(
outGW: OutMsgRouter,
liveMeeting: LiveMeeting,
intId: String,
leftFlag: Boolean
): Unit = {
for {
u <- Users2x.findWithIntId(liveMeeting.users2x, intId)
} yield {
val userLeftFlagMeetingEvent = MsgBuilder.buildUserLeftFlagUpdatedEvtMsg(liveMeeting.props.meetingProp.intId, u.intId, leftFlag)
outGW.send(userLeftFlagMeetingEvent)
}
}
def userJoinMeeting(outGW: OutMsgRouter, authToken: String, clientType: String,
liveMeeting: LiveMeeting, state: MeetingState2x): MeetingState2x = {

View File

@ -437,6 +437,7 @@ class MeetingActor(
case m: TransferUserToMeetingRequestMsg => state = handleTransferUserToMeetingRequestMsg(m, state)
case m: UpdateBreakoutRoomsTimeReqMsg => state = handleUpdateBreakoutRoomsTimeMsg(m, state)
case m: SendMessageToAllBreakoutRoomsReqMsg => state = handleSendMessageToAllBreakoutRoomsMsg(m, state)
case m: ChangeUserBreakoutReqMsg => state = handleChangeUserBreakoutReqMsg(m, state)
// Voice
case m: UserLeftVoiceConfEvtMsg => handleUserLeftVoiceConfEvtMsg(m)

View File

@ -53,7 +53,6 @@ class AnalyticsActor(val includeChat: Boolean) extends Actor with ActorLogging {
case m: UserLeftMeetingEvtMsg => logMessage(msg)
case m: PresenterUnassignedEvtMsg => logMessage(msg)
case m: PresenterAssignedEvtMsg => logMessage(msg)
case m: UserEjectedFromMeetingEvtMsg => logMessage(msg)
case m: EjectUserFromVoiceConfSysMsg => logMessage(msg)
case m: CreateBreakoutRoomSysCmdMsg => logMessage(msg)
case m: RequestBreakoutJoinURLReqMsg => logMessage(msg)

View File

@ -256,18 +256,6 @@ object MsgBuilder {
BbbCommonEnvCoreMsg(envelope, event)
}
def buildUserEjectedFromMeetingEvtMsg(meetingId: String, userId: String,
ejectedBy: String, reason: String,
reasonCode: String): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, userId)
val envelope = BbbCoreEnvelope(UserEjectedFromMeetingEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(UserEjectedFromMeetingEvtMsg.NAME, meetingId, userId)
val body = UserEjectedFromMeetingEvtMsgBody(userId, ejectedBy, reason, reasonCode)
val event = UserEjectedFromMeetingEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
def buildDisconnectClientSysMsg(meetingId: String, userId: String, ejectedBy: String, reason: String): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(MessageTypes.SYSTEM, meetingId, userId)
val envelope = BbbCoreEnvelope(DisconnectClientSysMsg.NAME, routing)
@ -288,16 +276,26 @@ object MsgBuilder {
BbbCommonEnvCoreMsg(envelope, event)
}
def buildUserLeftMeetingEvtMsg(meetingId: String, userId: String): BbbCommonEnvCoreMsg = {
def buildUserLeftMeetingEvtMsg(meetingId: String, userId: String, eject: Boolean = false, ejectedBy: String = "", reason: String = "", reasonCode: String = ""): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId)
val envelope = BbbCoreEnvelope(UserLeftMeetingEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(UserLeftMeetingEvtMsg.NAME, meetingId, userId)
val body = UserLeftMeetingEvtMsgBody(userId)
val body = UserLeftMeetingEvtMsgBody(userId, eject, ejectedBy, reason, reasonCode)
val event = UserLeftMeetingEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
def buildUserLeftFlagUpdatedEvtMsg(meetingId: String, userId: String, userLeftFlag: Boolean): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId)
val envelope = BbbCoreEnvelope(UserLeftFlagUpdatedEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(UserLeftFlagUpdatedEvtMsg.NAME, meetingId, userId)
val body = UserLeftFlagUpdatedEvtMsgBody(userId, userLeftFlag)
val event = UserLeftFlagUpdatedEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
def buildUserInactivityInspectMsg(meetingId: String, userId: String, responseDelay: Long): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, userId)
val envelope = BbbCoreEnvelope(UserInactivityInspectMsg.NAME, routing)

View File

@ -4,13 +4,6 @@ import org.bigbluebutton.core.running.OutMsgRouter
object Sender {
def sendUserEjectedFromMeetingClientEvtMsg(meetingId: String, userId: String,
ejectedBy: String, reason: String,
reasonCode: String, outGW: OutMsgRouter): Unit = {
val ejectFromMeetingClientEvent = MsgBuilder.buildUserEjectedFromMeetingEvtMsg(meetingId, userId, ejectedBy, reason, reasonCode)
outGW.send(ejectFromMeetingClientEvent)
}
def sendDisconnectClientSysMsg(meetingId: String, userId: String,
ejectedBy: String, reason: String, outGW: OutMsgRouter): Unit = {
val ejectFromMeetingSystemEvent = MsgBuilder.buildDisconnectClientSysMsg(meetingId, userId, ejectedBy, reason)

View File

@ -4,11 +4,11 @@ import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.models.UserState
object UserLeftMeetingEvtMsgSender {
def build(meetingId: String, user: UserState): BbbCommonEnvCoreMsg = {
def build(meetingId: String, user: UserState, eject: Boolean = false, ejectedBy: String = "", reason: String = "", reasonCode: String = ""): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, user.intId)
val envelope = BbbCoreEnvelope(UserLeftMeetingEvtMsg.NAME, routing)
val body = UserLeftMeetingEvtMsgBody(intId = user.intId)
val body = UserLeftMeetingEvtMsgBody(intId = user.intId, eject, ejectedBy, reason, reasonCode)
val event = UserLeftMeetingEvtMsg(meetingId, user.intId, body)
BbbCommonEnvCoreMsg(envelope, event)

View File

@ -142,7 +142,7 @@ class RedisRecorderActor(
ev.setSenderId(msg.body.msg.sender.id)
ev.setMessage(msg.body.msg.message)
ev.setSenderRole(msg.body.msg.sender.role)
val isModerator = msg.body.msg.sender.role == "MODERATOR"
ev.setChatEmphasizedText(msg.body.msg.chatEmphasizedText && isModerator)

View File

@ -53,4 +53,5 @@ akka-patterns-store/
lib_managed/
.cache
bin/
.bsp/

View File

@ -1,6 +1,6 @@
package org.bigbluebutton.common2.domain
case class PresentationVO(id: String, name: String, current: Boolean = false,
case class PresentationVO(id: String, temporaryPresentationId: String, name: String, current: Boolean = false,
pages: Vector[PageVO], downloadable: Boolean, removable: Boolean)
case class PageVO(id: String, num: Int, thumbUri: String = "", swfUri: String,

View File

@ -110,6 +110,14 @@ object SendMessageToAllBreakoutRoomsEvtMsg { val NAME = "SendMessageToAllBreakou
case class SendMessageToAllBreakoutRoomsEvtMsg(header: BbbClientMsgHeader, body: SendMessageToAllBreakoutRoomsEvtMsgBody) extends BbbCoreMsg
case class SendMessageToAllBreakoutRoomsEvtMsgBody(meetingId: String, senderId: String, msg: String, totalOfRooms: Int)
object ChangeUserBreakoutReqMsg { val NAME = "ChangeUserBreakoutReqMsg" }
case class ChangeUserBreakoutReqMsg(header: BbbClientMsgHeader, body: ChangeUserBreakoutReqMsgBody) extends StandardMsg
case class ChangeUserBreakoutReqMsgBody(meetingId: String, userId: String, fromBreakoutId: String, toBreakoutId: String)
object ChangeUserBreakoutEvtMsg { val NAME = "ChangeUserBreakoutEvtMsg" }
case class ChangeUserBreakoutEvtMsg(header: BbbClientMsgHeader, body: ChangeUserBreakoutEvtMsgBody) extends StandardMsg
case class ChangeUserBreakoutEvtMsgBody(meetingId: String, userId: String, fromBreakoutId: String, toBreakoutId: String, redirectToHtml5JoinURL: String)
// Common Value objects
case class BreakoutUserVO(id: String, name: String)

View File

@ -13,7 +13,7 @@ case class RemovePresentationPodPubMsgBody(podId: String)
object PresentationUploadTokenReqMsg { val NAME = "PresentationUploadTokenReqMsg" }
case class PresentationUploadTokenReqMsg(header: BbbClientMsgHeader, body: PresentationUploadTokenReqMsgBody) extends StandardMsg
case class PresentationUploadTokenReqMsgBody(podId: String, filename: String)
case class PresentationUploadTokenReqMsgBody(podId: String, filename: String, tmpPresId: String)
object GetAllPresentationPodsReqMsg { val NAME = "GetAllPresentationPodsReqMsg" }
case class GetAllPresentationPodsReqMsg(header: BbbClientMsgHeader, body: GetAllPresentationPodsReqMsgBody) extends StandardMsg
@ -113,13 +113,14 @@ case class PresentationConversionRequestReceivedSysMsg(
body: PresentationConversionRequestReceivedSysMsgBody
) extends StandardMsg
case class PresentationConversionRequestReceivedSysMsgBody(
podId: String,
presentationId: String,
current: Boolean,
presName: String,
downloadable: Boolean,
removable: Boolean,
authzToken: String
podId: String,
presentationId: String,
temporaryPresentationId: String,
current: Boolean,
presName: String,
downloadable: Boolean,
removable: Boolean,
authzToken: String
)
object PresentationPageConversionStartedSysMsg { val NAME = "PresentationPageConversionStartedSysMsg" }
@ -181,7 +182,7 @@ case class PdfConversionInvalidErrorEvtMsgBody(podId: String, messageKey: String
object PresentationUploadTokenPassRespMsg { val NAME = "PresentationUploadTokenPassRespMsg" }
case class PresentationUploadTokenPassRespMsg(header: BbbClientMsgHeader, body: PresentationUploadTokenPassRespMsgBody) extends StandardMsg
case class PresentationUploadTokenPassRespMsgBody(podId: String, authzToken: String, filename: String)
case class PresentationUploadTokenPassRespMsgBody(podId: String, authzToken: String, filename: String, tmpPresId: String)
object PresentationUploadTokenFailRespMsg { val NAME = "PresentationUploadTokenFailRespMsg" }
case class PresentationUploadTokenFailRespMsg(header: BbbClientMsgHeader, body: PresentationUploadTokenFailRespMsgBody) extends StandardMsg

View File

@ -68,7 +68,21 @@ case class UserLeftMeetingEvtMsg(
header: BbbClientMsgHeader,
body: UserLeftMeetingEvtMsgBody
) extends BbbCoreMsg
case class UserLeftMeetingEvtMsgBody(intId: String)
case class UserLeftMeetingEvtMsgBody(intId: String, eject: Boolean, ejectedBy: String, reason: String, reasonCode: String)
object UserLeftFlagUpdatedEvtMsg {
val NAME = "UserLeftFlagUpdatedEvtMsg"
def apply(meetingId: String, userId: String, body: UserLeftFlagUpdatedEvtMsgBody): UserLeftFlagUpdatedEvtMsg = {
val header = BbbClientMsgHeader(UserLeftFlagUpdatedEvtMsg.NAME, meetingId, userId)
UserLeftFlagUpdatedEvtMsg(header, body)
}
}
case class UserLeftFlagUpdatedEvtMsg(
header: BbbClientMsgHeader,
body: UserLeftFlagUpdatedEvtMsgBody
) extends BbbCoreMsg
case class UserLeftFlagUpdatedEvtMsgBody(intId: String, userLeftFlag: Boolean)
object UserJoinedMeetingEvtMsg {
val NAME = "UserJoinedMeetingEvtMsg"
@ -183,13 +197,6 @@ object UserEmojiChangedEvtMsg { val NAME = "UserEmojiChangedEvtMsg" }
case class UserEmojiChangedEvtMsg(header: BbbClientMsgHeader, body: UserEmojiChangedEvtMsgBody) extends BbbCoreMsg
case class UserEmojiChangedEvtMsgBody(userId: String, emoji: String)
/**
* Sent to all clients about a user ejected (kicked) from the meeting
*/
object UserEjectedFromMeetingEvtMsg { val NAME = "UserEjectedFromMeetingEvtMsg" }
case class UserEjectedFromMeetingEvtMsg(header: BbbClientMsgHeader, body: UserEjectedFromMeetingEvtMsgBody) extends StandardMsg
case class UserEjectedFromMeetingEvtMsgBody(userId: String, ejectedBy: String, reason: String, reasonCode: String)
object AssignPresenterReqMsg { val NAME = "AssignPresenterReqMsg" }
case class AssignPresenterReqMsg(header: BbbClientMsgHeader, body: AssignPresenterReqMsgBody) extends StandardMsg
case class AssignPresenterReqMsgBody(requesterId: String, newPresenterId: String, newPresenterName: String, assignedBy: String)

View File

@ -53,4 +53,5 @@ akka-patterns-store/
lib_managed/
.cache
bin/
.bsp/

View File

@ -824,11 +824,11 @@ public class ParamsProcessorUtil {
return DigestUtils.sha1Hex(extMeetingId);
}
public String processPassword(String pass) {
return StringUtils.isEmpty(pass) ? RandomStringUtils.randomAlphanumeric(8) : pass;
}
public String processPassword(String pass) {
return StringUtils.isEmpty(pass) ? RandomStringUtils.randomAlphanumeric(8) : pass;
}
public boolean hasChecksumAndQueryString(String checksum, String queryString) {
public boolean hasChecksumAndQueryString(String checksum, String queryString) {
return (! StringUtils.isEmpty(checksum) && StringUtils.isEmpty(queryString));
}

View File

@ -2,6 +2,7 @@ package org.bigbluebutton.api;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -45,7 +46,8 @@ public final class Util {
public static String generatePresentationId(String presFilename) {
long timestamp = System.currentTimeMillis();
return DigestUtils.sha1Hex(presFilename) + "-" + timestamp;
String uuid = UUID.randomUUID().toString();
return DigestUtils.sha1Hex(presFilename + uuid) + "-" + timestamp;
}
public static String createNewFilename(String presId, String fileExt) {

View File

@ -49,8 +49,8 @@ public class Meeting {
private boolean forciblyEnded = false;
private String telVoice;
private String webVoice;
private String moderatorPass;
private String viewerPass;
private String moderatorPass = "";
private String viewerPass = "";
private int learningDashboardCleanupDelayInMinutes;
private String learningDashboardAccessToken;
private ArrayList<String> disabledFeatures;
@ -116,10 +116,18 @@ public class Meeting {
name = builder.name;
extMeetingId = builder.externalId;
intMeetingId = builder.internalId;
viewerPass = builder.viewerPass;
moderatorPass = builder.moderatorPass;
disabledFeatures = builder.disabledFeatures;
notifyRecordingIsOn = builder.notifyRecordingIsOn;
if (builder.viewerPass == null){
viewerPass = "";
} else {
viewerPass = builder.viewerPass;
}
if (builder.moderatorPass == null){
moderatorPass = "";
} else {
moderatorPass = builder.moderatorPass;
}
learningDashboardCleanupDelayInMinutes = builder.learningDashboardCleanupDelayInMinutes;
learningDashboardAccessToken = builder.learningDashboardAccessToken;
maxUsers = builder.maxUsers;

View File

@ -1,22 +0,0 @@
package org.bigbluebutton.api.model.constraint;
import org.bigbluebutton.api.model.validator.ModeratorPasswordValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Constraint(validatedBy = ModeratorPasswordValidator.class)
@Target(TYPE)
@Retention(RUNTIME)
public @interface ModeratorPasswordConstraint {
String key() default "invalidPassword";
String message() default "The supplied moderator password is incorrect";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -29,7 +29,6 @@ public class EndMeeting extends RequestWithChecksum<EndMeeting.Params> {
private String meetingID;
@PasswordConstraint
@NotEmpty(message = "You must provide the moderator password")
private String password;
@Valid

View File

@ -38,7 +38,6 @@ public class JoinMeeting extends RequestWithChecksum<JoinMeeting.Params> {
private String fullName;
@PasswordConstraint
@NotEmpty(key = "invalidPassword", message = "You must provide either the moderator or attendee password")
private String password;
@IsBooleanConstraint(message = "Guest must be a boolean value (true or false)")

View File

@ -1,6 +1,3 @@
package org.bigbluebutton.api.model.shared;
import org.bigbluebutton.api.model.constraint.ModeratorPasswordConstraint;
@ModeratorPasswordConstraint
public class ModeratorPassword extends Password {}

View File

@ -7,7 +7,6 @@ public abstract class Password {
@NotEmpty(message = "You must provide the meeting ID")
protected String meetingID;
@NotEmpty(message = "You must provide the password for the call")
protected String password;
public String getMeetingID() {

View File

@ -36,18 +36,10 @@ public class JoinPasswordValidator implements ConstraintValidator<JoinPasswordCo
String attendeePassword = meeting.getViewerPassword();
String providedPassword = joinPassword.getPassword();
if(providedPassword == null) {
return false;
}
log.info("Moderator password: {}", moderatorPassword);
log.info("Attendee password: {}", attendeePassword);
log.info("Provided password: {}", providedPassword);
if(!providedPassword.equals(moderatorPassword) && !providedPassword.equals(attendeePassword)) {
return false;
}
return true;
}
}

View File

@ -1,52 +0,0 @@
package org.bigbluebutton.api.model.validator;
import org.bigbluebutton.api.domain.Meeting;
import org.bigbluebutton.api.model.constraint.ModeratorPasswordConstraint;
import org.bigbluebutton.api.model.shared.ModeratorPassword;
import org.bigbluebutton.api.service.ServiceUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class ModeratorPasswordValidator implements ConstraintValidator<ModeratorPasswordConstraint, ModeratorPassword> {
private static Logger log = LoggerFactory.getLogger(ModeratorPasswordValidator.class);
@Override
public void initialize(ModeratorPasswordConstraint constraintAnnotation) {}
@Override
public boolean isValid(ModeratorPassword moderatorPassword, ConstraintValidatorContext context) {
log.info("Validating password {} for meeting with ID {}",
moderatorPassword.getPassword(), moderatorPassword.getMeetingID());
if(moderatorPassword.getMeetingID() == null) {
return false;
}
Meeting meeting = ServiceUtils.findMeetingFromMeetingID(moderatorPassword.getMeetingID());
if(meeting == null) {
return false;
}
String actualPassword = meeting.getModeratorPassword();
String providedPassword = moderatorPassword.getPassword();
if(providedPassword == null) {
return false;
}
log.info("Actual password: {}", actualPassword);
log.info("Provided password: {}", providedPassword);
if(!providedPassword.equals(actualPassword)) {
return false;
}
return true;
}
}

View File

@ -19,15 +19,11 @@ public class PasswordValidator implements ConstraintValidator<PasswordConstraint
@Override
public boolean isValid(String password, ConstraintValidatorContext context) {
log.info("Validating password [{}]", password);
if(password == null || password.equals("")) {
log.info("Provided password is either null or an empty string");
return true;
}
if(password.length() < 2 || password.length() > 64) {
log.info("Passwords must be between 2 and 64 characters in length");
return false;
if (password != null && !password.isEmpty()){
if (password.length() < 2 || password.length() > 64) {
log.info("Passwords must be between 2 and 64 characters in length");
return false;
}
}
return true;

View File

@ -148,6 +148,7 @@ public class DocumentConversionServiceImp implements DocumentConversionService {
pres.getPodId(),
pres.getMeetingId(),
pres.getId(),
pres.getTemporaryPresentationId(),
pres.getName(),
pres.getAuthzToken(),
pres.isDownloadable(),

View File

@ -26,6 +26,7 @@ public final class UploadedPresentation {
private final String podId;
private final String meetingId;
private final String id;
private final String temporaryPresentationId;
private final String name;
private final boolean uploadFailed;
private final ArrayList<String> uploadFailReason;
@ -44,6 +45,7 @@ public final class UploadedPresentation {
public UploadedPresentation(String podId,
String meetingId,
String id,
String temporaryPresentationId,
String name,
String baseUrl,
Boolean current,
@ -53,6 +55,7 @@ public final class UploadedPresentation {
this.podId = podId;
this.meetingId = meetingId;
this.id = id;
this.temporaryPresentationId = temporaryPresentationId;
this.name = name;
this.baseUrl = baseUrl;
this.isDownloadable = false;
@ -62,6 +65,19 @@ public final class UploadedPresentation {
this.uploadFailReason = uploadFailReason;
}
public UploadedPresentation(String podId,
String meetingId,
String id,
String name,
String baseUrl,
Boolean current,
String authzToken,
Boolean uploadFailed,
ArrayList<String> uploadFailReason) {
this(podId, meetingId, id, "", name, baseUrl,
current, authzToken, uploadFailed, uploadFailReason);
}
public File getUploadedFile() {
return uploadedFile;
}
@ -82,6 +98,10 @@ public final class UploadedPresentation {
return id;
}
public String getTemporaryPresentationId() {
return temporaryPresentationId;
}
public String getName() {
return name;
}

View File

@ -85,7 +85,7 @@ public class SwfSlidesGenerationProgressNotifier {
}
DocPageCompletedProgress progress = new DocPageCompletedProgress(pres.getPodId(), pres.getMeetingId(),
pres.getId(), pres.getId(),
pres.getId(), pres.getTemporaryPresentationId(), pres.getId(),
pres.getName(), "notUsedYet", "notUsedYet",
pres.isDownloadable(), pres.isRemovable(), ConversionMessageConstants.CONVERSION_COMPLETED_KEY,
pres.getNumberOfPages(), generateBasePresUrl(pres), pres.isCurrent());

View File

@ -4,6 +4,7 @@ public class DocConversionRequestReceived implements IDocConversionMsg {
public final String podId;
public final String meetingId;
public final String presId;
public final String temporaryPresentationId;
public final String filename;
public final String authzToken;
public final Boolean downloadable;
@ -13,6 +14,7 @@ public class DocConversionRequestReceived implements IDocConversionMsg {
public DocConversionRequestReceived(String podId,
String meetingId,
String presId,
String temporaryPresentationId,
String filename,
String authzToken,
Boolean downloadable,
@ -21,6 +23,7 @@ public class DocConversionRequestReceived implements IDocConversionMsg {
this.podId = podId;
this.meetingId = meetingId;
this.presId = presId;
this.temporaryPresentationId = temporaryPresentationId;
this.filename = filename;
this.authzToken = authzToken;
this.downloadable = downloadable;

View File

@ -4,6 +4,7 @@ public class DocPageCompletedProgress implements IDocConversionMsg {
public final String podId;
public final String meetingId;
public final String presId;
public final String temporaryPresentationId;
public final String presInstance;
public final String filename;
public final String uploaderId;
@ -15,13 +16,14 @@ public class DocPageCompletedProgress implements IDocConversionMsg {
public final String presBaseUrl;
public final Boolean current;
public DocPageCompletedProgress(String podId, String meetingId, String presId, String presInstance,
public DocPageCompletedProgress(String podId, String meetingId, String presId, String temporaryPresentationId, String presInstance,
String filename, String uploaderId, String authzToken,
Boolean downloadable, Boolean removable, String key,
Integer numPages, String presBaseUrl, Boolean current) {
this.podId = podId;
this.meetingId = meetingId;
this.presId = presId;
this.temporaryPresentationId = temporaryPresentationId;
this.presInstance = presInstance;
this.filename = filename;
this.uploaderId = uploaderId;

View File

@ -157,7 +157,7 @@ object MsgBuilder {
val header = BbbClientMsgHeader(PresentationConversionCompletedSysPubMsg.NAME, msg.meetingId, msg.authzToken)
val pages = generatePresentationPages(msg.presId, msg.numPages.intValue(), msg.presBaseUrl)
val presentation = PresentationVO(msg.presId, msg.filename,
val presentation = PresentationVO(msg.presId, msg.temporaryPresentationId, msg.filename,
current = msg.current.booleanValue(), pages.values.toVector, msg.downloadable.booleanValue(), msg.removable.booleanValue())
val body = PresentationConversionCompletedSysPubMsgBody(podId = msg.podId, messageKey = msg.key,
@ -228,6 +228,7 @@ object MsgBuilder {
val body = PresentationConversionRequestReceivedSysMsgBody(
podId = msg.podId,
presentationId = msg.presId,
temporaryPresentationId = msg.temporaryPresentationId,
current = msg.current,
presName = msg.filename,
downloadable = msg.downloadable,

View File

@ -0,0 +1,22 @@
#!/usr/bin/env bash
cd "$(dirname "$0")"
for var in "$@"
do
if [[ $var == --reset ]] ; then
echo "Performing a full reset..."
rm -rf node_modules
fi
done
if [ ! -d ./node_modules ] ; then
npm install
fi
npm run build
cp -r build/* /var/bigbluebutton/learning-dashboard
sudo systemctl restart nginx
echo ''
echo ''
echo '----------------'
echo 'bbb-learning-dashboard updated'

View File

@ -0,0 +1,32 @@
#!/usr/bin/env bash
cd "$(dirname "$0")"
for var in "$@"
do
if [[ $var == --reset ]] ; then
echo "Performing a full reset..."
rm -rf node_modules
fi
done
if [ ! -d ./node_modules ] ; then
npm install
fi
mkdir -p public/test/test
if [ -e public/test/test/learning_dashboard_data.json ]; then
echo "Json found in public/test/test/learning_dashboard_data.json"
echo ""
tput setaf 2;
echo "To test the Dashboard access:"
echo "http://localhost:3000/learning-analytics-dashboard?meeting=test&report=test"
echo ""
tput sgr0
else
echo "ERROR: Before running, copy a Dashboard data .json from a meeting and save in $(pwd)/public/test/test/learning_dashboard_data.json"
echo "This file can be found in /var/bigbluebutton/learning-dashboard/\$meetingId/"
exit 1
fi
npm start | cat

View File

@ -1 +1 @@
git clone --branch v1.0.2 --depth 1 https://github.com/bigbluebutton/bbb-pads bbb-pads
git clone --branch v1.1.0 --depth 1 https://github.com/bigbluebutton/bbb-pads bbb-pads

View File

@ -1 +1 @@
BIGBLUEBUTTON_RELEASE=2.5.0-alpha.6
BIGBLUEBUTTON_RELEASE=2.5.0-beta.1

View File

@ -1296,19 +1296,6 @@ check_state() {
done
fi
stunServerAddress=$(cat /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini | sed -n '/^stunServerAddress/{s/.*=//;p}')
stunServerPort=$(cat /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini | sed -n '/^stunServerPort/{s/.*=//;p}')
if [ ! -z "$stunServerAddress" ]; then
if stunclient --mode full --localport 30000 $stunServerAddress $stunServerPort | grep -q "fail\|Unable\ to\ resolve"; then
echo
echo "#"
echo "# Warning: Failed to verify STUN server at $stunServerAddress:$stunServerPort with command"
echo "#"
echo "# stunclient --mode full --localport 30000 $stunServerAddress $stunServerPort"
echo "#"
fi
fi
BBB_LOG="/var/log/bigbluebutton"
if [ "$(stat -c "%U %G" $BBB_LOG)" != "bigbluebutton bigbluebutton" ]; then
echo
@ -1476,14 +1463,6 @@ if [ $CHECK ]; then
done
fi
stunServerAddress=$(cat /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini | sed -n '/^stunServerAddress/{s/.*=//;p}')
stunServerPort=$(cat /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini | sed -n '/^stunServerPort/{s/.*=//;p}')
if [ ! -z "$stunServerAddress" ]; then
echo
echo "/etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini (STUN Server)"
echo " stun: $stunServerAddress:$stunServerPort"
fi
check_state
echo

View File

@ -1,4 +1,5 @@
#!/bin/sh -ex
cd "$(dirname "$0")"
# Please check bigbluebutton/bigbluebutton-html5/dev_local_deployment/README.md
@ -6,7 +7,7 @@ UPPER_DESTINATION_DIR=/usr/share/meteor
DESTINATION_DIR=$UPPER_DESTINATION_DIR/bundle
SERVICE_FILES_DIR=/usr/lib/systemd/system
LOCAL_PACKAGING_DIR=/home/bigbluebutton/dev/bigbluebutton/build/packages-template/bbb-html5
LOCAL_PACKAGING_DIR="$(pwd)/../build/packages-template/bbb-html5"
if [ ! -d "$LOCAL_PACKAGING_DIR" ]; then
echo "Did not find LOCAL_PACKAGING_DIR=$LOCAL_PACKAGING_DIR"
@ -65,6 +66,8 @@ sudo chown -R meteor:meteor "$UPPER_DESTINATION_DIR"/
sudo chmod +x "$DESTINATION_DIR"/mongod_start_pre.sh
sudo chmod +x "$DESTINATION_DIR"/systemd_start.sh
sudo chmod +x "$DESTINATION_DIR"/systemd_start_frontend.sh
sudo cp $LOCAL_PACKAGING_DIR/workers-start.sh "$DESTINATION_DIR"/workers-start.sh
sudo chmod +x "$DESTINATION_DIR"/workers-start.sh

View File

@ -4,9 +4,11 @@ import handleBreakoutRoomsList from './handlers/breakoutList';
import handleUpdateTimeRemaining from './handlers/updateTimeRemaining';
import handleBreakoutClosed from './handlers/breakoutClosed';
import joinedUsersChanged from './handlers/joinedUsersChanged';
import userBreakoutChanged from '/imports/api/breakouts/server/handlers/userBreakoutChanged';
RedisPubSub.on('BreakoutRoomsListEvtMsg', handleBreakoutRoomsList);
RedisPubSub.on('BreakoutRoomJoinURLEvtMsg', handleBreakoutJoinURL);
RedisPubSub.on('BreakoutRoomsTimeRemainingUpdateEvtMsg', handleUpdateTimeRemaining);
RedisPubSub.on('BreakoutRoomEndedEvtMsg', handleBreakoutClosed);
RedisPubSub.on('UpdateBreakoutUsersEvtMsg', joinedUsersChanged);
RedisPubSub.on('ChangeUserBreakoutEvtMsg', userBreakoutChanged);

View File

@ -6,26 +6,26 @@ export default function userBreakoutChanged({ body }) {
check(body, Object);
const {
parentId,
meetingId,
userId,
fromBreakoutId,
toBreakoutId,
redirectToHtml5JoinUrl,
redirectToHtml5JoinURL,
} = body;
check(parentId, String);
check(meetingId, String);
check(userId, String);
check(fromBreakoutId, String);
check(toBreakoutId, String);
check(redirectToHtml5JoinUrl, String);
check(redirectToHtml5JoinURL, String);
const oldBreakoutSelector = {
parentMeetingId: parentId,
parentMeetingId: meetingId,
breakoutId: fromBreakoutId,
};
const newBreakoutSelector = {
parentMeetingId: parentId,
parentMeetingId: meetingId,
breakoutId: toBreakoutId,
};
@ -38,17 +38,24 @@ export default function userBreakoutChanged({ body }) {
const newModifier = {
$set: {
[`url_${userId}`]: {
redirectToHtml5JoinUrl,
redirectToHtml5JoinURL,
insertedTime: new Date().getTime(),
},
},
};
try {
const numberAffectedOld = Breakouts.update(oldBreakoutSelector, oldModifier);
const numberAffectedNew = Breakouts.update(newBreakoutSelector, newModifier);
let numberAffectedRows = 0;
if (numberAffectedOld && numberAffectedNew) {
if (oldBreakoutSelector.breakoutId !== '') {
numberAffectedRows += Breakouts.update(oldBreakoutSelector, oldModifier);
}
if (newBreakoutSelector.breakoutId !== '') {
numberAffectedRows += Breakouts.update(newBreakoutSelector, newModifier);
}
if (numberAffectedRows > 0) {
Logger.info(`Updated user breakout for userId=${userId}`);
}
} catch (err) {

View File

@ -1,4 +1,9 @@
import { PrometheusAgent, METRIC_NAMES } from '/imports/startup/server/prom-metrics/index.js'
// Round-trip time helper
export default function voidConnection() {
export default function voidConnection(previousRtt) {
if (previousRtt) {
PrometheusAgent.observe(METRIC_NAMES.METEOR_RTT, previousRtt/1000);
}
return 0;
}

View File

@ -137,7 +137,7 @@ Meteor.publish('meeting-time-remaining', timeRemainingPublish);
function notifications() {
const tokenValidation = AuthTokenValidation.findOne({ connectionId: this.connection.id });
if (tokenValidation || tokenValidation.validationStatus === ValidationStates.VALIDATED) {
if (tokenValidation && tokenValidation.validationStatus === ValidationStates.VALIDATED) {
notificationEmitter.on('notification', (notification) => {
const { meetingId, userId } = tokenValidation;
switch (notification.type) {

View File

@ -1,23 +1,26 @@
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import PresentationUploadToken from '/imports/api/presentation-upload-token';
import Presentations from '/imports/api/presentations';
export default function handlePresentationUploadTokenPass({ body, header }, meetingId) {
check(body, Object);
const { userId } = header;
const { podId, authzToken, filename } = body;
const { podId, authzToken, filename, tmpPresId } = body;
check(userId, String);
check(podId, String);
check(authzToken, String);
check(filename, String);
check(tmpPresId, String)
const selector = {
meetingId,
podId,
userId,
filename,
tmpPresId,
};
const modifier = {
@ -26,6 +29,7 @@ export default function handlePresentationUploadTokenPass({ body, header }, meet
userId,
filename,
authzToken,
tmpPresId,
failed: false,
used: false,
};

View File

@ -3,7 +3,7 @@ import { check } from 'meteor/check';
import { extractCredentials } from '/imports/api/common/server/helpers';
import Logger from '/imports/startup/server/logger';
export default function requestPresentationUploadToken(podId, filename) {
export default function requestPresentationUploadToken(podId, filename, tmpPresId) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'PresentationUploadTokenReqMsg';
@ -15,10 +15,12 @@ export default function requestPresentationUploadToken(podId, filename) {
check(requesterUserId, String);
check(podId, String);
check(filename, String);
check(tmpPresId, String);
const payload = {
podId,
filename,
tmpPresId
};
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);

View File

@ -4,7 +4,7 @@ import PresentationUploadToken from '/imports/api/presentation-upload-token';
import Logger from '/imports/startup/server/logger';
import AuthTokenValidation, { ValidationStates } from '/imports/api/auth-token-validation';
function presentationUploadToken(podId, filename) {
function presentationUploadToken(podId, filename, tmpPresId) {
const tokenValidation = AuthTokenValidation.findOne({ connectionId: this.connection.id });
if (!tokenValidation || tokenValidation.validationStatus !== ValidationStates.VALIDATED) {
@ -16,12 +16,14 @@ function presentationUploadToken(podId, filename) {
check(podId, String);
check(filename, String);
check(tmpPresId, String);
const selector = {
meetingId,
podId,
userId,
filename,
tmpPresId,
};
Logger.debug('Publishing PresentationUploadToken', { meetingId, userId });

View File

@ -34,6 +34,7 @@ export default function addPresentation(meetingId, podId, presentation) {
id: String,
name: String,
current: Boolean,
temporaryPresentationId: String,
pages: [
{
id: String,

View File

@ -26,10 +26,10 @@ export default function addUserPersistentData(user) {
locked: Boolean,
avatar: String,
clientType: String,
left: Boolean,
effectiveConnectionType: null,
});
const {
intId,
extId,

View File

@ -1,10 +1,10 @@
import RedisPubSub from '/imports/startup/server/redis';
import handleRemoveUser from './handlers/removeUser';
import handleUserJoined from './handlers/userJoined';
import handleUserLeftFlagUpdated from './handlers/userLeftFlagUpdated';
import handleValidateAuthToken from './handlers/validateAuthToken';
import handlePresenterAssigned from './handlers/presenterAssigned';
import handleEmojiStatus from './handlers/emojiStatus';
import handleUserEjected from './handlers/userEjected';
import handleChangeRole from './handlers/changeRole';
import handleUserPinChanged from './handlers/userPinChanged';
import handleUserInactivityInspect from './handlers/userInactivityInspect';
@ -14,7 +14,7 @@ RedisPubSub.on('UserJoinedMeetingEvtMsg', handleUserJoined);
RedisPubSub.on('UserLeftMeetingEvtMsg', handleRemoveUser);
RedisPubSub.on('ValidateAuthTokenRespMsg', handleValidateAuthToken);
RedisPubSub.on('UserEmojiChangedEvtMsg', handleEmojiStatus);
RedisPubSub.on('UserEjectedFromMeetingEvtMsg', handleUserEjected);
RedisPubSub.on('UserRoleChangedEvtMsg', handleChangeRole);
RedisPubSub.on('UserLeftFlagUpdatedEvtMsg', handleUserLeftFlagUpdated);
RedisPubSub.on('UserPinStateChangedEvtMsg', handleUserPinChanged);
RedisPubSub.on('UserInactivityInspectMsg', handleUserInactivityInspect);

View File

@ -8,5 +8,5 @@ export default function handleRemoveUser({ body }, meetingId) {
check(meetingId, String);
check(intId, String);
return removeUser(meetingId, intId);
return removeUser(body, meetingId);
}

View File

@ -1,8 +0,0 @@
import userEjected from '../modifiers/userEjected';
export default function handleEjectedUser({ header, body }) {
const { meetingId, userId } = header;
const { reasonCode } = body;
userEjected(meetingId, userId, reasonCode);
}

View File

@ -0,0 +1,13 @@
import { check } from 'meteor/check';
import userLeftFlag from '../modifiers/userLeftFlagUpdated';
export default function handleUserLeftFlag({ body }, meetingId) {
const user = body;
check(user, {
intId: String,
userLeftFlag: Boolean,
});
userLeftFlag(meetingId, user.intId, user.userLeftFlag);
}

View File

@ -62,6 +62,7 @@ export default function addUser(meetingId, userData) {
inactivityCheck: false,
responseDelay: 0,
loggedOut: false,
left: false,
...flat(user),
};

View File

@ -19,6 +19,7 @@ export default function createDummyUser(meetingId, userId, authToken) {
authToken,
clientType: 'HTML5',
validated: null,
left: false,
};
try {

View File

@ -6,6 +6,7 @@ import setloggedOutStatus from '/imports/api/users-persistent-data/server/modifi
import clearUserInfoForRequester from '/imports/api/users-infos/server/modifiers/clearUserInfoForRequester';
import ClientConnections from '/imports/startup/server/ClientConnections';
import UsersPersistentData from '/imports/api/users-persistent-data';
import userEjected from '/imports/api/users/server/modifiers/userEjected';
import VoiceUsers from '/imports/api/voice-users/';
const disconnectUser = (meetingId, userId) => {
@ -23,7 +24,8 @@ const disconnectUser = (meetingId, userId) => {
}
};
export default function removeUser(meetingId, userId) {
export default function removeUser(body, meetingId) {
const { intId: userId, reasonCode } = body;
check(meetingId, String);
check(userId, String);
@ -36,6 +38,10 @@ export default function removeUser(meetingId, userId) {
// we don't want to fully process the redis message in frontend
// since the backend is supposed to update Mongo
if ((process.env.BBB_HTML5_ROLE !== 'frontend')) {
if (body.eject) {
userEjected(meetingId, userId, reasonCode);
}
setloggedOutStatus(userId, meetingId, true);
VideoStreams.remove({ meetingId, userId });
@ -53,13 +59,12 @@ export default function removeUser(meetingId, userId) {
}
if (!process.env.BBB_HTML5_ROLE || process.env.BBB_HTML5_ROLE === 'frontend') {
//Wait for user removal and then kill user connections and sessions
// Wait for user removal and then kill user connections and sessions
const queryCurrentUser = Users.find(selector);
if (queryCurrentUser.count() === 0) {
disconnectUser(meetingId, userId);
} else {
let queryUserObserver = queryCurrentUser.observeChanges({
const queryUserObserver = queryCurrentUser.observeChanges({
removed() {
disconnectUser(meetingId, userId);
queryUserObserver.stop();

View File

@ -0,0 +1,24 @@
import Logger from '/imports/startup/server/logger';
import Users from '/imports/api/users';
export default function userLeftFlagUpdated(meetingId, userId, left) {
const selector = {
meetingId,
userId,
};
const modifier = {
$set: {
left,
},
};
try {
const numberAffected = Users.update(selector, modifier);
if (numberAffected) {
Logger.info(`Updated user ${userId} with left flag as ${left}`);
}
} catch (err) {
Logger.error(`Changed user role: ${err}`);
}
}

View File

@ -60,6 +60,7 @@ function users() {
{ meetingId },
],
intId: { $exists: true },
left: false,
};
const User = Users.findOne({ userId, meetingId }, { fields: { role: 1 } });

View File

@ -259,6 +259,11 @@ class Base extends Component {
}
if (ejected) {
if (meetingIsBreakout) {
window.close();
return null;
}
return (<MeetingEnded code="403" ejectedReason={ejectedReason} />);
}

View File

@ -144,7 +144,8 @@ Meteor.startup(() => {
Meteor.onMessage(event => {
const { method } = event;
if (method) {
PrometheusAgent.increment(METRIC_NAMES.METEOR_METHODS, { methodName: method });
const methodName = method.includes('stream-cursor') ? 'stream-cursor' : method;
PrometheusAgent.increment(METRIC_NAMES.METEOR_METHODS, { methodName });
}
});

View File

@ -1,5 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { createLogger, format, transports } from 'winston';
import WinstonPromTransport from './prom-metrics/winstonPromTransport';
const LOG_CONFIG = Meteor?.settings?.private?.serverLog || {};
const { level } = LOG_CONFIG;
@ -20,6 +21,10 @@ const Logger = createLogger({
handleExceptions: true,
level,
}),
// export error logs to prometheus
new WinstonPromTransport({
level: 'error',
}),
],
});

View File

@ -1,34 +1,60 @@
const {
Counter,
Gauge,
Histogram
} = require('prom-client');
const METRICS_PREFIX = 'html5_'
const METRIC_NAMES = {
METEOR_METHODS: 'meteorMethods',
METEOR_ERRORS_TOTAL: 'meteorErrorsTotal',
METEOR_RTT: 'meteorRtt',
REDIS_MESSAGE_QUEUE: 'redisMessageQueue',
REDIS_PAYLOAD_SIZE: 'redisPayloadSize',
REDIS_PROCESSING_TIME: 'redisProcessingTime'
}
const buildFrontendMetrics = () => {
return {
[METRIC_NAMES.METEOR_METHODS]: new Counter({
name: `${METRICS_PREFIX}meteor_methods`,
help: 'Total number of meteor methods processed in html5',
labelNames: ['methodName', 'role', 'instanceId'],
}),
}
}
const buildBackendMetrics = () => {
// TODO add relevant backend metrics
return {}
}
let METRICS;
const buildMetrics = () => {
if (METRICS == null) {
const isFrontend = (!process.env.BBB_HTML5_ROLE || process.env.BBB_HTML5_ROLE === 'frontend');
const isBackend = (!process.env.BBB_HTML5_ROLE || process.env.BBB_HTML5_ROLE === 'backend');
if (isFrontend) METRICS = buildFrontendMetrics();
if (isBackend) METRICS = { ...METRICS, ...buildBackendMetrics()}
METRICS = {
[METRIC_NAMES.METEOR_METHODS]: new Counter({
name: `${METRICS_PREFIX}meteor_methods`,
help: 'Total number of meteor methods processed in html5',
labelNames: ['methodName', 'role', 'instanceId'],
}),
[METRIC_NAMES.METEOR_ERRORS_TOTAL]: new Counter({
name: `${METRICS_PREFIX}meteor_errors_total`,
help: 'Total number of errors logs in meteor',
labelNames: ['errorMessage', 'role', 'instanceId'],
}),
[METRIC_NAMES.METEOR_RTT]: new Histogram({
name: `${METRICS_PREFIX}meteor_rtt_seconds`,
help: 'Round-trip time of meteor client-server connections in seconds',
buckets: [0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.75, 1, 1.5, 2, 2.5, 5],
labelNames: ['role', 'instanceId'],
}),
[METRIC_NAMES.REDIS_MESSAGE_QUEUE]: new Gauge({
name: `${METRICS_PREFIX}redis_message_queue`,
help: 'Message queue size in redis',
labelNames: ['meetingId', 'role', 'instanceId'],
}),
[METRIC_NAMES.REDIS_PAYLOAD_SIZE]: new Histogram({
name: `${METRICS_PREFIX}redis_payload_size`,
help: 'Redis events payload size',
labelNames: ['eventName', 'role', 'instanceId'],
}),
[METRIC_NAMES.REDIS_PROCESSING_TIME]: new Histogram({
name: `${METRICS_PREFIX}redis_processing_time`,
help: 'Redis events processing time in milliseconds',
labelNames: ['eventName', 'role', 'instanceId'],
}),
}
}
return METRICS;

View File

@ -81,6 +81,16 @@ class PrometheusScrapeAgent {
metric.set(labelsObject, value)
}
}
observe(metricName, value, labelsObject) {
if (!this.started) return;
const metric = this.metrics[metricName];
if (metric) {
labelsObject = { ...labelsObject, ...this.roleAndInstanceLabels };
metric.observe(labelsObject, value)
}
}
}
export default PrometheusScrapeAgent;

View File

@ -0,0 +1,19 @@
const Transport = require('winston-transport');
import { PrometheusAgent, METRIC_NAMES } from './index.js'
module.exports = class WinstonPromTransport extends Transport {
constructor(opts) {
super(opts);
}
log(info, callback) {
setImmediate(() => {
this.emit('logged', info);
});
PrometheusAgent.increment(METRIC_NAMES.METEOR_ERRORS_TOTAL, { errorMessage: info.message });
callback();
}
};

View File

@ -5,11 +5,13 @@ import { check } from 'meteor/check';
import Logger from './logger';
import Metrics from './metrics';
import queue from 'queue';
import { PrometheusAgent, METRIC_NAMES } from './prom-metrics/index.js'
// Fake meetingId used for messages that have no meetingId
const NO_MEETING_ID = '_';
const { queueMetrics } = Meteor.settings.private.redis.metrics;
const { collectRedisMetrics: PROM_METRICS_ENABLED } = Meteor.settings.private.prometheus;
const makeEnvelope = (channel, eventName, header, body, routing) => {
const envelope = {
@ -78,6 +80,16 @@ class MeetingMessageQueue {
}
const queueLength = this.queue.length;
if (PROM_METRICS_ENABLED) {
const dataLength = JSON.stringify(data).length;
const currentTimestamp = Date.now();
const processTime = currentTimestamp - beginHandleTimestamp;
PrometheusAgent.observe(METRIC_NAMES.REDIS_PROCESSING_TIME, processTime, { eventName });
PrometheusAgent.observe(METRIC_NAMES.REDIS_PAYLOAD_SIZE, dataLength, { eventName });
meetingId && PrometheusAgent.set(METRIC_NAMES.REDIS_MESSAGE_QUEUE, queueLength, { meetingId });
}
if (queueLength > 100) {
Logger.warn(`Redis: MeetingMessageQueue for meetingId=${meetingId} has queue size=${queueLength} `);
}

View File

@ -485,7 +485,6 @@ class BreakoutRoom extends PureComponent {
onUpdateBreakouts() {
const { users } = this.state;
const { sendInvitation } = this.props;
const leastOneUserIsValid = users.some((user) => user.from !== user.room);
if (!leastOneUserIsValid) {
@ -496,18 +495,20 @@ class BreakoutRoom extends PureComponent {
const { from, room } = user;
let { userId } = user;
if (from === room || room === 0) return;
if (from === room) return;
const toBreakout = this.getBreakoutBySequence(room);
const { breakoutId: toBreakoutId } = toBreakout;
let toBreakoutId = '';
if (room !== 0) {
const toBreakout = this.getBreakoutBySequence(room);
toBreakoutId = toBreakout.breakoutId;
}
if (!user.joined) return sendInvitation(toBreakoutId, userId);
userId = userId.split('-')[0];
const fromBreakout = this.getBreakoutBySequence(from);
const { breakoutId: fromBreakoutId } = fromBreakout;
if (toBreakout.freeJoin) return sendInvitation(toBreakoutId, userId);
let fromBreakoutId = '';
if (from !== 0) {
[userId] = userId.split('-');
const fromBreakout = this.getBreakoutBySequence(from);
fromBreakoutId = fromBreakout.breakoutId;
}
this.changeUserBreakout(fromBreakoutId, toBreakoutId, userId);
});
@ -561,6 +562,7 @@ class BreakoutRoom extends PureComponent {
.filter((user) => !stateUsersId.includes(user.userId))
.map((user) => ({
userId: user.userId,
extId: user.extId,
userName: user.name,
isModerator: user.role === ROLE_MODERATOR,
from: 0,
@ -703,7 +705,8 @@ class BreakoutRoom extends PureComponent {
}
populateWithLastBreakouts(lastBreakouts) {
const { getBreakoutUserWasIn, users, intl } = this.props;
const { getBreakoutUserWasIn, intl } = this.props;
const { users } = this.state;
const changedNames = [];
lastBreakouts.forEach((breakout) => {

View File

@ -153,6 +153,7 @@ class App extends Component {
settingsLayout,
isRTL,
hidePresentation,
autoSwapLayout,
} = this.props;
const { browserName } = browserInfo;
const { osName } = deviceInfo;
@ -166,7 +167,7 @@ class App extends Component {
layoutContextDispatch({
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
value: !hidePresentation,
value: !(autoSwapLayout || hidePresentation),
});
Modal.setAppElement('#app');

View File

@ -223,6 +223,7 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
Meteor.settings.public.presentation.restoreOnUpdate,
),
hidePresentation: getFromUserSettings('bbb_hide_presentation', LAYOUT_CONFIG.hidePresentation),
autoSwapLayout: getFromUserSettings('bbb_auto_swap_layout', LAYOUT_CONFIG.autoSwapLayout),
hideActionsBar: getFromUserSettings('bbb_hide_actions_bar', false),
isModalOpen: !!getModal(),
};

View File

@ -36,6 +36,10 @@ const intlMessages = defineMessages({
id: 'app.audio.audioSettings.speakerSourceLabel',
description: 'Label for speaker source',
},
testSpeakerLabel: {
id: 'app.audio.audioSettings.testSpeakerLabel',
description: 'Label for speaker source',
},
streamVolumeLabel: {
id: 'app.audio.audioSettings.microphoneStreamLabel',
description: 'Label for stream volume',
@ -136,9 +140,8 @@ class AudioSettings extends React.Component {
<Styled.Row>
<Styled.SpacedLeftCol>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<Styled.LabelSmall htmlFor="audioTest">
Test your speaker volume
{intl.formatMessage(intlMessages.testSpeakerLabel)}
<AudioTestContainer id="audioTest" />
</Styled.LabelSmall>
</Styled.SpacedLeftCol>

View File

@ -8,6 +8,7 @@ import AudioService from '../audio/service';
import VideoService from '../video-provider/service';
import { screenshareHasEnded } from '/imports/ui/components/screenshare/service';
import Styled from './styles';
import { Session } from 'meteor/session';
const intlMessages = defineMessages({
title: {
@ -129,6 +130,8 @@ class BreakoutJoinConfirmation extends Component {
extraInfo: { breakoutURL, isFreeJoin },
}, 'joining breakout room but redirected to about://blank');
}
Session.set('lastBreakoutIdOpened', selectValue);
window.open(url);
mountModal(null);
}

View File

@ -155,6 +155,8 @@ class BreakoutRoom extends PureComponent {
if (breakoutUrlData.redirectToHtml5JoinURL !== ''
&& breakoutUrlData.redirectToHtml5JoinURL !== prevBreakoutData.redirectToHtml5JoinURL) {
prevBreakoutData = breakoutUrlData;
Session.set('lastBreakoutIdOpened', requestedBreakoutId);
window.open(breakoutUrlData.redirectToHtml5JoinURL, '_blank');
_.delay(() => this.setState({ generated: true, waiting: false }), 1000);
}
@ -171,7 +173,6 @@ class BreakoutRoom extends PureComponent {
}
getBreakoutURL(breakoutId) {
Session.set('lastBreakoutOpened', breakoutId);
const { requestJoinURL, getBreakoutRoomUrl } = this.props;
const { waiting } = this.state;
const breakoutRoomUrlData = getBreakoutRoomUrl(breakoutId);
@ -187,6 +188,8 @@ class BreakoutRoom extends PureComponent {
}
if (breakoutRoomUrlData) {
Session.set('lastBreakoutIdOpened', breakoutId);
window.open(breakoutRoomUrlData.redirectToHtml5JoinURL, '_blank');
this.setState({ waiting: false, generated: false });
}

View File

@ -9,10 +9,10 @@ const BREAKOUT_MODAL_DELAY = 200;
const propTypes = {
mountModal: PropTypes.func.isRequired,
currentBreakoutUrlData: PropTypes.shape({
insertedTime: PropTypes.number.isRequired,
lastBreakoutReceived: PropTypes.shape({
breakoutUrlData: PropTypes.func.isRequired,
}),
breakoutUserIsIn: PropTypes.shape({
breakoutRoomsUserIsIn: PropTypes.shape({
sequence: PropTypes.number.isRequired,
}),
breakouts: PropTypes.arrayOf(PropTypes.shape({
@ -21,8 +21,8 @@ const propTypes = {
};
const defaultProps = {
currentBreakoutUrlData: undefined,
breakoutUserIsIn: undefined,
lastBreakoutReceived: undefined,
breakoutRoomsUserIsIn: undefined,
breakouts: [],
};
@ -54,30 +54,34 @@ class BreakoutRoomInvitation extends Component {
checkBreakouts(oldProps) {
const {
breakouts,
currentBreakoutUrlData,
getBreakoutByUrlData,
breakoutUserIsIn,
lastBreakoutReceived,
breakoutRoomsUserIsIn,
} = this.props;
const {
didSendBreakoutInvite,
} = this.state;
const hasBreakouts = breakouts.length > 0;
const hasBreakoutsAvailable = breakouts.length > 0;
if (hasBreakouts && !breakoutUserIsIn && BreakoutService.checkInviteModerators()) {
if (hasBreakoutsAvailable
&& !breakoutRoomsUserIsIn
&& BreakoutService.checkInviteModerators()) {
const freeJoinRooms = breakouts.filter((breakout) => breakout.freeJoin);
if (currentBreakoutUrlData) {
const breakoutRoom = getBreakoutByUrlData(currentBreakoutUrlData);
const currentInsertedTime = currentBreakoutUrlData.insertedTime;
const oldCurrentUrlData = oldProps.currentBreakoutUrlData || {};
const oldInsertedTime = oldCurrentUrlData.insertedTime;
if (currentInsertedTime !== oldInsertedTime) {
const lastBreakoutId = Session.get('lastBreakoutOpened');
if (breakoutRoom.breakoutId !== lastBreakoutId) {
this.inviteUserToBreakout(breakoutRoom);
}
if (lastBreakoutReceived) {
const lastBreakoutIdOpened = Session.get('lastBreakoutIdOpened');
const oldLastBktReceivedInsertedTime = (typeof oldProps.lastBreakoutReceived === 'object') ? oldProps.lastBreakoutReceived.breakoutUrlData.insertedTime : 0;
// check if user has a new invitation
if (lastBreakoutReceived.breakoutUrlData.insertedTime !== oldLastBktReceivedInsertedTime
// or check if user just left a room and was invited to another room in last 15 secs
|| (typeof oldProps.breakoutRoomsUserIsIn === 'object'
&& !breakoutRoomsUserIsIn
&& lastBreakoutReceived.breakoutId !== lastBreakoutIdOpened
&& lastBreakoutReceived.breakoutUrlData.insertedTime > (new Date().getTime()) - 15000)
) {
this.inviteUserToBreakout(lastBreakoutReceived);
}
} else if (freeJoinRooms.length > 0 && !didSendBreakoutInvite) {
const maxSeq = Math.max(...freeJoinRooms.map(((room) => room.sequence)));
@ -90,12 +94,13 @@ class BreakoutRoomInvitation extends Component {
}
}
if (!hasBreakouts && didSendBreakoutInvite) {
if (!hasBreakoutsAvailable && didSendBreakoutInvite) {
this.setState({ didSendBreakoutInvite: false });
}
}
inviteUserToBreakout(breakout) {
Session.set('lastBreakoutIdInvited', breakout.breakoutId);
const {
mountModal,
} = this.props;

View File

@ -15,7 +15,6 @@ const BreakoutRoomInvitationContainer = ({ isMeetingBreakout, ...props }) => {
export default withTracker(() => ({
isMeetingBreakout: AppService.meetingIsBreakout(),
breakouts: BreakoutService.getBreakoutsNoTime(),
getBreakoutByUrlData: BreakoutService.getBreakoutByUrlData,
currentBreakoutUrlData: BreakoutService.getBreakoutUrlByUserId(Auth.userID),
breakoutUserIsIn: BreakoutService.getBreakoutUserIsIn(Auth.userID),
lastBreakoutReceived: BreakoutService.getLastBreakoutByUserId(Auth.userID),
breakoutRoomsUserIsIn: BreakoutService.getBreakoutUserIsIn(Auth.userID),
}))(BreakoutRoomInvitationContainer);

View File

@ -122,26 +122,27 @@ const checkInviteModerators = () => {
const getBreakoutByUserId = (userId) =>
Breakouts.find(
{ [`url_${userId}`]: { $exists: true } },
{ fields: { timeRemaining: 0 } }
{ fields: { timeRemaining: 0 } },
).fetch();
const getBreakoutByUrlData = (breakoutUrlData) =>
Breakouts.findOne({ [`url_${Auth.userID}`]: breakoutUrlData });
const getWithBreakoutUrlData = (userId) => (breakoutsArray) => breakoutsArray
.map((breakout) => {
if (typeof breakout[`url_${userId}`] === 'object') {
return Object.assign(breakout, { breakoutUrlData: breakout[`url_${userId}`] });
}
return Object.assign(breakout, { breakoutUrlData: { insertedTime: 0 } });
})
.reduce((acc, urlDataArray) => acc.concat(urlDataArray), []);
const getUrlFromBreakouts = (userId) => (breakoutsArray) =>
breakoutsArray
.map((breakout) => breakout[`url_${userId}`])
.reduce((acc, urlDataArray) => acc.concat(urlDataArray), []);
const getLastBreakoutInserted = (breakoutURLArray) => breakoutURLArray.sort((a, b) => {
return a.breakoutUrlData.insertedTime - b.breakoutUrlData.insertedTime;
}).pop();
const getLastURLInserted = (breakoutURLArray) =>
breakoutURLArray.sort((a, b) => a.insertedTime - b.insertedTime).pop();
const getBreakoutUrlByUserId = (userId) =>
fp.pipe(
getBreakoutByUserId,
getUrlFromBreakouts(userId),
getLastURLInserted
)(userId);
const getLastBreakoutByUserId = (userId) => fp.pipe(
getBreakoutByUserId,
getWithBreakoutUrlData(userId),
getLastBreakoutInserted,
)(userId);
const getBreakouts = () =>
Breakouts.find({}, { sort: { sequence: 1 } }).fetch();
@ -205,8 +206,7 @@ export default {
transferToBreakout,
meetingId: () => Auth.meetingID,
amIModerator,
getBreakoutUrlByUserId,
getBreakoutByUrlData,
getLastBreakoutByUserId,
getBreakouts,
getBreakoutsNoTime,
getBreakoutUserIsIn,

View File

@ -68,8 +68,6 @@ const ChatAlert = (props) => {
unreadMessagesByChat,
intl,
layoutContextDispatch,
chatsTracker,
notify,
} = props;
const [unreadMessagesCount, setUnreadMessagesCount] = useState(0);
@ -105,35 +103,6 @@ const ChatAlert = (props) => {
}
}, [pushAlertEnabled]);
useEffect(() => {
const keys = Object.keys(chatsTracker);
keys.forEach((key) => {
if (chatsTracker[key]?.shouldNotify) {
if (audioAlertEnabled) {
AudioService.playAlertSound(`${Meteor.settings.public.app.cdn
+ Meteor.settings.public.app.basename
+ Meteor.settings.public.app.instanceId}`
+ '/resources/sounds/notify.mp3');
}
if (pushAlertEnabled) {
notify(
key === 'MAIN-PUBLIC-GROUP-CHAT'
? intl.formatMessage(intlMessages.publicChatMsg)
: intl.formatMessage(intlMessages.privateChatMsg),
'info',
'chat',
{ autoClose: 3000 },
<div>
<div style={{ fontWeight: 700 }}>{chatsTracker[key].lastSender}</div>
<div dangerouslySetInnerHTML={{ __html: chatsTracker[key].content }} />
</div>,
true,
);
}
}
});
}, [chatsTracker]);
useEffect(() => {
if (pushAlertEnabled) {
const alertsObject = unreadMessagesByChat;

View File

@ -1,7 +1,5 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import logger from '/imports/startup/client/logger';
import Auth from '/imports/ui/services/auth';
import ChatAlert from './component';
import { layoutSelect, layoutSelectInput, layoutDispatch } from '../../layout/context';
import { PANELS } from '../../layout/enums';
@ -18,15 +16,6 @@ const propTypes = {
pushAlertEnabled: PropTypes.bool.isRequired,
};
// custom hook for getting previous value
function usePrevious(value) {
const ref = React.useRef();
React.useEffect(() => {
ref.current = value;
});
return ref.current;
}
const ChatAlertContainer = (props) => {
const idChatOpen = layoutSelect((i) => i.idChatOpen);
const sidebarContent = layoutSelectInput((i) => i.sidebarContent);
@ -65,47 +54,9 @@ const ChatAlertContainer = (props) => {
})
: null;
const chatsTracker = {};
if (usingChatContext.chats) {
const chatsActive = Object.entries(usingChatContext.chats);
chatsActive.forEach((c) => {
try {
if (c[0] === idChat || (c[0] === 'MAIN-PUBLIC-GROUP-CHAT' && idChat === 'public')) {
chatsTracker[c[0]] = {};
if (c[1]?.posJoinMessages || c[1]?.messageGroups) {
const m = Object.entries(c[1]?.posJoinMessages || c[1]?.messageGroups);
const sameUserCount = m.filter((message) => message[1]?.sender === Auth.userID).length;
if (m[m.length - 1] && m[m.length - 1][1]?.sender !== Auth.userID) {
chatsTracker[c[0]].lastSender = users[Auth.meetingID][c[1]?.lastSender]?.name;
chatsTracker[c[0]].content = m[m.length - 1][1]?.message;
chatsTracker[c[0]].count = m?.length - sameUserCount;
}
}
}
} catch (e) {
logger.error({
logCode: 'chat_alert_component_error',
}, 'Error : ', e.error);
}
});
const prevTracker = usePrevious(chatsTracker);
if (prevTracker) {
const keys = Object.keys(prevTracker);
keys.forEach((key) => {
if (chatsTracker[key]?.count > (prevTracker[key]?.count || 0)) {
chatsTracker[key].shouldNotify = true;
}
});
}
}
return (
<ChatAlert
{...props}
chatsTracker={chatsTracker}
layoutContextDispatch={layoutContextDispatch}
unreadMessagesCountByChat={unreadMessagesCountByChat}
unreadMessagesByChat={unreadMessagesByChat}

View File

@ -71,7 +71,9 @@ class ConfirmationModal extends Component {
</Styled.Title>
</Styled.Header>
<Styled.Description>
<span dangerouslySetInnerHTML={{ __html: description }} />
<Styled.DescriptionText>
{description}
</Styled.DescriptionText>
{ hasCheckbox ? (
<label htmlFor="confirmationCheckbox" key="confirmation-checkbox">
<Styled.Checkbox

View File

@ -55,6 +55,10 @@ const Description = styled.div`
margin-bottom: ${jumboPaddingY};
`;
const DescriptionText = styled.span`
white-space: pre-line;
`;
const Checkbox = styled.input`
position: relative;
top: 0.134rem;
@ -85,6 +89,7 @@ export default {
Header,
Title,
Description,
DescriptionText,
Checkbox,
Footer,
ConfirmationButton,

View File

@ -29,6 +29,7 @@ const intlMessages = defineMessages({
});
let stats = -1;
let lastRtt = null;
const statsDep = new Tracker.Dependency();
let statsTimeout = null;
@ -111,11 +112,12 @@ const addConnectionStatus = (level, type, value) => {
const fetchRoundTripTime = () => {
const t0 = Date.now();
makeCall('voidConnection').then(() => {
makeCall('voidConnection', lastRtt).then(() => {
const tf = Date.now();
const rtt = tf - t0;
const event = new CustomEvent('socketstats', { detail: { rtt } });
window.dispatchEvent(event);
lastRtt = rtt;
});
};

View File

@ -46,7 +46,8 @@ class EndMeetingComponent extends PureComponent {
: intl.formatMessage(intlMessages.endMeetingNoUserDescription);
if (warnAboutUnsavedContentOnMeetingEnd) {
description += `<p>${intl.formatMessage(intlMessages.contentWarning)}</p>`;
// the double breakline it to put one empty line between the descriptions
description += `\n\n${intl.formatMessage(intlMessages.contentWarning)}`;
}
return (

View File

@ -75,6 +75,8 @@ class VideoPlayer extends Component {
volume: 1,
playbackRate: 1,
key: 0,
played:0,
loaded:0,
};
this.hideVolume = {
@ -301,12 +303,16 @@ class VideoPlayer extends Component {
}
}
handleOnProgress() {
handleOnProgress(data) {
const { mutedByEchoTest } = this.state;
const volume = this.getCurrentVolume();
const muted = this.getMuted();
const { played, loaded } = data;
this.setState({played, loaded});
if (!mutedByEchoTest) {
this.setState({ volume, muted });
}
@ -380,10 +386,10 @@ class VideoPlayer extends Component {
}
getMuted() {
const { mutedByEchoTest } = this.state;
const { mutedByEchoTest, muted } = this.state;
const intPlayer = this.player && this.player.getInternalPlayer();
return intPlayer && intPlayer.isMuted && intPlayer.isMuted() && !mutedByEchoTest;
return (intPlayer && intPlayer.isMuted && intPlayer.isMuted?.() && !mutedByEchoTest) || muted;
}
autoPlayBlockDetected() {
@ -549,7 +555,7 @@ class VideoPlayer extends Component {
const {
playing, playbackRate, mutedByEchoTest, autoPlayBlocked,
volume, muted, key, showHoverToolBar,
volume, muted, key, showHoverToolBar, played, loaded
} = this.state;
// This looks weird, but I need to get this nested player
@ -617,10 +623,19 @@ class VideoPlayer extends Component {
!isPresenter
? [
(
<Styled.HoverToolbar
toolbarStyle={toolbarStyle}
key="hover-toolbar-external-video"
>
<Styled.ProgressBar>
<Styled.Loaded
style={{ width: loaded * 100 + '%' }}
>
<Styled.Played
style={{ width: played * 100 / loaded + '%'}}
>
</Styled.Played>
</Styled.Loaded>
</Styled.ProgressBar>
),
(
<Styled.HoverToolbar key="hover-toolbar-external-video">
<VolumeSlider
hideVolume={this.hideVolume[playerName]}
volume={volume}

View File

@ -73,10 +73,36 @@ const HoverToolbar = styled.div`
`}
`;
const ProgressBar = styled.div`
display: none;
:hover > & {
display: block;
}
height: 5px;
width: 100%;
background-color: transparent;
`;
const Loaded = styled.div`
height: 100%;
background-color: gray;
`;
const Played = styled.div`
height: 100%;
background-color: #DF2721;
`;
export default {
VideoPlayerWrapper,
AutoPlayWarning,
VideoPlayer,
MobileControlsOverlay,
HoverToolbar,
ProgressBar,
Loaded,
Played,
};

View File

@ -174,14 +174,8 @@ class MeetingEnded extends PureComponent {
}
confirmRedirect() {
const {
selected,
} = this.state;
if (selected <= 0) {
if (meetingIsBreakout()) window.close();
if (allowRedirectToLogoutURL()) logoutRouteHandler();
}
if (meetingIsBreakout()) window.close();
if (allowRedirectToLogoutURL()) logoutRouteHandler();
}
getEndingMessage() {
@ -236,18 +230,22 @@ class MeetingEnded extends PureComponent {
dispatched: true,
});
if (allowRedirectToLogoutURL()) {
const FEEDBACK_WAIT_TIME = 500;
setTimeout(() => {
fetch(url, options)
.then(() => {
logoutRouteHandler();
})
.catch(() => {
logoutRouteHandler();
});
}, FEEDBACK_WAIT_TIME);
}
fetch(url, options).then(() => {
if (this.localUserRole === 'VIEWER') {
const REDIRECT_WAIT_TIME = 5000;
setTimeout(() => {
logoutRouteHandler();
}, REDIRECT_WAIT_TIME);
}
}).catch((e) => {
logger.warn({
logCode: 'user_feedback_not_sent_error',
extraInfo: {
errorName: e.name,
errorMessage: e.message,
},
}, `Unable to send feedback: ${e.message}`);
});
}
renderNoFeedback() {
@ -306,7 +304,6 @@ class MeetingEnded extends PureComponent {
const { intl, code, ejectedReason } = this.props;
const {
selected,
dispatched,
} = this.state;
const noRating = selected <= 0;
@ -343,16 +340,15 @@ class MeetingEnded extends PureComponent {
) : null}
</div>
) : null}
{noRating && allowRedirectToLogoutURL() ? (
{noRating ? (
<Styled.MeetingEndedButton
color="primary"
onClick={this.confirmRedirect}
onClick={() => this.setState({ dispatched: true })}
label={intl.formatMessage(intlMessage.buttonOkay)}
description={intl.formatMessage(intlMessage.confirmDesc)}
/>
) : null}
{!noRating && !dispatched ? (
{!noRating ? (
<Styled.MeetingEndedButton
color="primary"
onClick={this.sendFeedback}

View File

@ -3,12 +3,25 @@ import Styled from './styles';
const PadContent = ({
content,
}) => (
<Styled.Wrapper>
<Styled.Content>
<div dangerouslySetInnerHTML={{ __html: content }} />
</Styled.Content>
</Styled.Wrapper>
);
}) => {
const contentSplit = content.split('<body>');
const contentStyle = `
<body>
<style type="text/css">
body {
${Styled.contentText}
}
</style>
`;
const contentWithStyle = [contentSplit[0], contentStyle, contentSplit[1]].join('');
return (
<Styled.Wrapper>
<Styled.Iframe
title="shared notes viewing mode"
srcDoc={contentWithStyle}
/>
</Styled.Wrapper>
);
};
export default PadContent;

View File

@ -1,7 +1,6 @@
import styled from 'styled-components';
import {
colorGray,
colorGrayLightest,
} from '/imports/ui/stylesheets/styled-components/palette';
const Wrapper = styled.div`
@ -11,36 +10,41 @@ const Wrapper = styled.div`
width: 100%;
`;
const Content = styled.div`
font-family: Verdana, Arial, Helvetica, sans-serif;
font-size: 1.15rem;
color: ${colorGray};
border-top: 1px solid ${colorGrayLightest};
border-bottom: 1px solid ${colorGrayLightest};
bottom: 0;
box-sizing: border-box;
display: flex;
overflow-x: hidden;
overflow-wrap: break-word;
overflow-y: auto;
padding-top: 1rem;
position: absolute;
const contentText = `
font-family: Verdana, Arial, Helvetica, sans-serif;
font-size: 15px;
color: ${colorGray};
bottom: 0;
box-sizing: border-box;
display: flex;
overflow-x: hidden;
overflow-wrap: break-word;
word-break: break-all;
overflow-y: auto;
padding-top: 1rem;
position: absolute;
width: 100%;
top: 0;
[dir="ltr"] & {
padding-left: 1rem;
padding-right: .5rem;
}
[dir="rtl"] & {
padding-left: .5rem;
padding-right: 1rem;
}
`;
const Iframe = styled.iframe`
border-width: 0;
width: 100%;
top: 0;
white-space: pre-wrap;
[dir="ltr"] & {
padding-left: 1rem;
padding-right: .5rem;
}
[dir="rtl"] & {
padding-left: .5rem;
padding-right: 1rem;
}
`;
export default {
Wrapper,
Content,
Iframe,
contentText,
};

View File

@ -1,9 +1,11 @@
import _ from 'lodash';
import Pads, { PadsUpdates } from '/imports/api/pads';
import { makeCall } from '/imports/ui/services/api';
import Auth from '/imports/ui/services/auth';
import Settings from '/imports/ui/services/settings';
const PADS_CONFIG = Meteor.settings.public.pads;
const THROTTLE_TIMEOUT = 2000;
const getLang = () => {
const { locale } = Settings.application;
@ -38,6 +40,11 @@ const hasPad = (externalId) => {
const createSession = (externalId) => makeCall('createSession', externalId);
const throttledCreateSession = _.throttle(createSession, THROTTLE_TIMEOUT, {
leading: true,
trailing: false,
});
const buildPadURL = (padId) => {
if (padId) {
const params = getParams();
@ -89,7 +96,7 @@ export default {
getPadId,
createGroup,
hasPad,
createSession,
createSession: (externalId) => throttledCreateSession(externalId),
buildPadURL,
getRev,
getPadTail,

View File

@ -155,6 +155,8 @@ class Presentation extends PureComponent {
intl,
} = this.props;
const { presentationWidth, presentationHeight } = this.state;
const {
numCameras: prevNumCameras,
presentationBounds: prevPresentationBounds,
@ -232,7 +234,22 @@ class Presentation extends PureComponent {
}
}
if (presentationBounds !== prevPresentationBounds) this.onResize();
if ((presentationBounds !== prevPresentationBounds) ||
(!presentationWidth && !presentationHeight)) this.onResize();
} else if (slidePosition) {
const { width: currWidth, height: currHeight } = slidePosition;
layoutContextDispatch({
type: ACTIONS.SET_PRESENTATION_CURRENT_SLIDE_SIZE,
value: {
width: currWidth,
height: currHeight,
},
});
layoutContextDispatch({
type: ACTIONS.SET_PRESENTATION_NUM_CURRENT_SLIDE,
value: currentSlide.num,
});
}
}
@ -731,7 +748,7 @@ class Presentation extends PureComponent {
return (
<PresentationMenu
fullscreenRef={this.refPresentationContainer}
screenshotRef={this.getSvgRef()}
getScreenshotRef={this.getSvgRef}
elementName={intl.formatMessage(intlMessages.presentationLabel)}
elementId={fullscreenElementId}
toggleSwapLayout={MediaService.toggleSwapLayout}

Some files were not shown because too many files have changed in this diff Show More