Merge pull request #1 from gustavotrott/breakout-updated
Move user among brekout rooms - Backend part
This commit is contained in:
commit
9ec06177c1
1
akka-bbb-apps/.gitignore
vendored
1
akka-bbb-apps/.gitignore
vendored
@ -48,4 +48,5 @@ lib_managed/
|
||||
.cache
|
||||
bin/
|
||||
src/main/resources/
|
||||
.bsp/
|
||||
|
||||
|
13
akka-bbb-apps/deploy.sh
Executable file
13
akka-bbb-apps/deploy.sh
Executable 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'
|
@ -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
|
||||
|
@ -11,6 +11,7 @@ trait BreakoutApp2x extends BreakoutRoomCreatedMsgHdlr
|
||||
with CreateBreakoutRoomsCmdMsgHdlr
|
||||
with EndAllBreakoutRoomsMsgHdlr
|
||||
with UpdateBreakoutRoomsTimeMsgHdlr
|
||||
with ChangeUserBreakoutReqMsgHdlr
|
||||
with SendMessageToAllBreakoutRoomsMsgHdlr
|
||||
with SendMessageToBreakoutRoomInternalMsgHdlr
|
||||
with RequestBreakoutJoinURLReqMsgHdlr
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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] = {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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}")
|
||||
|
@ -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] = {
|
||||
|
@ -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 =>
|
||||
|
@ -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 = {
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
1
bbb-common-message/.gitignore
vendored
1
bbb-common-message/.gitignore
vendored
@ -53,4 +53,5 @@ akka-patterns-store/
|
||||
lib_managed/
|
||||
.cache
|
||||
bin/
|
||||
.bsp/
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
@ -115,6 +115,7 @@ case class PresentationConversionRequestReceivedSysMsg(
|
||||
case class PresentationConversionRequestReceivedSysMsgBody(
|
||||
podId: String,
|
||||
presentationId: String,
|
||||
temporaryPresentationId: String,
|
||||
current: Boolean,
|
||||
presName: String,
|
||||
downloadable: Boolean,
|
||||
@ -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
|
||||
|
@ -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)
|
||||
|
1
bbb-common-web/.gitignore
vendored
1
bbb-common-web/.gitignore
vendored
@ -53,4 +53,5 @@ akka-patterns-store/
|
||||
lib_managed/
|
||||
.cache
|
||||
bin/
|
||||
.bsp/
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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 {};
|
||||
}
|
@ -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
|
||||
|
@ -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)")
|
||||
|
@ -1,6 +1,3 @@
|
||||
package org.bigbluebutton.api.model.shared;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.ModeratorPasswordConstraint;
|
||||
|
||||
@ModeratorPasswordConstraint
|
||||
public class ModeratorPassword extends Password {}
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -19,16 +19,12 @@ 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 != 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;
|
||||
}
|
||||
|
@ -148,6 +148,7 @@ public class DocumentConversionServiceImp implements DocumentConversionService {
|
||||
pres.getPodId(),
|
||||
pres.getMeetingId(),
|
||||
pres.getId(),
|
||||
pres.getTemporaryPresentationId(),
|
||||
pres.getName(),
|
||||
pres.getAuthzToken(),
|
||||
pres.isDownloadable(),
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
22
bbb-learning-dashboard/deploy.sh
Executable file
22
bbb-learning-dashboard/deploy.sh
Executable 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'
|
32
bbb-learning-dashboard/run-dev.sh
Executable file
32
bbb-learning-dashboard/run-dev.sh
Executable 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
|
@ -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
|
||||
|
@ -1 +1 @@
|
||||
BIGBLUEBUTTON_RELEASE=2.5.0-alpha.6
|
||||
BIGBLUEBUTTON_RELEASE=2.5.0-beta.1
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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 });
|
||||
|
@ -34,6 +34,7 @@ export default function addPresentation(meetingId, podId, presentation) {
|
||||
id: String,
|
||||
name: String,
|
||||
current: Boolean,
|
||||
temporaryPresentationId: String,
|
||||
pages: [
|
||||
{
|
||||
id: String,
|
||||
|
@ -26,10 +26,10 @@ export default function addUserPersistentData(user) {
|
||||
locked: Boolean,
|
||||
avatar: String,
|
||||
clientType: String,
|
||||
left: Boolean,
|
||||
effectiveConnectionType: null,
|
||||
});
|
||||
|
||||
|
||||
const {
|
||||
intId,
|
||||
extId,
|
||||
|
@ -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);
|
||||
|
@ -8,5 +8,5 @@ export default function handleRemoveUser({ body }, meetingId) {
|
||||
check(meetingId, String);
|
||||
check(intId, String);
|
||||
|
||||
return removeUser(meetingId, intId);
|
||||
return removeUser(body, meetingId);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -62,6 +62,7 @@ export default function addUser(meetingId, userData) {
|
||||
inactivityCheck: false,
|
||||
responseDelay: 0,
|
||||
loggedOut: false,
|
||||
left: false,
|
||||
...flat(user),
|
||||
};
|
||||
|
||||
|
@ -19,6 +19,7 @@ export default function createDummyUser(meetingId, userId, authToken) {
|
||||
authToken,
|
||||
clientType: 'HTML5',
|
||||
validated: null,
|
||||
left: false,
|
||||
};
|
||||
|
||||
try {
|
||||
|
@ -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
|
||||
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();
|
||||
|
@ -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}`);
|
||||
}
|
||||
}
|
@ -60,6 +60,7 @@ function users() {
|
||||
{ meetingId },
|
||||
],
|
||||
intId: { $exists: true },
|
||||
left: false,
|
||||
};
|
||||
|
||||
const User = Users.findOne({ userId, meetingId }, { fields: { role: 1 } });
|
||||
|
@ -259,6 +259,11 @@ class Base extends Component {
|
||||
}
|
||||
|
||||
if (ejected) {
|
||||
if (meetingIsBreakout) {
|
||||
window.close();
|
||||
return null;
|
||||
}
|
||||
|
||||
return (<MeetingEnded code="403" ejectedReason={ejectedReason} />);
|
||||
}
|
||||
|
||||
|
@ -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 });
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
|
@ -1,34 +1,60 @@
|
||||
const {
|
||||
Counter,
|
||||
Gauge,
|
||||
Histogram
|
||||
} = require('prom-client');
|
||||
|
||||
const METRICS_PREFIX = 'html5_'
|
||||
const METRIC_NAMES = {
|
||||
METEOR_METHODS: 'meteorMethods',
|
||||
}
|
||||
|
||||
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 {}
|
||||
METEOR_ERRORS_TOTAL: 'meteorErrorsTotal',
|
||||
METEOR_RTT: 'meteorRtt',
|
||||
REDIS_MESSAGE_QUEUE: 'redisMessageQueue',
|
||||
REDIS_PAYLOAD_SIZE: 'redisPayloadSize',
|
||||
REDIS_PROCESSING_TIME: 'redisProcessingTime'
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
@ -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} `);
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
let toBreakoutId = '';
|
||||
if (room !== 0) {
|
||||
const toBreakout = this.getBreakoutBySequence(room);
|
||||
const { breakoutId: toBreakoutId } = toBreakout;
|
||||
toBreakoutId = toBreakout.breakoutId;
|
||||
}
|
||||
|
||||
if (!user.joined) return sendInvitation(toBreakoutId, userId);
|
||||
|
||||
userId = userId.split('-')[0];
|
||||
let fromBreakoutId = '';
|
||||
if (from !== 0) {
|
||||
[userId] = userId.split('-');
|
||||
const fromBreakout = this.getBreakoutBySequence(from);
|
||||
const { breakoutId: fromBreakoutId } = fromBreakout;
|
||||
|
||||
if (toBreakout.freeJoin) return sendInvitation(toBreakoutId, userId);
|
||||
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) => {
|
||||
|
@ -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');
|
||||
|
@ -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(),
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 });
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -122,25 +122,26 @@ 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 getUrlFromBreakouts = (userId) => (breakoutsArray) =>
|
||||
breakoutsArray
|
||||
.map((breakout) => breakout[`url_${userId}`])
|
||||
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 getLastURLInserted = (breakoutURLArray) =>
|
||||
breakoutURLArray.sort((a, b) => a.insertedTime - b.insertedTime).pop();
|
||||
const getLastBreakoutInserted = (breakoutURLArray) => breakoutURLArray.sort((a, b) => {
|
||||
return a.breakoutUrlData.insertedTime - b.breakoutUrlData.insertedTime;
|
||||
}).pop();
|
||||
|
||||
const getBreakoutUrlByUserId = (userId) =>
|
||||
fp.pipe(
|
||||
const getLastBreakoutByUserId = (userId) => fp.pipe(
|
||||
getBreakoutByUserId,
|
||||
getUrlFromBreakouts(userId),
|
||||
getLastURLInserted
|
||||
getWithBreakoutUrlData(userId),
|
||||
getLastBreakoutInserted,
|
||||
)(userId);
|
||||
|
||||
const getBreakouts = () =>
|
||||
@ -205,8 +206,7 @@ export default {
|
||||
transferToBreakout,
|
||||
meetingId: () => Auth.meetingID,
|
||||
amIModerator,
|
||||
getBreakoutUrlByUserId,
|
||||
getBreakoutByUrlData,
|
||||
getLastBreakoutByUserId,
|
||||
getBreakouts,
|
||||
getBreakoutsNoTime,
|
||||
getBreakoutUserIsIn,
|
||||
|
@ -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;
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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 (
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -174,15 +174,9 @@ class MeetingEnded extends PureComponent {
|
||||
}
|
||||
|
||||
confirmRedirect() {
|
||||
const {
|
||||
selected,
|
||||
} = this.state;
|
||||
|
||||
if (selected <= 0) {
|
||||
if (meetingIsBreakout()) window.close();
|
||||
if (allowRedirectToLogoutURL()) logoutRouteHandler();
|
||||
}
|
||||
}
|
||||
|
||||
getEndingMessage() {
|
||||
const { intl, code, endedReason } = this.props;
|
||||
@ -236,18 +230,22 @@ class MeetingEnded extends PureComponent {
|
||||
dispatched: true,
|
||||
});
|
||||
|
||||
if (allowRedirectToLogoutURL()) {
|
||||
const FEEDBACK_WAIT_TIME = 500;
|
||||
fetch(url, options).then(() => {
|
||||
if (this.localUserRole === 'VIEWER') {
|
||||
const REDIRECT_WAIT_TIME = 5000;
|
||||
setTimeout(() => {
|
||||
fetch(url, options)
|
||||
.then(() => {
|
||||
logoutRouteHandler();
|
||||
})
|
||||
.catch(() => {
|
||||
logoutRouteHandler();
|
||||
});
|
||||
}, FEEDBACK_WAIT_TIME);
|
||||
}, 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}
|
||||
|
@ -3,12 +3,25 @@ import Styled from './styles';
|
||||
|
||||
const PadContent = ({
|
||||
content,
|
||||
}) => (
|
||||
}) => {
|
||||
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.Content>
|
||||
<div dangerouslySetInnerHTML={{ __html: content }} />
|
||||
</Styled.Content>
|
||||
<Styled.Iframe
|
||||
title="shared notes viewing mode"
|
||||
srcDoc={contentWithStyle}
|
||||
/>
|
||||
</Styled.Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default PadContent;
|
||||
|
@ -1,7 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
colorGray,
|
||||
colorGrayLightest,
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
@ -11,23 +10,22 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
const contentText = `
|
||||
font-family: Verdana, Arial, Helvetica, sans-serif;
|
||||
font-size: 1.15rem;
|
||||
font-size: 15px;
|
||||
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;
|
||||
word-break: break-all;
|
||||
overflow-y: auto;
|
||||
padding-top: 1rem;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
white-space: pre-wrap;
|
||||
|
||||
|
||||
[dir="ltr"] & {
|
||||
padding-left: 1rem;
|
||||
@ -40,7 +38,13 @@ const Content = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const Iframe = styled.iframe`
|
||||
border-width: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export default {
|
||||
Wrapper,
|
||||
Content,
|
||||
Iframe,
|
||||
contentText,
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
|
@ -90,7 +90,7 @@ const PresentationMenu = (props) => {
|
||||
currentElement,
|
||||
currentGroup,
|
||||
fullscreenRef,
|
||||
screenshotRef,
|
||||
getScreenshotRef,
|
||||
handleToggleFullscreen,
|
||||
layoutContextDispatch,
|
||||
meetingName,
|
||||
@ -199,7 +199,7 @@ const PresentationMenu = (props) => {
|
||||
},
|
||||
});
|
||||
|
||||
toPng(screenshotRef, {
|
||||
toPng(getScreenshotRef(), {
|
||||
width: window.screen.width,
|
||||
height: window.screen.height,
|
||||
}).then((data) => {
|
||||
|
@ -554,8 +554,8 @@ class PresentationUploader extends Component {
|
||||
const { presentations: propPresentations } = this.props;
|
||||
const ids = new Set(propPresentations.map((d) => d.ID));
|
||||
const merged = [
|
||||
...propPresentations,
|
||||
...presentations.filter((d) => !ids.has(d.ID)),
|
||||
...propPresentations,
|
||||
];
|
||||
this.setState(
|
||||
{ presentations: merged },
|
||||
@ -1039,7 +1039,7 @@ class PresentationUploader extends Component {
|
||||
|
||||
let hasNewUpload = false;
|
||||
|
||||
presentations.map((item) => {
|
||||
presentations.forEach((item) => {
|
||||
if (item.id.indexOf(item.filename) !== -1 && item.upload.progress === 0) hasNewUpload = true;
|
||||
});
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user