Merge remote-tracking branch 'upstream/v3.0.x-release' into migrate-poll-creation
This commit is contained in:
commit
3859a7c854
2
.gitignore
vendored
2
.gitignore
vendored
@ -23,3 +23,5 @@ cache/*
|
||||
artifacts/*
|
||||
bbb-presentation-video.zip
|
||||
bbb-presentation-video
|
||||
bbb-graphql-actions-adapter-server/
|
||||
bigbluebutton-html5/public/locales/index.json
|
||||
|
@ -17,7 +17,7 @@ As such, we recommend that all administrators deploy 2.7 going forward. You'll
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you believe you have found a security vunerability in BigBlueButton please let us know directly by
|
||||
If you believe you have found a security vulnerability in BigBlueButton please let us know directly by
|
||||
- using GitHub's "Report a vulnerability" functionality on https://github.com/bigbluebutton/bigbluebutton/security/advisories
|
||||
- or e-mailing security@bigbluebutton.org with as much detail as possible.
|
||||
|
||||
|
@ -75,5 +75,6 @@ daemonUser in Linux := user
|
||||
daemonGroup in Linux := group
|
||||
|
||||
javaOptions in Universal ++= Seq("-J-Xms130m", "-J-Xmx256m", "-Dconfig.file=/etc/bigbluebutton/bbb-apps-akka.conf", "-Dlogback.configurationFile=conf/logback.xml")
|
||||
javaOptions in reStart ++= Seq("-Dconfig.file=/etc/bigbluebutton/bbb-apps-akka.conf", "-Dlogback.configurationFile=conf/logback.xml")
|
||||
|
||||
debianPackageDependencies in Debian ++= Seq("java17-runtime-headless", "bash")
|
||||
|
@ -16,7 +16,7 @@ object Dependencies {
|
||||
val pekkoHttpVersion = "1.0.0"
|
||||
val gson = "2.8.9"
|
||||
val jackson = "2.13.5"
|
||||
val logback = "1.2.11"
|
||||
val logback = "1.2.13"
|
||||
val quicklens = "1.7.5"
|
||||
val spray = "1.3.6"
|
||||
|
||||
|
@ -10,7 +10,7 @@ import org.bigbluebutton.core.bus._
|
||||
import org.bigbluebutton.core.pubsub.senders.ReceivedJsonMsgHandlerActor
|
||||
import org.bigbluebutton.core2.AnalyticsActor
|
||||
import org.bigbluebutton.core2.FromAkkaAppsMsgSenderActor
|
||||
import org.bigbluebutton.endpoint.redis.{AppsRedisSubscriberActor, ExportAnnotationsActor, GraphqlActionsActor, LearningDashboardActor, RedisRecorderActor}
|
||||
import org.bigbluebutton.endpoint.redis.{AppsRedisSubscriberActor, ExportAnnotationsActor, GraphqlConnectionsActor, LearningDashboardActor, RedisRecorderActor}
|
||||
import org.bigbluebutton.common2.bus.IncomingJsonMessageBus
|
||||
import org.bigbluebutton.service.{HealthzService, MeetingInfoActor, MeetingInfoService}
|
||||
|
||||
@ -67,9 +67,9 @@ object Boot extends App with SystemConfiguration {
|
||||
"LearningDashboardActor"
|
||||
)
|
||||
|
||||
val graphqlActionsActor = system.actorOf(
|
||||
GraphqlActionsActor.props(system),
|
||||
"GraphqlActionsActor"
|
||||
val graphqlConnectionsActor = system.actorOf(
|
||||
GraphqlConnectionsActor.props(system, eventBus, outGW),
|
||||
"GraphqlConnectionsActor"
|
||||
)
|
||||
|
||||
ClientSettings.loadClientSettingsFromFile()
|
||||
@ -89,8 +89,8 @@ object Boot extends App with SystemConfiguration {
|
||||
outBus2.subscribe(learningDashboardActor, outBbbMsgMsgChannel)
|
||||
bbbMsgBus.subscribe(learningDashboardActor, analyticsChannel)
|
||||
|
||||
eventBus.subscribe(graphqlActionsActor, meetingManagerChannel)
|
||||
bbbMsgBus.subscribe(graphqlActionsActor, analyticsChannel)
|
||||
eventBus.subscribe(graphqlConnectionsActor, meetingManagerChannel)
|
||||
bbbMsgBus.subscribe(graphqlConnectionsActor, analyticsChannel)
|
||||
|
||||
val bbbActor = system.actorOf(BigBlueButtonActor.props(system, eventBus, bbbMsgBus, outGW, healthzService), "bigbluebutton-actor")
|
||||
eventBus.subscribe(bbbActor, meetingManagerChannel)
|
||||
|
@ -38,6 +38,9 @@ object ClientSettings extends SystemConfiguration {
|
||||
Map[String, Object]()
|
||||
}
|
||||
)
|
||||
|
||||
//Remove `:private` once it's used only by Meteor internal configs
|
||||
clientSettingsFromFile -= "private"
|
||||
}
|
||||
|
||||
def getClientSettingsWithOverride(clientSettingsOverrideJson: String): Map[String, Object] = {
|
||||
@ -56,7 +59,7 @@ object ClientSettings extends SystemConfiguration {
|
||||
getConfigPropertyValueByPath(map, path) match {
|
||||
case Some(configValue: Int) => configValue
|
||||
case _ =>
|
||||
logger.debug("Config `{}` not found.", path)
|
||||
logger.debug(s"Config `$path` with type Integer not found in clientSettings.")
|
||||
alternativeValue
|
||||
}
|
||||
}
|
||||
@ -65,7 +68,7 @@ object ClientSettings extends SystemConfiguration {
|
||||
getConfigPropertyValueByPath(map, path) match {
|
||||
case Some(configValue: String) => configValue
|
||||
case _ =>
|
||||
logger.debug("Config `{}` not found.", path)
|
||||
logger.debug(s"Config `$path` with type String not found in clientSettings.")
|
||||
alternativeValue
|
||||
}
|
||||
}
|
||||
@ -74,7 +77,7 @@ object ClientSettings extends SystemConfiguration {
|
||||
getConfigPropertyValueByPath(map, path) match {
|
||||
case Some(configValue: Boolean) => configValue
|
||||
case _ =>
|
||||
logger.debug("Config `{}` not found.", path)
|
||||
logger.debug(s"Config `$path` with type Boolean found in clientSettings.")
|
||||
alternativeValue
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ trait SystemConfiguration {
|
||||
lazy val bbbWebPort = Try(config.getInt("services.bbbWebPort")).getOrElse(8888)
|
||||
lazy val bbbWebAPI = Try(config.getString("services.bbbWebAPI")).getOrElse("localhost")
|
||||
lazy val bbbWebSharedSecret = Try(config.getString("services.sharedSecret")).getOrElse("changeme")
|
||||
lazy val checkSumAlgorithmForBreakouts = Try(config.getString("services.checkSumAlgorithmForBreakouts")).getOrElse("sha256")
|
||||
lazy val bbbWebModeratorPassword = Try(config.getString("services.moderatorPassword")).getOrElse("changeme")
|
||||
lazy val bbbWebViewerPassword = Try(config.getString("services.viewerPassword")).getOrElse("changeme")
|
||||
lazy val keysExpiresInSec = Try(config.getInt("redis.keyExpiry")).getOrElse(14 * 86400) // 14 days
|
||||
|
@ -14,6 +14,7 @@ import org.bigbluebutton.SystemConfiguration
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.db.{ DatabaseConnection, MeetingDAO }
|
||||
import org.bigbluebutton.core.domain.MeetingEndReason
|
||||
import org.bigbluebutton.core.running.RunningMeeting
|
||||
import org.bigbluebutton.core.util.ColorPicker
|
||||
import org.bigbluebutton.core2.RunningMeetings
|
||||
@ -57,6 +58,9 @@ class BigBlueButtonActor(
|
||||
override def preStart() {
|
||||
bbbMsgBus.subscribe(self, meetingManagerChannel)
|
||||
DatabaseConnection.initialize()
|
||||
|
||||
//Terminate all previous meetings, as they will not function following the akka-apps restart
|
||||
MeetingDAO.setAllMeetingsEnded(MeetingEndReason.ENDED_DUE_TO_SERVICE_INTERRUPTION, "system")
|
||||
}
|
||||
|
||||
override def postStop() {
|
||||
@ -83,6 +87,7 @@ class BigBlueButtonActor(
|
||||
case m: ValidateConnAuthTokenSysMsg => handleValidateConnAuthTokenSysMsg(m)
|
||||
case _: UserGraphqlConnectionEstablishedSysMsg => //Ignore
|
||||
case _: UserGraphqlConnectionClosedSysMsg => //Ignore
|
||||
case _: CheckGraphqlMiddlewareAlivePongSysMsg => //Ignore
|
||||
case _ => log.warning("Cannot handle " + msg.envelope.name)
|
||||
}
|
||||
}
|
||||
@ -189,9 +194,10 @@ class BigBlueButtonActor(
|
||||
context.stop(m.actorRef)
|
||||
}
|
||||
|
||||
MeetingDAO.delete(msg.meetingId)
|
||||
// MeetingDAO.delete(msg.meetingId)
|
||||
// MeetingDAO.setMeetingEnded(msg.meetingId)
|
||||
// Removing the meeting is enough, all other tables has "ON DELETE CASCADE"
|
||||
// UserDAO.deleteAllFromMeeting(msg.meetingId)
|
||||
// UserDAO.softDeleteAllFromMeeting(msg.meetingId)
|
||||
// MeetingRecordingDAO.updateStopped(msg.meetingId, "")
|
||||
|
||||
//Remove ColorPicker idx of the meeting
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.bigbluebutton.core.api
|
||||
|
||||
import org.bigbluebutton.core.apps.users.UserEstablishedGraphqlConnectionInternalMsgHdlr
|
||||
import org.bigbluebutton.core.domain.{ BreakoutUser, BreakoutVoiceUser }
|
||||
import spray.json.JsObject
|
||||
case class InMessageHeader(name: String)
|
||||
@ -126,6 +127,18 @@ case class SetPresenterInDefaultPodInternalMsg(presenterId: String) extends InMe
|
||||
*/
|
||||
case class CaptureSharedNotesReqInternalMsg(breakoutId: String, filename: String) extends InMessage
|
||||
|
||||
/**
|
||||
* Sent by GraphqlActionsActor to inform MeetingActor that user disconnected
|
||||
* @param userId
|
||||
*/
|
||||
case class UserClosedAllGraphqlConnectionsInternalMsg(userId: String) extends InMessage
|
||||
|
||||
/**
|
||||
* Sent by GraphqlActionsActor to inform MeetingActor that user came back from disconnection
|
||||
* @param userId
|
||||
*/
|
||||
case class UserEstablishedGraphqlConnectionInternalMsg(userId: String) extends InMessage
|
||||
|
||||
// DeskShare
|
||||
case class DeskShareStartedRequest(conferenceName: String, callerId: String, callerIdName: String) extends InMessage
|
||||
case class DeskShareStoppedRequest(conferenceName: String, callerId: String, callerIdName: String) extends InMessage
|
||||
|
@ -4,6 +4,7 @@ import org.bigbluebutton.core.running.MeetingActor
|
||||
import java.net.URLEncoder
|
||||
import scala.collection.SortedSet
|
||||
import org.apache.commons.codec.digest.DigestUtils
|
||||
import org.bigbluebutton.SystemConfiguration
|
||||
|
||||
trait BreakoutApp2x extends BreakoutRoomCreatedMsgHdlr
|
||||
with BreakoutRoomsListMsgHdlr
|
||||
@ -26,7 +27,7 @@ trait BreakoutApp2x extends BreakoutRoomCreatedMsgHdlr
|
||||
|
||||
}
|
||||
|
||||
object BreakoutRoomsUtil {
|
||||
object BreakoutRoomsUtil extends SystemConfiguration {
|
||||
def createMeetingIds(id: String, index: Int): (String, String) = {
|
||||
val timeStamp = System.currentTimeMillis()
|
||||
val externalHash = DigestUtils.sha1Hex(id.concat("-").concat(timeStamp.toString()).concat("-").concat(index.toString()))
|
||||
@ -48,7 +49,13 @@ object BreakoutRoomsUtil {
|
||||
//checksum() -- Return a checksum based on SHA-1 digest
|
||||
//
|
||||
def checksum(s: String): String = {
|
||||
DigestUtils.sha256Hex(s);
|
||||
checkSumAlgorithmForBreakouts match {
|
||||
case "sha1" => DigestUtils.sha1Hex(s);
|
||||
case "sha256" => DigestUtils.sha256Hex(s);
|
||||
case "sha384" => DigestUtils.sha384Hex(s);
|
||||
case "sha512" => DigestUtils.sha512Hex(s);
|
||||
case _ => DigestUtils.sha256Hex(s); // default
|
||||
}
|
||||
}
|
||||
|
||||
def calculateChecksum(apiCall: String, baseString: String, sharedSecret: String): String = {
|
||||
|
@ -30,7 +30,7 @@ trait EjectUserFromBreakoutInternalMsgHdlr {
|
||||
)
|
||||
|
||||
//TODO inform reason
|
||||
UserDAO.delete(registeredUser.id)
|
||||
UserDAO.softDelete(registeredUser.id)
|
||||
|
||||
// send a system message to force disconnection
|
||||
Sender.sendDisconnectClientSysMsg(msg.breakoutId, registeredUser.id, msg.ejectedBy, msg.reasonCode, outGW)
|
||||
|
@ -6,6 +6,7 @@ import org.bigbluebutton.core.running.OutMsgRouter
|
||||
import org.bigbluebutton.core2.MeetingStatus2x
|
||||
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
|
||||
import org.bigbluebutton.core.db.LayoutDAO
|
||||
import org.bigbluebutton.core2.message.senders.{ MsgBuilder }
|
||||
|
||||
trait BroadcastLayoutMsgHdlr extends RightsManagementTrait {
|
||||
this: LayoutApp2x =>
|
||||
@ -60,5 +61,18 @@ trait BroadcastLayoutMsgHdlr extends RightsManagementTrait {
|
||||
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
|
||||
outGW.send(msgEvent)
|
||||
|
||||
if (body.pushLayout) {
|
||||
val notifyEvent = MsgBuilder.buildNotifyUserInMeetingEvtMsg(
|
||||
fromUserId,
|
||||
liveMeeting.props.meetingProp.intId,
|
||||
"info",
|
||||
"user",
|
||||
"app.layoutUpdate.label",
|
||||
"Notification to when the presenter changes size of cams",
|
||||
Vector()
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ trait MakePresentationDownloadReqMsgHdlr extends RightsManagementTrait {
|
||||
def buildNewPresFileAvailable(annotatedFileURI: String, originalFileURI: String, convertedFileURI: String,
|
||||
presId: String, fileStateType: String): NewPresFileAvailableMsg = {
|
||||
val header = BbbClientMsgHeader(NewPresFileAvailableMsg.NAME, "not-used", "not-used")
|
||||
val body = NewPresFileAvailableMsgBody(annotatedFileURI, originalFileURI, convertedFileURI, presId, fileStateType)
|
||||
val body = NewPresFileAvailableMsgBody(annotatedFileURI, originalFileURI, convertedFileURI, presId, fileStateType, "")
|
||||
|
||||
NewPresFileAvailableMsg(header, body)
|
||||
}
|
||||
@ -160,7 +160,7 @@ trait MakePresentationDownloadReqMsgHdlr extends RightsManagementTrait {
|
||||
val pages: List[Int] = m.body.pages // Desired presentation pages for export
|
||||
val pagesRange: List[Int] = if (allPages) (1 to pageCount).toList else pages
|
||||
|
||||
val exportJob: ExportJob = new ExportJob(jobId, JobTypes.DOWNLOAD, "annotated_slides", presId, presLocation, allPages, pagesRange, meetingId, "");
|
||||
val exportJob: ExportJob = new ExportJob(jobId, JobTypes.DOWNLOAD, currentPres.get.name, "annotated_slides", presId, presLocation, allPages, pagesRange, meetingId, "");
|
||||
val storeAnnotationPages: List[PresentationPageForExport] = getPresentationPagesForExport(pagesRange, pageCount, presId, currentPres, liveMeeting);
|
||||
|
||||
val isPresentationOriginalOrConverted = m.body.fileStateType == "Original" || m.body.fileStateType == "Converted"
|
||||
@ -226,7 +226,7 @@ trait MakePresentationDownloadReqMsgHdlr extends RightsManagementTrait {
|
||||
val currentPage: PresentationPage = PresentationInPod.getCurrentPage(currentPres.get).get
|
||||
val pagesRange: List[Int] = if (allPages) (1 to pageCount).toList else List(currentPage.num)
|
||||
|
||||
val exportJob: ExportJob = ExportJob(jobId, JobTypes.CAPTURE_PRESENTATION, filename, presId, presLocation, allPages, pagesRange, parentMeetingId, presentationUploadToken)
|
||||
val exportJob: ExportJob = ExportJob(jobId, JobTypes.CAPTURE_PRESENTATION, filename, filename, presId, presLocation, allPages, pagesRange, parentMeetingId, presentationUploadToken)
|
||||
val storeAnnotationPages: List[PresentationPageForExport] = getPresentationPagesForExport(pagesRange, pageCount, presId, currentPres, liveMeeting);
|
||||
|
||||
val annotationCount: Int = storeAnnotationPages.map(_.annotations.size).sum
|
||||
@ -252,11 +252,10 @@ trait MakePresentationDownloadReqMsgHdlr extends RightsManagementTrait {
|
||||
liveMeeting.props.meetingProp.intId, m.body.presId
|
||||
)
|
||||
|
||||
//TODO let frontend choose the name in favor of internationalization
|
||||
if (m.body.fileStateType == "Annotated") {
|
||||
val presentationDownloadInfo = Map(
|
||||
"fileURI" -> m.body.annotatedFileURI,
|
||||
"filename" -> "annotated_slides.pdf"
|
||||
"filename" -> m.body.fileName
|
||||
)
|
||||
ChatMessageDAO.insertSystemMsg(liveMeeting.props.meetingProp.intId, GroupChatApp.MAIN_PUBLIC_CHAT, "", GroupChatMessageType.PRESENTATION, presentationDownloadInfo, "")
|
||||
} else if (m.body.fileStateType == "Converted") {
|
||||
@ -295,7 +294,7 @@ trait MakePresentationDownloadReqMsgHdlr extends RightsManagementTrait {
|
||||
|
||||
bus.outGW.send(buildPresentationUploadTokenSysPubMsg(m.body.parentMeetingId, userId, presentationUploadToken, filename, presentationId))
|
||||
|
||||
val exportJob = new ExportJob(jobId, JobTypes.CAPTURE_NOTES, filename, m.body.padId, "", true, List(), m.body.parentMeetingId, presentationUploadToken)
|
||||
val exportJob = new ExportJob(jobId, JobTypes.CAPTURE_NOTES, filename, filename, m.body.padId, "", true, List(), m.body.parentMeetingId, presentationUploadToken)
|
||||
val job = buildStoreExportJobInRedisSysMsg(exportJob, liveMeeting)
|
||||
|
||||
bus.outGW.send(job)
|
||||
|
@ -15,7 +15,7 @@ trait SetPresenterInDefaultPodInternalMsgHdlr {
|
||||
msg: SetPresenterInDefaultPodInternalMsg, state: MeetingState2x,
|
||||
liveMeeting: LiveMeeting, bus: MessageBus
|
||||
): MeetingState2x = {
|
||||
// Swith presenter as default presenter pod has changed.
|
||||
// Switch presenter as default presenter pod has changed.
|
||||
log.info("Presenter pod change will trigger a presenter change")
|
||||
SetPresenterInPodActionHandler.handleAction(state, liveMeeting, bus.outGW, "", PresentationPod.DEFAULT_PRESENTATION_POD, msg.presenterId)
|
||||
}
|
||||
|
@ -1,63 +0,0 @@
|
||||
package org.bigbluebutton.core.apps.users
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
|
||||
import org.bigbluebutton.core.models.{ UserState, Users2x }
|
||||
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
|
||||
import org.bigbluebutton.core2.MeetingStatus2x
|
||||
import org.bigbluebutton.SystemConfiguration
|
||||
import scala.util.Random
|
||||
|
||||
trait SelectRandomViewerReqMsgHdlr extends RightsManagementTrait {
|
||||
this: UsersApp =>
|
||||
|
||||
val outGW: OutMsgRouter
|
||||
|
||||
def handleSelectRandomViewerReqMsg(msg: SelectRandomViewerReqMsg): Unit = {
|
||||
log.debug("Received SelectRandomViewerReqMsg {}", SelectRandomViewerReqMsg)
|
||||
|
||||
def broadcastEvent(msg: SelectRandomViewerReqMsg, users: Vector[String], choice: String): Unit = {
|
||||
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, liveMeeting.props.meetingProp.intId, msg.header.userId)
|
||||
val envelope = BbbCoreEnvelope(SelectRandomViewerRespMsg.NAME, routing)
|
||||
val header = BbbClientMsgHeader(SelectRandomViewerRespMsg.NAME, liveMeeting.props.meetingProp.intId, msg.header.userId)
|
||||
|
||||
val body = SelectRandomViewerRespMsgBody(msg.header.userId, users, choice)
|
||||
val event = SelectRandomViewerRespMsg(header, body)
|
||||
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
if (permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
val reason = "No permission to select random user."
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)
|
||||
} else {
|
||||
val users = Users2x.getRandomlyPickableUsers(liveMeeting.users2x, false)
|
||||
|
||||
val usersPicked = Users2x.getRandomlyPickableUsers(liveMeeting.users2x, reduceDuplicatedPick)
|
||||
|
||||
val randNum = new scala.util.Random
|
||||
var pickedUser = if (usersPicked.size == 0) "" else usersPicked(randNum.nextInt(usersPicked.size)).intId
|
||||
|
||||
if (reduceDuplicatedPick) {
|
||||
if (usersPicked.size <= 1) {
|
||||
// Initialise the exemption
|
||||
val usersToUnexempt = Users2x.findAll(liveMeeting.users2x)
|
||||
usersToUnexempt foreach { u =>
|
||||
Users2x.setUserExempted(liveMeeting.users2x, u.intId, false)
|
||||
}
|
||||
if (usersPicked.size == 0) {
|
||||
// Pick again
|
||||
val usersRepicked = Users2x.getRandomlyPickableUsers(liveMeeting.users2x, reduceDuplicatedPick)
|
||||
pickedUser = if (usersRepicked.size == 0) "" else usersRepicked(randNum.nextInt(usersRepicked.size)).intId
|
||||
Users2x.setUserExempted(liveMeeting.users2x, pickedUser, true)
|
||||
}
|
||||
} else if (usersPicked.size > 1) {
|
||||
Users2x.setUserExempted(liveMeeting.users2x, pickedUser, true)
|
||||
}
|
||||
}
|
||||
val userIds = users.map { case (v) => v.intId }
|
||||
broadcastEvent(msg, userIds, pickedUser)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package org.bigbluebutton.core.apps.users
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.apps.RightsManagementTrait
|
||||
import org.bigbluebutton.core.db.UserConnectionStatusDAO
|
||||
import org.bigbluebutton.core.models.{ UserState, Users2x }
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
|
||||
|
||||
trait UserConnectionAliveReqMsgHdlr extends RightsManagementTrait {
|
||||
this: UsersApp =>
|
||||
|
||||
val liveMeeting: LiveMeeting
|
||||
val outGW: OutMsgRouter
|
||||
|
||||
def handleUserConnectionAliveReqMsg(msg: UserConnectionAliveReqMsg): Unit = {
|
||||
log.info("handleUserConnectionAliveReqMsg: userId={}", msg.body.userId)
|
||||
|
||||
for {
|
||||
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId)
|
||||
} yield {
|
||||
UserConnectionStatusDAO.updateUserAlive(user.intId)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package org.bigbluebutton.core.apps.users
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.apps.RightsManagementTrait
|
||||
import org.bigbluebutton.core.db.UserConnectionStatusDAO
|
||||
import org.bigbluebutton.core.models.{ UserState, Users2x }
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
|
||||
|
||||
trait UserConnectionUpdateRttReqMsgHdlr extends RightsManagementTrait {
|
||||
this: UsersApp =>
|
||||
|
||||
val liveMeeting: LiveMeeting
|
||||
val outGW: OutMsgRouter
|
||||
|
||||
def handleUserConnectionUpdateRttReqMsg(msg: UserConnectionUpdateRttReqMsg): Unit = {
|
||||
log.info("handleUserConnectionUpdateRttReqMsg: networkRttInMs={} userId={}", msg.body.networkRttInMs, msg.body.userId)
|
||||
|
||||
for {
|
||||
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId)
|
||||
} yield {
|
||||
UserConnectionStatusDAO.updateUserRtt(user.intId, msg.body.networkRttInMs)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package org.bigbluebutton.core.apps.users
|
||||
|
||||
import org.bigbluebutton.core.api.UserEstablishedGraphqlConnectionInternalMsg
|
||||
import org.bigbluebutton.core.domain.MeetingState2x
|
||||
import org.bigbluebutton.core.models.Users2x
|
||||
import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting, MeetingActor, OutMsgRouter }
|
||||
|
||||
trait UserEstablishedGraphqlConnectionInternalMsgHdlr extends HandlerHelpers {
|
||||
this: MeetingActor =>
|
||||
|
||||
val liveMeeting: LiveMeeting
|
||||
val outGW: OutMsgRouter
|
||||
|
||||
def handleUserEstablishedGraphqlConnectionInternalMsg(msg: UserEstablishedGraphqlConnectionInternalMsg, state: MeetingState2x): MeetingState2x = {
|
||||
log.info("Received user established a graphql connection. user {} meetingId={}", msg.userId, liveMeeting.props.meetingProp.intId)
|
||||
Users2x.findWithIntId(liveMeeting.users2x, msg.userId) match {
|
||||
case Some(reconnectingUser) =>
|
||||
if (reconnectingUser.userLeftFlag.left) {
|
||||
log.info("Resetting flag that user left meeting. user {}", msg.userId)
|
||||
sendUserLeftFlagUpdatedEvtMsg(outGW, liveMeeting, msg.userId, leftFlag = false)
|
||||
Users2x.resetUserLeftFlag(liveMeeting.users2x, msg.userId)
|
||||
}
|
||||
state
|
||||
case None =>
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ trait UserJoinMeetingAfterReconnectReqMsgHdlr extends HandlerHelpers with UserJo
|
||||
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)
|
||||
sendUserLeftFlagUpdatedEvtMsg(outGW, liveMeeting, msg.body.userId, leftFlag = false)
|
||||
Users2x.resetUserLeftFlag(liveMeeting.users2x, msg.body.userId)
|
||||
}
|
||||
state
|
||||
|
@ -71,7 +71,7 @@ trait UserJoinMeetingReqMsgHdlr extends HandlerHelpers {
|
||||
|
||||
private def resetUserLeftFlag(msg: UserJoinMeetingReqMsg) = {
|
||||
log.info("Resetting flag that user left meeting. user {}", msg.body.userId)
|
||||
sendUserLeftFlagUpdatedEvtMsg(outGW, liveMeeting, msg.body.userId, false)
|
||||
sendUserLeftFlagUpdatedEvtMsg(outGW, liveMeeting, msg.body.userId, leftFlag = false)
|
||||
Users2x.resetUserLeftFlag(liveMeeting.users2x, msg.body.userId)
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.bigbluebutton.core.apps.users
|
||||
|
||||
import org.bigbluebutton.common2.msgs.UserLeaveReqMsg
|
||||
import org.bigbluebutton.core.api.{ UserClosedAllGraphqlConnectionsInternalMsg }
|
||||
import org.bigbluebutton.core.domain.MeetingState2x
|
||||
import org.bigbluebutton.core.models.{ RegisteredUsers, Users2x }
|
||||
import org.bigbluebutton.core.running.{ HandlerHelpers, MeetingActor, OutMsgRouter }
|
||||
@ -12,23 +13,33 @@ trait UserLeaveReqMsgHdlr extends HandlerHelpers {
|
||||
val outGW: OutMsgRouter
|
||||
|
||||
def handleUserLeaveReqMsg(msg: UserLeaveReqMsg, state: MeetingState2x): MeetingState2x = {
|
||||
Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId) match {
|
||||
handleUserLeaveReq(msg.body.userId, msg.header.meetingId, msg.body.loggedOut, state)
|
||||
}
|
||||
|
||||
def handleUserClosedAllGraphqlConnectionsInternalMsg(msg: UserClosedAllGraphqlConnectionsInternalMsg, state: MeetingState2x): MeetingState2x = {
|
||||
log.info("Received user closed all graphql connections. user {} meetingId={}", msg.userId, liveMeeting.props.meetingProp.intId)
|
||||
|
||||
handleUserLeaveReq(msg.userId, liveMeeting.props.meetingProp.intId, loggedOut = false, state)
|
||||
}
|
||||
|
||||
def handleUserLeaveReq(userId: String, meetingId: String, loggedOut: Boolean, state: MeetingState2x): MeetingState2x = {
|
||||
Users2x.findWithIntId(liveMeeting.users2x, userId) match {
|
||||
case Some(reconnectingUser) =>
|
||||
log.info("Received user left meeting. user {} meetingId={}", msg.body.userId, msg.header.meetingId)
|
||||
log.info("Received user left meeting. user {} meetingId={}", userId, meetingId)
|
||||
if (!reconnectingUser.userLeftFlag.left) {
|
||||
log.info("Setting user left flag. user {} meetingId={}", msg.body.userId, msg.header.meetingId)
|
||||
log.info("Setting user left flag. user {} meetingId={}", userId, meetingId)
|
||||
// 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)
|
||||
sendUserLeftFlagUpdatedEvtMsg(outGW, liveMeeting, userId, leftFlag = true)
|
||||
|
||||
Users2x.setUserLeftFlag(liveMeeting.users2x, msg.body.userId)
|
||||
Users2x.setUserLeftFlag(liveMeeting.users2x, userId)
|
||||
}
|
||||
if (msg.body.loggedOut) {
|
||||
log.info("Setting user logged out flag. user {} meetingId={}", msg.body.userId, msg.header.meetingId)
|
||||
if (loggedOut) {
|
||||
log.info("Setting user logged out flag. user {} meetingId={}", userId, meetingId)
|
||||
|
||||
for {
|
||||
ru <- RegisteredUsers.findWithUserId(msg.body.userId, liveMeeting.registeredUsers)
|
||||
ru <- RegisteredUsers.findWithUserId(userId, liveMeeting.registeredUsers)
|
||||
} yield {
|
||||
RegisteredUsers.setUserLoggedOutFlag(liveMeeting.registeredUsers, ru)
|
||||
Sender.sendForceUserGraphqlReconnectionSysMsg(liveMeeting.props.meetingProp.intId, ru.id, ru.sessionToken, "user_loggedout", outGW)
|
||||
@ -39,4 +50,5 @@ trait UserLeaveReqMsgHdlr extends HandlerHelpers {
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -164,10 +164,11 @@ class UsersApp(
|
||||
with RecordAndClearPreviousMarkersCmdMsgHdlr
|
||||
with SendRecordingTimerInternalMsgHdlr
|
||||
with GetRecordingStatusReqMsgHdlr
|
||||
with SelectRandomViewerReqMsgHdlr
|
||||
with AssignPresenterReqMsgHdlr
|
||||
with ChangeUserPinStateReqMsgHdlr
|
||||
with ChangeUserMobileFlagReqMsgHdlr
|
||||
with UserConnectionAliveReqMsgHdlr
|
||||
with UserConnectionUpdateRttReqMsgHdlr
|
||||
with ChangeUserReactionEmojiReqMsgHdlr
|
||||
with ChangeUserRaiseHandReqMsgHdlr
|
||||
with ChangeUserAwayReqMsgHdlr
|
||||
|
@ -58,7 +58,6 @@ trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration {
|
||||
avatar = "",
|
||||
color = userColor,
|
||||
clientType = if (isDialInUser) "dial-in-user" else "",
|
||||
pickExempted = false,
|
||||
userLeftFlag = UserLeftFlag(false, 0)
|
||||
)
|
||||
Users2x.add(liveMeeting.users2x, newUser)
|
||||
|
@ -40,7 +40,7 @@ trait UserLeftVoiceConfEvtMsgHdlr {
|
||||
UsersApp.guestWaitingLeft(liveMeeting, user.intId, outGW)
|
||||
}
|
||||
Users2x.remove(liveMeeting.users2x, user.intId)
|
||||
UserDAO.delete(user.intId)
|
||||
UserDAO.softDelete(user.intId)
|
||||
VoiceApp.removeUserFromVoiceConf(liveMeeting, outGW, msg.body.voiceUserId)
|
||||
}
|
||||
|
||||
|
@ -52,11 +52,11 @@ trait SendWhiteboardAnnotationsPubMsgHdlr extends RightsManagementTrait {
|
||||
)
|
||||
|
||||
if (isUserOneOfPermited || isUserAmongPresenters) {
|
||||
println("============= Printing Sanitized annotations ============")
|
||||
for (annotation <- msg.body.annotations) {
|
||||
printAnnotationInfo(annotation)
|
||||
}
|
||||
println("============= Printed Sanitized annotations ============")
|
||||
// println("============= Printing Sanitized annotations ============")
|
||||
// for (annotation <- msg.body.annotations) {
|
||||
// printAnnotationInfo(annotation)
|
||||
// }
|
||||
// println("============= Printed Sanitized annotations ============")
|
||||
val annotations = sendWhiteboardAnnotations(msg.body.whiteboardId, msg.header.userId, msg.body.annotations, liveMeeting, isUserAmongPresenters, isUserModerator)
|
||||
broadcastEvent(msg, msg.body.whiteboardId, annotations, msg.body.html5InstanceId)
|
||||
} else {
|
||||
|
@ -20,8 +20,16 @@ case class MeetingDbModel(
|
||||
presentationUploadExternalUrl: String,
|
||||
learningDashboardAccessToken: String,
|
||||
logoutUrl: String,
|
||||
customLogoUrl: Option[String],
|
||||
bannerText: Option[String],
|
||||
bannerColor: Option[String],
|
||||
createdTime: Long,
|
||||
durationInSeconds: Int
|
||||
durationInSeconds: Int,
|
||||
endWhenNoModerator: Boolean,
|
||||
endWhenNoModeratorDelayInMinutes: Int,
|
||||
endedAt: Option[java.sql.Timestamp],
|
||||
endedReasonCode: Option[String],
|
||||
endedBy: Option[String],
|
||||
)
|
||||
|
||||
class MeetingDbTableDef(tag: Tag) extends Table[MeetingDbModel](tag, None, "meeting") {
|
||||
@ -38,8 +46,16 @@ class MeetingDbTableDef(tag: Tag) extends Table[MeetingDbModel](tag, None, "meet
|
||||
presentationUploadExternalUrl,
|
||||
learningDashboardAccessToken,
|
||||
logoutUrl,
|
||||
customLogoUrl,
|
||||
bannerText,
|
||||
bannerColor,
|
||||
createdTime,
|
||||
durationInSeconds
|
||||
durationInSeconds,
|
||||
endWhenNoModerator,
|
||||
endWhenNoModeratorDelayInMinutes,
|
||||
endedAt,
|
||||
endedReasonCode,
|
||||
endedBy
|
||||
) <> (MeetingDbModel.tupled, MeetingDbModel.unapply)
|
||||
val meetingId = column[String]("meetingId", O.PrimaryKey)
|
||||
val extId = column[String]("extId")
|
||||
@ -53,8 +69,16 @@ class MeetingDbTableDef(tag: Tag) extends Table[MeetingDbModel](tag, None, "meet
|
||||
val presentationUploadExternalUrl = column[String]("presentationUploadExternalUrl")
|
||||
val learningDashboardAccessToken = column[String]("learningDashboardAccessToken")
|
||||
val logoutUrl = column[String]("logoutUrl")
|
||||
val customLogoUrl = column[Option[String]]("customLogoUrl")
|
||||
val bannerText = column[Option[String]]("bannerText")
|
||||
val bannerColor = column[Option[String]]("bannerColor")
|
||||
val createdTime = column[Long]("createdTime")
|
||||
val durationInSeconds = column[Int]("durationInSeconds")
|
||||
val endWhenNoModerator = column[Boolean]("endWhenNoModerator")
|
||||
val endWhenNoModeratorDelayInMinutes = column[Int]("endWhenNoModeratorDelayInMinutes")
|
||||
val endedAt = column[Option[java.sql.Timestamp]]("endedAt")
|
||||
val endedReasonCode = column[Option[String]]("endedReasonCode")
|
||||
val endedBy = column[Option[String]]("endedBy")
|
||||
}
|
||||
|
||||
object MeetingDAO {
|
||||
@ -74,28 +98,45 @@ object MeetingDAO {
|
||||
presentationUploadExternalUrl = meetingProps.meetingProp.presentationUploadExternalUrl,
|
||||
learningDashboardAccessToken = meetingProps.password.learningDashboardAccessToken,
|
||||
logoutUrl = meetingProps.systemProps.logoutUrl,
|
||||
customLogoUrl = meetingProps.systemProps.customLogoURL match {
|
||||
case "" => None
|
||||
case logoUrl => Some(logoUrl)
|
||||
},
|
||||
bannerText = meetingProps.systemProps.bannerText match {
|
||||
case "" => None
|
||||
case bannerText => Some(bannerText)
|
||||
},
|
||||
bannerColor = meetingProps.systemProps.bannerColor match {
|
||||
case "" => None
|
||||
case bannerColor => Some(bannerColor)
|
||||
},
|
||||
createdTime = meetingProps.durationProps.createdTime,
|
||||
durationInSeconds = meetingProps.durationProps.duration * 60
|
||||
durationInSeconds = meetingProps.durationProps.duration * 60,
|
||||
endWhenNoModerator = meetingProps.durationProps.endWhenNoModerator,
|
||||
endWhenNoModeratorDelayInMinutes = meetingProps.durationProps.endWhenNoModeratorDelayInMinutes,
|
||||
endedAt = None,
|
||||
endedReasonCode = None,
|
||||
endedBy = None
|
||||
)
|
||||
)
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => {
|
||||
DatabaseConnection.logger.debug(s"$rowsAffected row(s) inserted in Meeting table!")
|
||||
ChatDAO.insert(meetingProps.meetingProp.intId, GroupChatApp.createDefaultPublicGroupChat())
|
||||
MeetingUsersPoliciesDAO.insert(meetingProps.meetingProp.intId, meetingProps.usersProp)
|
||||
MeetingLockSettingsDAO.insert(meetingProps.meetingProp.intId, meetingProps.lockSettingsProps)
|
||||
MeetingMetadataDAO.insert(meetingProps.meetingProp.intId, meetingProps.metadataProp)
|
||||
MeetingRecordingPoliciesDAO.insert(meetingProps.meetingProp.intId, meetingProps.recordProp)
|
||||
MeetingVoiceDAO.insert(meetingProps.meetingProp.intId, meetingProps.voiceProp)
|
||||
MeetingWelcomeDAO.insert(meetingProps.meetingProp.intId, meetingProps.welcomeProp)
|
||||
MeetingGroupDAO.insert(meetingProps.meetingProp.intId, meetingProps.groups)
|
||||
MeetingBreakoutDAO.insert(meetingProps.meetingProp.intId, meetingProps.breakoutProps)
|
||||
TimerDAO.insert(meetingProps.meetingProp.intId)
|
||||
LayoutDAO.insert(meetingProps.meetingProp.intId, meetingProps.usersProp.meetingLayout)
|
||||
MeetingClientSettingsDAO.insert(meetingProps.meetingProp.intId, JsonUtils.mapToJson(clientSettings))
|
||||
}
|
||||
case Failure(e) => DatabaseConnection.logger.error(s"Error inserting Meeting: $e")
|
||||
case Success(rowsAffected) => {
|
||||
DatabaseConnection.logger.debug(s"$rowsAffected row(s) inserted in Meeting table!")
|
||||
ChatDAO.insert(meetingProps.meetingProp.intId, GroupChatApp.createDefaultPublicGroupChat())
|
||||
MeetingUsersPoliciesDAO.insert(meetingProps.meetingProp.intId, meetingProps.usersProp)
|
||||
MeetingLockSettingsDAO.insert(meetingProps.meetingProp.intId, meetingProps.lockSettingsProps)
|
||||
MeetingMetadataDAO.insert(meetingProps.meetingProp.intId, meetingProps.metadataProp)
|
||||
MeetingRecordingPoliciesDAO.insert(meetingProps.meetingProp.intId, meetingProps.recordProp)
|
||||
MeetingVoiceDAO.insert(meetingProps.meetingProp.intId, meetingProps.voiceProp)
|
||||
MeetingWelcomeDAO.insert(meetingProps.meetingProp.intId, meetingProps.welcomeProp)
|
||||
MeetingGroupDAO.insert(meetingProps.meetingProp.intId, meetingProps.groups)
|
||||
MeetingBreakoutDAO.insert(meetingProps.meetingProp.intId, meetingProps.breakoutProps)
|
||||
TimerDAO.insert(meetingProps.meetingProp.intId)
|
||||
LayoutDAO.insert(meetingProps.meetingProp.intId, meetingProps.usersProp.meetingLayout)
|
||||
MeetingClientSettingsDAO.insert(meetingProps.meetingProp.intId, JsonUtils.mapToJson(clientSettings))
|
||||
}
|
||||
case Failure(e) => DatabaseConnection.logger.error(s"Error inserting Meeting: $e")
|
||||
}
|
||||
}
|
||||
|
||||
def updateMeetingDurationByParentMeeting(parentMeetingId: String, newDurationInSeconds: Int) = {
|
||||
@ -110,9 +151,9 @@ object MeetingDAO {
|
||||
.map(u => u.durationInSeconds)
|
||||
.update(newDurationInSeconds)
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated durationInSeconds on Meeting table")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating durationInSeconds on Meeting: $e")
|
||||
}
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated durationInSeconds on Meeting table")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating durationInSeconds on Meeting: $e")
|
||||
}
|
||||
}
|
||||
|
||||
def delete(meetingId: String) = {
|
||||
@ -121,9 +162,55 @@ object MeetingDAO {
|
||||
.filter(_.meetingId === meetingId)
|
||||
.delete
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"Meeting ${meetingId} deleted")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error deleting meeting ${meetingId}: $e")
|
||||
}
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"Meeting ${meetingId} deleted")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error deleting meeting ${meetingId}: $e")
|
||||
}
|
||||
}
|
||||
|
||||
def setMeetingEnded(meetingId: String, endedReasonCode: String, endedBy: String) = {
|
||||
|
||||
UserDAO.softDeleteAllFromMeeting(meetingId)
|
||||
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[MeetingDbTableDef]
|
||||
.filter(_.meetingId === meetingId)
|
||||
.map(a => (a.endedAt, a.endedReasonCode, a.endedBy))
|
||||
.update(
|
||||
(
|
||||
Some(new java.sql.Timestamp(System.currentTimeMillis())),
|
||||
Some(endedReasonCode),
|
||||
endedBy match {
|
||||
case "" => None
|
||||
case c => Some(c)
|
||||
}
|
||||
)
|
||||
)
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated endedAt=now() on Meeting table!")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating endedAt=now() Meeting: $e")
|
||||
}
|
||||
}
|
||||
|
||||
def setAllMeetingsEnded(endedReasonCode: String, endedBy: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[MeetingDbTableDef]
|
||||
.filter(_.endedAt.isEmpty)
|
||||
.map(a => (a.endedAt, a.endedReasonCode, a.endedBy))
|
||||
.update(
|
||||
(
|
||||
Some(new java.sql.Timestamp(System.currentTimeMillis())),
|
||||
Some(endedReasonCode),
|
||||
endedBy match {
|
||||
case "" => None
|
||||
case c => Some(c)
|
||||
}
|
||||
)
|
||||
)
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated all-meetings endedAt=now() on Meeting table!")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating all-meetings endedAt=now() on Meeting table: $e")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -14,8 +14,8 @@ case class TimerDbModel(
|
||||
active: Boolean,
|
||||
time: Long,
|
||||
accumulated: Long,
|
||||
startedAt: Long,
|
||||
endedAt: Long,
|
||||
startedOn: Long,
|
||||
endedOn: Long,
|
||||
songTrack: String,
|
||||
)
|
||||
|
||||
@ -26,10 +26,10 @@ class TimerDbTableDef(tag: Tag) extends Table[TimerDbModel](tag, None, "timer")
|
||||
val active = column[Boolean]("active")
|
||||
val time = column[Long]("time")
|
||||
val accumulated = column[Long]("accumulated")
|
||||
val startedAt = column[Long]("startedAt")
|
||||
val endedAt = column[Long]("endedAt")
|
||||
val startedOn = column[Long]("startedOn")
|
||||
val endedOn = column[Long]("endedOn")
|
||||
val songTrack = column[String]("songTrack")
|
||||
override def * = (meetingId, stopwatch, running, active, time, accumulated, startedAt, endedAt, songTrack) <> (TimerDbModel.tupled, TimerDbModel.unapply)
|
||||
override def * = (meetingId, stopwatch, running, active, time, accumulated, startedOn, endedOn, songTrack) <> (TimerDbModel.tupled, TimerDbModel.unapply)
|
||||
}
|
||||
|
||||
object TimerDAO {
|
||||
@ -43,8 +43,8 @@ object TimerDAO {
|
||||
active = false,
|
||||
time = 300000,
|
||||
accumulated = 0,
|
||||
startedAt = 0,
|
||||
endedAt = 0,
|
||||
startedOn = 0,
|
||||
endedOn = 0,
|
||||
songTrack = "noTrack",
|
||||
)
|
||||
)
|
||||
@ -58,7 +58,7 @@ object TimerDAO {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[TimerDbTableDef]
|
||||
.filter(_.meetingId === meetingId)
|
||||
.map(t => (t.stopwatch, t.running, t.active, t.time, t.accumulated, t.startedAt, t.endedAt, t.songTrack))
|
||||
.map(t => (t.stopwatch, t.running, t.active, t.time, t.accumulated, t.startedOn, t.endedOn, t.songTrack))
|
||||
.update((getStopwatch(timerModel), getRunning(timerModel), getIsActive(timerModel), getTime(timerModel), getAccumulated(timerModel), getStartedAt(timerModel), getEndedAt(timerModel), getTrack(timerModel))
|
||||
)
|
||||
).onComplete {
|
||||
|
@ -5,18 +5,22 @@ import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.util.{ Failure, Success }
|
||||
|
||||
case class UserConnectionStatusDbModel(
|
||||
userId: String,
|
||||
meetingId: String,
|
||||
connectionAliveAt: Option[java.sql.Timestamp]
|
||||
userId: String,
|
||||
meetingId: String,
|
||||
connectionAliveAt: Option[java.sql.Timestamp],
|
||||
userClientResponseAt: Option[java.sql.Timestamp],
|
||||
networkRttInMs: Option[Double]
|
||||
)
|
||||
|
||||
class UserConnectionStatusDbTableDef(tag: Tag) extends Table[UserConnectionStatusDbModel](tag, None, "user_connectionStatus") {
|
||||
override def * = (
|
||||
userId, meetingId, connectionAliveAt
|
||||
userId, meetingId, connectionAliveAt, userClientResponseAt, networkRttInMs
|
||||
) <> (UserConnectionStatusDbModel.tupled, UserConnectionStatusDbModel.unapply)
|
||||
val userId = column[String]("userId", O.PrimaryKey)
|
||||
val meetingId = column[String]("meetingId")
|
||||
val connectionAliveAt = column[Option[java.sql.Timestamp]]("connectionAliveAt")
|
||||
val userClientResponseAt = column[Option[java.sql.Timestamp]]("userClientResponseAt")
|
||||
val networkRttInMs = column[Option[Double]]("networkRttInMs")
|
||||
}
|
||||
|
||||
object UserConnectionStatusDAO {
|
||||
@ -27,7 +31,9 @@ object UserConnectionStatusDAO {
|
||||
UserConnectionStatusDbModel(
|
||||
userId = userId,
|
||||
meetingId = meetingId,
|
||||
connectionAliveAt = None
|
||||
connectionAliveAt = None,
|
||||
userClientResponseAt = None,
|
||||
networkRttInMs = None
|
||||
)
|
||||
)
|
||||
).onComplete {
|
||||
@ -36,4 +42,28 @@ object UserConnectionStatusDAO {
|
||||
}
|
||||
}
|
||||
|
||||
def updateUserAlive(userId: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[UserConnectionStatusDbTableDef]
|
||||
.filter(_.userId === userId)
|
||||
.map(t => (t.connectionAliveAt))
|
||||
.update(Some(new java.sql.Timestamp(System.currentTimeMillis())))
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated connectionAliveAt on UserConnectionStatus table!")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating connectionAliveAt on UserConnectionStatus: $e")
|
||||
}
|
||||
}
|
||||
|
||||
def updateUserRtt(userId: String, networkRttInMs: Double) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[UserConnectionStatusDbTableDef]
|
||||
.filter(_.userId === userId)
|
||||
.map(t => (t.networkRttInMs, t.userClientResponseAt))
|
||||
.update((Some(networkRttInMs), Some(new java.sql.Timestamp(System.currentTimeMillis()))))
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated networkRttInMs on UserConnectionStatus table!")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating networkRttInMs on UserConnectionStatus: $e")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -135,7 +135,7 @@ object UserDAO {
|
||||
}
|
||||
|
||||
|
||||
def delete(intId: String) = {
|
||||
def softDelete(intId: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[UserDbTableDef]
|
||||
.filter(_.userId === intId)
|
||||
@ -147,7 +147,19 @@ object UserDAO {
|
||||
}
|
||||
}
|
||||
|
||||
def deleteAllFromMeeting(meetingId: String) = {
|
||||
def softDeleteAllFromMeeting(meetingId: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[UserDbTableDef]
|
||||
.filter(_.meetingId === meetingId)
|
||||
.map(u => (u.loggedOut))
|
||||
.update((true))
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated loggedOut=true on user table!")
|
||||
case Failure(e) => DatabaseConnection.logger.error(s"Error updating loggedOut=true user: $e")
|
||||
}
|
||||
}
|
||||
|
||||
def permanentlyDeleteAllFromMeeting(meetingId: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[UserDbTableDef]
|
||||
.filter(_.meetingId === meetingId)
|
||||
|
@ -9,32 +9,35 @@ import scala.util.{Failure, Success }
|
||||
case class UserGraphqlConnectionDbModel (
|
||||
graphqlConnectionId: Option[Int],
|
||||
sessionToken: String,
|
||||
middlewareUID: String,
|
||||
middlewareConnectionId: String,
|
||||
stablishedAt: java.sql.Timestamp,
|
||||
establishedAt: java.sql.Timestamp,
|
||||
closedAt: Option[java.sql.Timestamp],
|
||||
)
|
||||
|
||||
class UserGraphqlConnectionDbTableDef(tag: Tag) extends Table[UserGraphqlConnectionDbModel](tag, None, "user_graphqlConnection") {
|
||||
override def * = (
|
||||
graphqlConnectionId, sessionToken, middlewareConnectionId, stablishedAt, closedAt
|
||||
graphqlConnectionId, sessionToken, middlewareUID, middlewareConnectionId, establishedAt, closedAt
|
||||
) <> (UserGraphqlConnectionDbModel.tupled, UserGraphqlConnectionDbModel.unapply)
|
||||
val graphqlConnectionId = column[Option[Int]]("graphqlConnectionId", O.PrimaryKey, O.AutoInc)
|
||||
val sessionToken = column[String]("sessionToken")
|
||||
val middlewareUID = column[String]("middlewareUID")
|
||||
val middlewareConnectionId = column[String]("middlewareConnectionId")
|
||||
val stablishedAt = column[java.sql.Timestamp]("stablishedAt")
|
||||
val establishedAt = column[java.sql.Timestamp]("establishedAt")
|
||||
val closedAt = column[Option[java.sql.Timestamp]]("closedAt")
|
||||
}
|
||||
|
||||
|
||||
object UserGraphqlConnectionDAO {
|
||||
def insert(sessionToken: String, middlewareConnectionId: String) = {
|
||||
def insert(sessionToken: String, middlewareUID:String, middlewareConnectionId: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[UserGraphqlConnectionDbTableDef].insertOrUpdate(
|
||||
UserGraphqlConnectionDbModel(
|
||||
graphqlConnectionId = None,
|
||||
sessionToken = sessionToken,
|
||||
middlewareUID = middlewareUID,
|
||||
middlewareConnectionId = middlewareConnectionId,
|
||||
stablishedAt = new java.sql.Timestamp(System.currentTimeMillis()),
|
||||
establishedAt = new java.sql.Timestamp(System.currentTimeMillis()),
|
||||
closedAt = None
|
||||
)
|
||||
)
|
||||
@ -46,11 +49,12 @@ object UserGraphqlConnectionDAO {
|
||||
}
|
||||
}
|
||||
|
||||
def updateClosed(sessionToken: String, middlewareConnectionId: String) = {
|
||||
def updateClosed(sessionToken: String, middlewareUID: String, middlewareConnectionId: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[UserGraphqlConnectionDbTableDef]
|
||||
.filter(_.sessionToken === sessionToken)
|
||||
.filter(_.middlewareConnectionId === middlewareConnectionId)
|
||||
.filter(_.middlewareUID === middlewareUID)
|
||||
.filter(_.closedAt.isEmpty)
|
||||
.map(u => u.closedAt)
|
||||
.update(Some(new java.sql.Timestamp(System.currentTimeMillis())))
|
||||
|
@ -26,11 +26,15 @@ case class UserStateDbModel(
|
||||
pinned: Boolean = false,
|
||||
locked: Boolean = false,
|
||||
speechLocale: String,
|
||||
inactivityWarningDisplay: Boolean = false,
|
||||
inactivityWarningTimeoutSecs: Option[Long],
|
||||
)
|
||||
|
||||
class UserStateDbTableDef(tag: Tag) extends Table[UserStateDbModel](tag, None, "user") {
|
||||
override def * = (
|
||||
userId,emoji,away,raiseHand,guestStatus,guestStatusSetByModerator,guestLobbyMessage,mobile,clientType,disconnected,expired,ejected,ejectReason,ejectReasonCode,ejectedByModerator,presenter,pinned,locked,speechLocale) <> (UserStateDbModel.tupled, UserStateDbModel.unapply)
|
||||
userId,emoji,away,raiseHand,guestStatus,guestStatusSetByModerator,guestLobbyMessage,mobile,clientType,disconnected,
|
||||
expired,ejected,ejectReason,ejectReasonCode,ejectedByModerator,presenter,pinned,locked,speechLocale,
|
||||
inactivityWarningDisplay, inactivityWarningTimeoutSecs) <> (UserStateDbModel.tupled, UserStateDbModel.unapply)
|
||||
val userId = column[String]("userId", O.PrimaryKey)
|
||||
val emoji = column[String]("emoji")
|
||||
val away = column[Boolean]("away")
|
||||
@ -50,6 +54,8 @@ class UserStateDbTableDef(tag: Tag) extends Table[UserStateDbModel](tag, None, "
|
||||
val pinned = column[Boolean]("pinned")
|
||||
val locked = column[Boolean]("locked")
|
||||
val speechLocale = column[String]("speechLocale")
|
||||
val inactivityWarningDisplay = column[Boolean]("inactivityWarningDisplay")
|
||||
val inactivityWarningTimeoutSecs = column[Option[Long]]("inactivityWarningTimeoutSecs")
|
||||
}
|
||||
|
||||
object UserStateDAO {
|
||||
@ -119,4 +125,21 @@ object UserStateDAO {
|
||||
}
|
||||
}
|
||||
|
||||
def updateInactivityWarning(intId: String, inactivityWarningDisplay: Boolean, inactivityWarningTimeoutSecs: Long) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[UserStateDbTableDef]
|
||||
.filter(_.userId === intId)
|
||||
.map(u => (u.inactivityWarningDisplay, u.inactivityWarningTimeoutSecs))
|
||||
.update((inactivityWarningDisplay,
|
||||
inactivityWarningTimeoutSecs match {
|
||||
case 0 => None
|
||||
case timeout: Long => Some(timeout)
|
||||
case _ => None
|
||||
}))
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated inactivityWarningDisplay on user table!")
|
||||
case Failure(e) => DatabaseConnection.logger.error(s"Error updating inactivityWarningDisplay user: $e")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -42,4 +42,5 @@ object MeetingEndReason {
|
||||
val BREAKOUT_ENDED_BY_MOD = "BREAKOUT_ENDED_BY_MOD"
|
||||
val ENDED_DUE_TO_NO_AUTHED_USER = "ENDED_DUE_TO_NO_AUTHED_USER"
|
||||
val ENDED_DUE_TO_NO_MODERATOR = "ENDED_DUE_TO_NO_MODERATOR"
|
||||
val ENDED_DUE_TO_SERVICE_INTERRUPTION = "ENDED_DUE_TO_SERVICE_INTERRUPTION"
|
||||
}
|
||||
|
@ -104,7 +104,7 @@ object Polls {
|
||||
} yield {
|
||||
val pageId = if (poll.id.contains("deskshare")) "deskshare" else page.id
|
||||
val updatedShape = shape + ("whiteboardId" -> pageId)
|
||||
val annotation = new AnnotationVO(poll.id, updatedShape, pageId, requesterId)
|
||||
val annotation = new AnnotationVO(s"shape:poll-result-${poll.id}", updatedShape, pageId, requesterId)
|
||||
annotation
|
||||
}
|
||||
}
|
||||
@ -253,12 +253,13 @@ object Polls {
|
||||
|
||||
private def pollResultToWhiteboardShape(result: SimplePollResultOutVO): scala.collection.immutable.Map[String, Object] = {
|
||||
val shape = new scala.collection.mutable.HashMap[String, Object]()
|
||||
shape += "numRespondents" -> new Integer(result.numRespondents)
|
||||
shape += "numResponders" -> new Integer(result.numResponders)
|
||||
shape += "numRespondents" -> Integer.valueOf(result.numRespondents)
|
||||
shape += "numResponders" -> Integer.valueOf(result.numResponders)
|
||||
shape += "questionType" -> result.questionType
|
||||
shape += "questionText" -> result.questionText
|
||||
shape += "id" -> result.id
|
||||
shape += "questionText" -> result.questionText.getOrElse("")
|
||||
shape += "id" -> s"shape:poll-result-${result.id}"
|
||||
shape += "answers" -> result.answers
|
||||
shape += "type" -> "geo"
|
||||
shape.toMap
|
||||
}
|
||||
|
||||
|
@ -91,7 +91,7 @@ object RegisteredUsers {
|
||||
// will fail and can't join.
|
||||
// ralam april 21, 2020
|
||||
val bannedUser = user.copy(banned = true)
|
||||
//UserDAO.insert(meetingId, bannedUser)
|
||||
UserDAO.insert(meetingId, bannedUser)
|
||||
users.save(bannedUser)
|
||||
} else {
|
||||
// If user hasn't been ejected, we allow user to join
|
||||
@ -122,7 +122,7 @@ object RegisteredUsers {
|
||||
u
|
||||
} else {
|
||||
users.delete(ejectedUser.id)
|
||||
// UserDAO.delete(ejectedUser) it's being removed in User2x already
|
||||
// UserDAO.softDelete(ejectedUser) it's being removed in User2x already
|
||||
ejectedUser
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ object Users2x {
|
||||
}
|
||||
|
||||
def remove(users: Users2x, intId: String): Option[UserState] = {
|
||||
//UserDAO.delete(intId)
|
||||
//UserDAO.softDelete(intId)
|
||||
users.remove(intId)
|
||||
}
|
||||
|
||||
@ -78,15 +78,6 @@ object Users2x {
|
||||
users.toVector.filter(u => !u.presenter)
|
||||
}
|
||||
|
||||
def getRandomlyPickableUsers(users: Users2x, reduceDup: Boolean): Vector[UserState] = {
|
||||
|
||||
if (reduceDup) {
|
||||
users.toVector.filter(u => !u.presenter && u.role != Roles.MODERATOR_ROLE && !u.userLeftFlag.left && !u.pickExempted)
|
||||
} else {
|
||||
users.toVector.filter(u => !u.presenter && u.role != Roles.MODERATOR_ROLE && !u.userLeftFlag.left)
|
||||
}
|
||||
}
|
||||
|
||||
def findViewers(users: Users2x): Vector[UserState] = {
|
||||
users.toVector.filter(u => u.role == Roles.VIEWER_ROLE)
|
||||
}
|
||||
@ -98,6 +89,19 @@ object Users2x {
|
||||
def updateLastUserActivity(users: Users2x, u: UserState): UserState = {
|
||||
val newUserState = modify(u)(_.lastActivityTime).setTo(System.currentTimeMillis())
|
||||
users.save(newUserState)
|
||||
|
||||
//Reset inactivity warning
|
||||
if (u.lastInactivityInspect != 0) {
|
||||
resetLastInactivityInspect(users, newUserState)
|
||||
} else {
|
||||
newUserState
|
||||
}
|
||||
}
|
||||
|
||||
def resetLastInactivityInspect(users: Users2x, u: UserState): UserState = {
|
||||
val newUserState = modify(u)(_.lastInactivityInspect).setTo(0)
|
||||
users.save(newUserState)
|
||||
UserStateDAO.updateInactivityWarning(u.intId, inactivityWarningDisplay = false, 0)
|
||||
newUserState
|
||||
}
|
||||
|
||||
@ -125,7 +129,7 @@ object Users2x {
|
||||
_ <- users.remove(intId)
|
||||
ejectedUser <- users.removeFromCache(intId)
|
||||
} yield {
|
||||
// UserDAO.delete(intId) --it will keep the user on Db
|
||||
// UserDAO.softDelete(intId) --it will keep the user on Db
|
||||
ejectedUser
|
||||
}
|
||||
}
|
||||
@ -241,16 +245,6 @@ object Users2x {
|
||||
}
|
||||
}
|
||||
|
||||
def setUserExempted(users: Users2x, intId: String, exempted: Boolean): Option[UserState] = {
|
||||
for {
|
||||
u <- findWithIntId(users, intId)
|
||||
} yield {
|
||||
val newUser = u.modify(_.pickExempted).setTo(exempted)
|
||||
users.save(newUser)
|
||||
newUser
|
||||
}
|
||||
}
|
||||
|
||||
def setUserSpeechLocale(users: Users2x, intId: String, locale: String): Option[UserState] = {
|
||||
for {
|
||||
u <- findWithIntId(users, intId)
|
||||
@ -435,7 +429,6 @@ case class UserState(
|
||||
lastActivityTime: Long = System.currentTimeMillis(),
|
||||
lastInactivityInspect: Long = 0,
|
||||
clientType: String,
|
||||
pickExempted: Boolean,
|
||||
userLeftFlag: UserLeftFlag,
|
||||
speechLocale: String = ""
|
||||
)
|
||||
|
@ -111,10 +111,12 @@ class ReceivedJsonMsgHandlerActor(
|
||||
routeGenericMsg[ChangeUserPinStateReqMsg](envelope, jsonNode)
|
||||
case ChangeUserMobileFlagReqMsg.NAME =>
|
||||
routeGenericMsg[ChangeUserMobileFlagReqMsg](envelope, jsonNode)
|
||||
case UserConnectionAliveReqMsg.NAME =>
|
||||
routeGenericMsg[UserConnectionAliveReqMsg](envelope, jsonNode)
|
||||
case UserConnectionUpdateRttReqMsg.NAME =>
|
||||
routeGenericMsg[UserConnectionUpdateRttReqMsg](envelope, jsonNode)
|
||||
case SetUserSpeechLocaleReqMsg.NAME =>
|
||||
routeGenericMsg[SetUserSpeechLocaleReqMsg](envelope, jsonNode)
|
||||
case SelectRandomViewerReqMsg.NAME =>
|
||||
routeGenericMsg[SelectRandomViewerReqMsg](envelope, jsonNode)
|
||||
|
||||
// Poll
|
||||
case StartCustomPollReqMsg.NAME =>
|
||||
@ -462,6 +464,9 @@ class ReceivedJsonMsgHandlerActor(
|
||||
case UserGraphqlConnectionClosedSysMsg.NAME =>
|
||||
route[UserGraphqlConnectionClosedSysMsg](meetingManagerChannel, envelope, jsonNode)
|
||||
|
||||
case CheckGraphqlMiddlewareAlivePongSysMsg.NAME =>
|
||||
route[CheckGraphqlMiddlewareAlivePongSysMsg](meetingManagerChannel, envelope, jsonNode)
|
||||
|
||||
case _ =>
|
||||
log.error("Cannot route envelope name " + envelope.name)
|
||||
// do nothing
|
||||
|
@ -27,6 +27,10 @@ class StoreExportJobInRedisPresAnnEvent extends AbstractPresentationWithAnnotati
|
||||
|
||||
setEvent("StoreExportJobInRedisPresAnnEvent")
|
||||
|
||||
def setserverSideFilename(serverSideFilename: String) {
|
||||
eventMap.put(SERVER_SIDE_FILENAME, serverSideFilename)
|
||||
}
|
||||
|
||||
def setJobId(jobId: String) {
|
||||
eventMap.put(JOB_ID, jobId)
|
||||
}
|
||||
@ -68,6 +72,7 @@ object StoreExportJobInRedisPresAnnEvent {
|
||||
protected final val JOB_ID = "jobId"
|
||||
protected final val JOB_TYPE = "jobType"
|
||||
protected final val FILENAME = "filename"
|
||||
protected final val SERVER_SIDE_FILENAME = "serverSideFilename"
|
||||
protected final val PRES_ID = "presId"
|
||||
protected final val PRES_LOCATION = "presLocation"
|
||||
protected final val ALL_PAGES = "allPages"
|
||||
|
@ -7,7 +7,7 @@ import org.bigbluebutton.core.apps.groupchats.GroupChatApp
|
||||
import org.bigbluebutton.core.apps.users.UsersApp
|
||||
import org.bigbluebutton.core.apps.voice.VoiceApp
|
||||
import org.bigbluebutton.core.bus.{BigBlueButtonEvent, InternalEventBus}
|
||||
import org.bigbluebutton.core.db.{BreakoutRoomUserDAO, MeetingRecordingDAO, UserBreakoutRoomDAO}
|
||||
import org.bigbluebutton.core.db.{BreakoutRoomUserDAO, MeetingDAO, MeetingRecordingDAO, UserBreakoutRoomDAO}
|
||||
import org.bigbluebutton.core.domain.{MeetingEndReason, MeetingState2x}
|
||||
import org.bigbluebutton.core.models._
|
||||
import org.bigbluebutton.core2.MeetingStatus2x
|
||||
@ -73,7 +73,6 @@ trait HandlerHelpers extends SystemConfiguration {
|
||||
avatar = regUser.avatarURL,
|
||||
color = regUser.color,
|
||||
clientType = clientType,
|
||||
pickExempted = false,
|
||||
userLeftFlag = UserLeftFlag(false, 0)
|
||||
)
|
||||
}
|
||||
@ -206,6 +205,8 @@ trait HandlerHelpers extends SystemConfiguration {
|
||||
|
||||
val endedEvnt = buildMeetingEndedEvtMsg(liveMeeting.props.meetingProp.intId)
|
||||
outGW.send(endedEvnt)
|
||||
|
||||
MeetingDAO.setMeetingEnded(liveMeeting.props.meetingProp.intId, reason, userId)
|
||||
}
|
||||
|
||||
def destroyMeeting(eventBus: InternalEventBus, meetingId: String): Unit = {
|
||||
|
@ -76,6 +76,7 @@ class MeetingActor(
|
||||
|
||||
with UserJoinMeetingReqMsgHdlr
|
||||
with UserJoinMeetingAfterReconnectReqMsgHdlr
|
||||
with UserEstablishedGraphqlConnectionInternalMsgHdlr
|
||||
with UserConnectedToGlobalAudioMsgHdlr
|
||||
with UserDisconnectedFromGlobalAudioMsgHdlr
|
||||
with MuteAllExceptPresentersCmdMsgHdlr
|
||||
@ -266,8 +267,14 @@ class MeetingActor(
|
||||
// internal messages
|
||||
case msg: MonitorNumberOfUsersInternalMsg => handleMonitorNumberOfUsers(msg)
|
||||
case msg: SetPresenterInDefaultPodInternalMsg => state = presentationPodsApp.handleSetPresenterInDefaultPodInternalMsg(msg, state, liveMeeting, msgBus)
|
||||
case msg: UserClosedAllGraphqlConnectionsInternalMsg =>
|
||||
state = handleUserClosedAllGraphqlConnectionsInternalMsg(msg, state)
|
||||
updateModeratorsPresence()
|
||||
case msg: UserEstablishedGraphqlConnectionInternalMsg =>
|
||||
state = handleUserEstablishedGraphqlConnectionInternalMsg(msg, state)
|
||||
updateModeratorsPresence()
|
||||
|
||||
case msg: ExtendMeetingDuration => handleExtendMeetingDuration(msg)
|
||||
case msg: ExtendMeetingDuration => handleExtendMeetingDuration(msg)
|
||||
case msg: SendTimeRemainingAuditInternalMsg =>
|
||||
if (!liveMeeting.props.meetingProp.isBreakout) {
|
||||
// Update users of meeting remaining time.
|
||||
@ -395,9 +402,10 @@ class MeetingActor(
|
||||
case m: UserReactionTimeExpiredCmdMsg => handleUserReactionTimeExpiredCmdMsg(m)
|
||||
case m: ClearAllUsersEmojiCmdMsg => handleClearAllUsersEmojiCmdMsg(m)
|
||||
case m: ClearAllUsersReactionCmdMsg => handleClearAllUsersReactionCmdMsg(m)
|
||||
case m: SelectRandomViewerReqMsg => usersApp.handleSelectRandomViewerReqMsg(m)
|
||||
case m: ChangeUserPinStateReqMsg => usersApp.handleChangeUserPinStateReqMsg(m)
|
||||
case m: ChangeUserMobileFlagReqMsg => usersApp.handleChangeUserMobileFlagReqMsg(m)
|
||||
case m: UserConnectionAliveReqMsg => usersApp.handleUserConnectionAliveReqMsg(m)
|
||||
case m: UserConnectionUpdateRttReqMsg => usersApp.handleUserConnectionUpdateRttReqMsg(m)
|
||||
case m: SetUserSpeechLocaleReqMsg => usersApp.handleSetUserSpeechLocaleReqMsg(m)
|
||||
|
||||
// Client requested to eject user
|
||||
@ -975,6 +983,7 @@ class MeetingActor(
|
||||
|
||||
val secsToDisconnect = TimeUnit.MILLISECONDS.toSeconds(expiryTracker.userActivitySignResponseDelayInMs);
|
||||
Sender.sendUserInactivityInspectMsg(liveMeeting.props.meetingProp.intId, u.intId, secsToDisconnect, outGW)
|
||||
UserStateDAO.updateInactivityWarning(u.intId, inactivityWarningDisplay = true, secsToDisconnect)
|
||||
updateUserLastInactivityInspect(u.intId)
|
||||
}
|
||||
}
|
||||
|
@ -367,7 +367,7 @@ public class Bezier {
|
||||
* @param first Indice of first point in d.
|
||||
* @param last Indice of last point in d.
|
||||
* @param tHat1 Unit tangent vectors at start point.
|
||||
* @param tHat2 Unit tanget vector at end point.
|
||||
* @param tHat2 Unit tangent vector at end point.
|
||||
* @param errorSquared User-defined errorSquared squared.
|
||||
* @param bezierPath Path to which the bezier curve segments are added.
|
||||
*/
|
||||
@ -580,7 +580,7 @@ public class Bezier {
|
||||
*
|
||||
* @param Q Current fitted bezier curve.
|
||||
* @param P Digitized point.
|
||||
* @param u Parameter value vor P.
|
||||
* @param u Parameter value for P.
|
||||
*/
|
||||
private static double newtonRaphsonRootFind(Point2D.Double[] Q, Point2D.Double P, double u) {
|
||||
double numerator, denominator;
|
||||
@ -661,7 +661,7 @@ public class Bezier {
|
||||
* @param last Indice of last point in d.
|
||||
* @param uPrime Parameter values for region .
|
||||
* @param tHat1 Unit tangent vectors at start point.
|
||||
* @param tHat2 Unit tanget vector at end point.
|
||||
* @param tHat2 Unit tangent vector at end point.
|
||||
* @return A cubic bezier curve consisting of 4 control points.
|
||||
*/
|
||||
private static Point2D.Double[] generateBezier(ArrayList<Point2D.Double> d, int first, int last, double[] uPrime, Point2D.Double tHat1, Point2D.Double tHat2) {
|
||||
|
@ -40,7 +40,7 @@ public class BezierPath extends ArrayList<BezierPath.Node>
|
||||
private static final long serialVersionUID=1L;
|
||||
|
||||
/** Constant for having only control point C0 in effect. C0 is the point
|
||||
* through whitch the curve passes. */
|
||||
* through which the curve passes. */
|
||||
public static final int C0_MASK = 0;
|
||||
/** Constant for having control point C1 in effect (in addition
|
||||
* to C0). C1 controls the curve going towards C0.
|
||||
|
@ -78,6 +78,8 @@ class AnalyticsActor(val includeChat: Boolean) extends Actor with ActorLogging {
|
||||
case m: AssignPresenterReqMsg => logMessage(msg)
|
||||
case m: ChangeUserPinStateReqMsg => logMessage(msg)
|
||||
case m: ChangeUserMobileFlagReqMsg => logMessage(msg)
|
||||
case m: UserConnectionAliveReqMsg => logMessage(msg)
|
||||
case m: UserConnectionUpdateRttReqMsg => logMessage(msg)
|
||||
case m: ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg => logMessage(msg)
|
||||
case m: ScreenshareRtmpBroadcastStoppedVoiceConfEvtMsg => logMessage(msg)
|
||||
case m: ScreenshareRtmpBroadcastStartedEvtMsg => logMessage(msg)
|
||||
|
@ -256,6 +256,16 @@ object MsgBuilder {
|
||||
BbbCommonEnvCoreMsg(envelope, event)
|
||||
}
|
||||
|
||||
def buildCheckGraphqlMiddlewareAlivePingSysMsg(middlewareUid: String): BbbCommonEnvCoreMsg = {
|
||||
val routing = Routing.addMsgToClientRouting(MessageTypes.SYSTEM, "", "")
|
||||
val envelope = BbbCoreEnvelope(CheckGraphqlMiddlewareAlivePingSysMsg.NAME, routing)
|
||||
val header = BbbCoreHeaderWithMeetingId(CheckGraphqlMiddlewareAlivePingSysMsg.NAME, "")
|
||||
val body = CheckGraphqlMiddlewareAlivePingSysMsgBody(middlewareUid)
|
||||
val event = CheckGraphqlMiddlewareAlivePingSysMsg(header, body)
|
||||
|
||||
BbbCommonEnvCoreMsg(envelope, event)
|
||||
}
|
||||
|
||||
def buildEjectAllFromVoiceConfMsg(meetingId: String, voiceConf: String): BbbCommonEnvCoreMsg = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
|
||||
val envelope = BbbCoreEnvelope(EjectAllFromVoiceConfMsg.NAME, routing)
|
||||
|
@ -69,8 +69,7 @@ trait FakeTestData {
|
||||
UserState(intId = regUser.id, extId = regUser.externId, name = regUser.name, role = regUser.role, pin = false,
|
||||
mobile = false, guest = regUser.guest, authed = regUser.authed, guestStatus = regUser.guestStatus,
|
||||
emoji = "none", reactionEmoji = "none", raiseHand = false, away = false, locked = false, presenter = false,
|
||||
avatar = regUser.avatarURL, color = "#ff6242", clientType = "unknown",
|
||||
pickExempted = false, userLeftFlag = UserLeftFlag(false, 0))
|
||||
avatar = regUser.avatarURL, color = "#ff6242", clientType = "unknown", userLeftFlag = UserLeftFlag(false, 0))
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -73,6 +73,7 @@ class ExportAnnotationsActor(
|
||||
private def handleStoreExportJobInRedisSysMsg(msg: StoreExportJobInRedisSysMsg) {
|
||||
val ev = new StoreExportJobInRedisPresAnnEvent()
|
||||
|
||||
ev.setserverSideFilename(msg.body.exportJob.serverSideFilename)
|
||||
ev.setJobId(msg.body.exportJob.jobId)
|
||||
ev.setJobType(msg.body.exportJob.jobType)
|
||||
ev.setFilename(msg.body.exportJob.filename)
|
||||
|
@ -1,43 +0,0 @@
|
||||
package org.bigbluebutton.endpoint.redis
|
||||
|
||||
import org.apache.pekko.actor.{Actor, ActorLogging, ActorSystem, Props}
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.db.UserGraphqlConnectionDAO
|
||||
|
||||
object GraphqlActionsActor {
|
||||
def props(system: ActorSystem): Props =
|
||||
Props(
|
||||
classOf[GraphqlActionsActor],
|
||||
system,
|
||||
)
|
||||
}
|
||||
|
||||
class GraphqlActionsActor(
|
||||
system: ActorSystem,
|
||||
) extends Actor with ActorLogging {
|
||||
|
||||
def receive = {
|
||||
//=============================
|
||||
// 2x messages
|
||||
case msg: BbbCommonEnvCoreMsg => handleBbbCommonEnvCoreMsg(msg)
|
||||
case _ => // do nothing
|
||||
}
|
||||
|
||||
private def handleBbbCommonEnvCoreMsg(msg: BbbCommonEnvCoreMsg): Unit = {
|
||||
msg.core match {
|
||||
// Messages from bbb-graphql-middleware
|
||||
case m: UserGraphqlConnectionEstablishedSysMsg => handleUserGraphqlConnectionEstablishedSysMsg(m)
|
||||
case m: UserGraphqlConnectionClosedSysMsg => handleUserGraphqlConnectionClosedSysMsg(m)
|
||||
case _ => // message not to be handled.
|
||||
}
|
||||
}
|
||||
|
||||
private def handleUserGraphqlConnectionEstablishedSysMsg(msg: UserGraphqlConnectionEstablishedSysMsg) {
|
||||
UserGraphqlConnectionDAO.insert(msg.body.sessionToken, msg.body.browserConnectionId)
|
||||
}
|
||||
|
||||
private def handleUserGraphqlConnectionClosedSysMsg(msg: UserGraphqlConnectionClosedSysMsg) {
|
||||
UserGraphqlConnectionDAO.updateClosed(msg.body.sessionToken, msg.body.browserConnectionId)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,171 @@
|
||||
package org.bigbluebutton.endpoint.redis
|
||||
|
||||
import org.apache.pekko.actor.{Actor, ActorLogging, ActorSystem, Props}
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.OutMessageGateway
|
||||
import org.bigbluebutton.core.api.{UserClosedAllGraphqlConnectionsInternalMsg, UserEstablishedGraphqlConnectionInternalMsg}
|
||||
import org.bigbluebutton.core.bus.{BigBlueButtonEvent, InternalEventBus}
|
||||
import org.bigbluebutton.core.db.UserGraphqlConnectionDAO
|
||||
import org.bigbluebutton.core2.message.senders.MsgBuilder
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.concurrent.duration._
|
||||
import ExecutionContext.Implicits.global
|
||||
|
||||
case object MiddlewareHealthCheckScheduler10Sec
|
||||
|
||||
object GraphqlConnectionsActor {
|
||||
def props(system: ActorSystem,
|
||||
eventBus: InternalEventBus,
|
||||
outGW: OutMessageGateway,
|
||||
): Props =
|
||||
Props(
|
||||
classOf[GraphqlConnectionsActor],
|
||||
system,
|
||||
eventBus,
|
||||
outGW,
|
||||
)
|
||||
}
|
||||
|
||||
case class GraphqlUser(
|
||||
intId: String,
|
||||
meetingId: String,
|
||||
sessionToken: String,
|
||||
)
|
||||
|
||||
case class GraphqlUserConnection(
|
||||
middlewareUID: String,
|
||||
browserConnectionId: String,
|
||||
sessionToken: String,
|
||||
user: GraphqlUser,
|
||||
)
|
||||
|
||||
|
||||
class GraphqlConnectionsActor(
|
||||
system: ActorSystem,
|
||||
val eventBus: InternalEventBus,
|
||||
val outGW: OutMessageGateway,
|
||||
) extends Actor with ActorLogging {
|
||||
|
||||
private var users: Map[String, GraphqlUser] = Map()
|
||||
private var graphqlConnections: Map[String, GraphqlUserConnection] = Map()
|
||||
private var pendingResponseMiddlewareUIDs: Map[String, BigInt] = Map()
|
||||
|
||||
system.scheduler.schedule(10.seconds, 10.seconds, self, MiddlewareHealthCheckScheduler10Sec)
|
||||
private val maxMiddlewareInactivityInMillis = 11000
|
||||
|
||||
def receive = {
|
||||
//=============================
|
||||
// 2x messages
|
||||
case msg: BbbCommonEnvCoreMsg => handleBbbCommonEnvCoreMsg(msg)
|
||||
case MiddlewareHealthCheckScheduler10Sec => runMiddlewareHealthCheck()
|
||||
case _ => // do nothing
|
||||
}
|
||||
|
||||
private def handleBbbCommonEnvCoreMsg(msg: BbbCommonEnvCoreMsg): Unit = {
|
||||
msg.core match {
|
||||
case m: RegisterUserReqMsg => handleUserRegisteredRespMsg(m)
|
||||
case m: DestroyMeetingSysCmdMsg => handleDestroyMeetingSysCmdMsg(m)
|
||||
// Messages from bbb-graphql-middleware
|
||||
case m: UserGraphqlConnectionEstablishedSysMsg => handleUserGraphqlConnectionEstablishedSysMsg(m)
|
||||
case m: UserGraphqlConnectionClosedSysMsg => handleUserGraphqlConnectionClosedSysMsg(m)
|
||||
case m: CheckGraphqlMiddlewareAlivePongSysMsg => handleCheckGraphqlMiddlewareAlivePongSysMsg(m)
|
||||
case _ => // message not to be handled.
|
||||
}
|
||||
}
|
||||
|
||||
private def handleUserRegisteredRespMsg(msg: RegisterUserReqMsg): Unit = {
|
||||
users += (msg.body.sessionToken -> GraphqlUser(
|
||||
msg.body.intUserId,
|
||||
msg.body.meetingId,
|
||||
msg.body.sessionToken
|
||||
))
|
||||
}
|
||||
|
||||
private def handleDestroyMeetingSysCmdMsg(msg: DestroyMeetingSysCmdMsg): Unit = {
|
||||
users = users.filter(u => u._2.meetingId != msg.body.meetingId)
|
||||
graphqlConnections = graphqlConnections.filter(c => c._2.user.meetingId != msg.body.meetingId)
|
||||
}
|
||||
|
||||
private def handleUserGraphqlConnectionEstablishedSysMsg(msg: UserGraphqlConnectionEstablishedSysMsg): Unit = {
|
||||
UserGraphqlConnectionDAO.insert(msg.body.sessionToken, msg.body.middlewareUID, msg.body.browserConnectionId)
|
||||
|
||||
for {
|
||||
user <- users.get(msg.body.sessionToken)
|
||||
} yield {
|
||||
|
||||
//Send internal message informing user has connected
|
||||
if (!graphqlConnections.values.exists(c => c.sessionToken == msg.body.sessionToken)) {
|
||||
eventBus.publish(BigBlueButtonEvent(user.meetingId, UserEstablishedGraphqlConnectionInternalMsg(user.intId)))
|
||||
}
|
||||
|
||||
graphqlConnections += (msg.body.browserConnectionId -> GraphqlUserConnection(
|
||||
msg.body.middlewareUID,
|
||||
msg.body.browserConnectionId,
|
||||
msg.body.sessionToken,
|
||||
user
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private def handleUserGraphqlConnectionClosedSysMsg(msg: UserGraphqlConnectionClosedSysMsg): Unit = {
|
||||
handleUserGraphqlConnectionClosed(msg.body.sessionToken, msg.body.middlewareUID, msg.body.browserConnectionId)
|
||||
}
|
||||
|
||||
private def handleUserGraphqlConnectionClosed(sessionToken: String, middlewareUID: String, browserConnectionId: String): Unit = {
|
||||
UserGraphqlConnectionDAO.updateClosed(sessionToken, middlewareUID, browserConnectionId)
|
||||
|
||||
for {
|
||||
user <- users.get(sessionToken)
|
||||
} yield {
|
||||
graphqlConnections = graphqlConnections.-(browserConnectionId)
|
||||
|
||||
//Send internal message informing user disconnected
|
||||
if (!graphqlConnections.values.exists(c => c.sessionToken == sessionToken)) {
|
||||
eventBus.publish(BigBlueButtonEvent(user.meetingId, UserClosedAllGraphqlConnectionsInternalMsg(user.intId)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def runMiddlewareHealthCheck(): Unit = {
|
||||
removeInactiveConnections()
|
||||
sendPingMessageToAllMiddlewareServices()
|
||||
}
|
||||
|
||||
private def sendPingMessageToAllMiddlewareServices(): Unit = {
|
||||
graphqlConnections.map(c => {
|
||||
c._2.middlewareUID
|
||||
}).toVector.distinct.map(middlewareUID => {
|
||||
val event = MsgBuilder.buildCheckGraphqlMiddlewareAlivePingSysMsg(middlewareUID)
|
||||
outGW.send(event)
|
||||
log.debug(s"Sent ping message from graphql middleware ${middlewareUID}.")
|
||||
pendingResponseMiddlewareUIDs.get(middlewareUID) match {
|
||||
case None => pendingResponseMiddlewareUIDs += (middlewareUID -> System.currentTimeMillis)
|
||||
case _ => //Ignore
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private def removeInactiveConnections(): Unit = {
|
||||
for {
|
||||
(middlewareUid, pingSentAt) <- pendingResponseMiddlewareUIDs
|
||||
if (System.currentTimeMillis - pingSentAt) > maxMiddlewareInactivityInMillis
|
||||
} yield {
|
||||
log.info("Removing connections from the middleware {} due to inactivity of the service.",middlewareUid)
|
||||
for {
|
||||
(_, graphqlConn) <- graphqlConnections
|
||||
if graphqlConn.middlewareUID == middlewareUid
|
||||
} yield {
|
||||
handleUserGraphqlConnectionClosed(graphqlConn.sessionToken, graphqlConn.middlewareUID, graphqlConn.browserConnectionId)
|
||||
}
|
||||
|
||||
pendingResponseMiddlewareUIDs -= middlewareUid
|
||||
}
|
||||
}
|
||||
|
||||
private def handleCheckGraphqlMiddlewareAlivePongSysMsg(msg: CheckGraphqlMiddlewareAlivePongSysMsg): Unit = {
|
||||
log.debug(s"Received pong message from graphql middleware ${msg.body.middlewareUID}.")
|
||||
pendingResponseMiddlewareUIDs -= msg.body.middlewareUID
|
||||
}
|
||||
|
||||
}
|
@ -20,6 +20,7 @@ case class Meeting(
|
||||
intId: String,
|
||||
extId: String,
|
||||
name: String,
|
||||
downloadSessionDataEnabled: Boolean,
|
||||
users: Map[String, User] = Map(),
|
||||
polls: Map[String, Poll] = Map(),
|
||||
screenshares: Vector[Screenshare] = Vector(),
|
||||
@ -585,6 +586,7 @@ class LearningDashboardActor(
|
||||
msg.body.props.meetingProp.intId,
|
||||
msg.body.props.meetingProp.extId,
|
||||
msg.body.props.meetingProp.name,
|
||||
downloadSessionDataEnabled = !msg.body.props.meetingProp.disabledFeatures.contains("learningDashboardDownloadSessionData"),
|
||||
)
|
||||
|
||||
meetings += (newMeeting.intId -> newMeeting)
|
||||
|
@ -85,6 +85,9 @@ class RedisRecorderActor(
|
||||
case m: UserLeftMeetingEvtMsg => handleUserLeftMeetingEvtMsg(m)
|
||||
case m: PresenterAssignedEvtMsg => handlePresenterAssignedEvtMsg(m)
|
||||
case m: UserEmojiChangedEvtMsg => handleUserEmojiChangedEvtMsg(m)
|
||||
case m: UserAwayChangedEvtMsg => handleUserAwayChangedEvtMsg(m)
|
||||
case m: UserRaiseHandChangedEvtMsg => handleUserRaiseHandChangedEvtMsg(m)
|
||||
case m: UserReactionEmojiChangedEvtMsg => handleUserReactionEmojiChangedEvtMsg(m)
|
||||
case m: UserRoleChangedEvtMsg => handleUserRoleChangedEvtMsg(m)
|
||||
case m: UserBroadcastCamStartedEvtMsg => handleUserBroadcastCamStartedEvtMsg(m)
|
||||
case m: UserBroadcastCamStoppedEvtMsg => handleUserBroadcastCamStoppedEvtMsg(m)
|
||||
@ -112,7 +115,7 @@ class RedisRecorderActor(
|
||||
//case m: DeskShareNotifyViewersRTMP => handleDeskShareNotifyViewersRTMP(m)
|
||||
|
||||
// AudioCaptions
|
||||
case m: TranscriptUpdatedEvtMsg => handleTranscriptUpdatedEvtMsg(m)
|
||||
//case m: TranscriptUpdatedEvtMsg => handleTranscriptUpdatedEvtMsg(m) // temporarily disabling due to issue https://github.com/bigbluebutton/bigbluebutton/issues/19701
|
||||
|
||||
// Meeting
|
||||
case m: RecordingStatusChangedEvtMsg => handleRecordingStatusChangedEvtMsg(m)
|
||||
@ -281,7 +284,7 @@ class RedisRecorderActor(
|
||||
}
|
||||
|
||||
private def getPresentationId(whiteboardId: String): String = {
|
||||
// Need to split the whiteboard id into presenation id and page num as the old
|
||||
// Need to split the whiteboard id into presentation id and page num as the old
|
||||
// recording expects them
|
||||
val strId = new StringOps(whiteboardId)
|
||||
val ids = strId.split('/')
|
||||
@ -379,6 +382,18 @@ class RedisRecorderActor(
|
||||
handleUserStatusChange(msg.header.meetingId, msg.body.userId, "emojiStatus", msg.body.emoji)
|
||||
}
|
||||
|
||||
private def handleUserAwayChangedEvtMsg(msg: UserAwayChangedEvtMsg) {
|
||||
handleUserStatusChange(msg.header.meetingId, msg.body.userId, "away", if (msg.body.away) "true" else "false")
|
||||
}
|
||||
|
||||
private def handleUserRaiseHandChangedEvtMsg(msg: UserRaiseHandChangedEvtMsg) {
|
||||
handleUserStatusChange(msg.header.meetingId, msg.body.userId, "raiseHand", if (msg.body.raiseHand) "true" else "false")
|
||||
}
|
||||
|
||||
private def handleUserReactionEmojiChangedEvtMsg(msg: UserReactionEmojiChangedEvtMsg) {
|
||||
handleUserStatusChange(msg.header.meetingId, msg.body.userId, "reactionEmoji", msg.body.reactionEmoji)
|
||||
}
|
||||
|
||||
private def handleUserRoleChangedEvtMsg(msg: UserRoleChangedEvtMsg) {
|
||||
handleUserStatusChange(msg.header.meetingId, msg.body.userId, "role", msg.body.role)
|
||||
}
|
||||
@ -521,6 +536,7 @@ class RedisRecorderActor(
|
||||
}
|
||||
*/
|
||||
|
||||
/* temporarily disabling due to issue https://github.com/bigbluebutton/bigbluebutton/issues/19701
|
||||
private def handleTranscriptUpdatedEvtMsg(msg: TranscriptUpdatedEvtMsg) {
|
||||
val ev = new TranscriptUpdatedRecordEvent()
|
||||
ev.setMeetingId(msg.header.meetingId)
|
||||
@ -529,6 +545,7 @@ class RedisRecorderActor(
|
||||
|
||||
record(msg.header.meetingId, ev.toMap.asJava)
|
||||
}
|
||||
*/
|
||||
|
||||
private def handleStartExternalVideoEvtMsg(msg: StartExternalVideoEvtMsg) {
|
||||
val ev = new StartExternalVideoRecordEvent()
|
||||
|
@ -51,7 +51,7 @@ object TestDataGen {
|
||||
guest = regUser.guest, authed = regUser.authed, guestStatus = regUser.guestStatus,
|
||||
emoji = "none", reactionEmoji = "none", raiseHand = false, away = false, pin = false, mobile = false,
|
||||
locked = false, presenter = false, avatar = regUser.avatarURL, color = "#ff6242",
|
||||
clientType = "unknown", pickExempted = false, userLeftFlag = UserLeftFlag(false, 0))
|
||||
clientType = "unknown", userLeftFlag = UserLeftFlag(false, 0))
|
||||
Users2x.add(liveMeeting.users2x, u)
|
||||
u
|
||||
}
|
||||
|
@ -51,6 +51,7 @@ postgres {
|
||||
}
|
||||
numThreads = 1
|
||||
maxConnections = 1
|
||||
queueSize = 20000
|
||||
}
|
||||
|
||||
|
||||
@ -64,6 +65,7 @@ expire {
|
||||
services {
|
||||
bbbWebAPI = "https://192.168.23.33/bigbluebutton/api"
|
||||
sharedSecret = "changeme"
|
||||
checkSumAlgorithmForBreakouts = "sha256"
|
||||
}
|
||||
|
||||
eventBus {
|
||||
|
@ -14,7 +14,7 @@ object Dependencies {
|
||||
// Libraries
|
||||
val pekkoVersion = "1.0.1"
|
||||
val pekkoHttpVersion = "1.0.0"
|
||||
val logback = "1.2.10"
|
||||
val logback = "1.2.13"
|
||||
|
||||
// Apache Commons
|
||||
val lang = "3.12.0"
|
||||
|
@ -70,6 +70,9 @@ case class LockSettingsProps(
|
||||
case class SystemProps(
|
||||
html5InstanceId: Int,
|
||||
logoutUrl: String,
|
||||
customLogoURL: String,
|
||||
bannerText: String,
|
||||
bannerColor: String,
|
||||
)
|
||||
|
||||
case class GroupProps(
|
||||
|
@ -17,7 +17,7 @@ case class MakePresentationDownloadReqMsgBody(presId: String, allPages: Boolean,
|
||||
object NewPresFileAvailableMsg { val NAME = "NewPresFileAvailableMsg" }
|
||||
case class NewPresFileAvailableMsg(header: BbbClientMsgHeader, body: NewPresFileAvailableMsgBody) extends StandardMsg
|
||||
case class NewPresFileAvailableMsgBody(annotatedFileURI: String, originalFileURI: String, convertedFileURI: String,
|
||||
presId: String, fileStateType: String)
|
||||
presId: String, fileStateType: String, fileName: String)
|
||||
|
||||
object PresAnnStatusMsg { val NAME = "PresAnnStatusMsg" }
|
||||
case class PresAnnStatusMsg(header: BbbClientMsgHeader, body: PresAnnStatusMsgBody) extends StandardMsg
|
||||
|
@ -232,6 +232,26 @@ object DeletedRecordingSysMsg { val NAME = "DeletedRecordingSysMsg" }
|
||||
case class DeletedRecordingSysMsg(header: BbbCoreBaseHeader, body: DeletedRecordingSysMsgBody) extends BbbCoreMsg
|
||||
case class DeletedRecordingSysMsgBody(recordId: String)
|
||||
|
||||
/**
|
||||
* Sent from akka-apps to graphql-middleware
|
||||
*/
|
||||
object CheckGraphqlMiddlewareAlivePingSysMsg { val NAME = "CheckGraphqlMiddlewareAlivePingSysMsg" }
|
||||
case class CheckGraphqlMiddlewareAlivePingSysMsg(
|
||||
header: BbbCoreHeaderWithMeetingId,
|
||||
body: CheckGraphqlMiddlewareAlivePingSysMsgBody
|
||||
) extends BbbCoreMsg
|
||||
case class CheckGraphqlMiddlewareAlivePingSysMsgBody(middlewareUID: String)
|
||||
|
||||
/**
|
||||
* Sent from graphql-middleware to akka-apps
|
||||
*/
|
||||
object CheckGraphqlMiddlewareAlivePongSysMsg { val NAME = "CheckGraphqlMiddlewareAlivePongSysMsg" }
|
||||
case class CheckGraphqlMiddlewareAlivePongSysMsg(
|
||||
header: BbbCoreBaseHeader,
|
||||
body: CheckGraphqlMiddlewareAlivePongSysMsgBody
|
||||
) extends BbbCoreMsg
|
||||
case class CheckGraphqlMiddlewareAlivePongSysMsgBody(middlewareUID: String)
|
||||
|
||||
/**
|
||||
* Sent from akka-apps to graphql-middleware
|
||||
*/
|
||||
@ -251,21 +271,21 @@ case class UserGraphqlReconnectionForcedEvtMsg(
|
||||
header: BbbCoreBaseHeader,
|
||||
body: UserGraphqlReconnectionForcedEvtMsgBody
|
||||
) extends BbbCoreMsg
|
||||
case class UserGraphqlReconnectionForcedEvtMsgBody(sessionToken: String, browserConnectionId: String)
|
||||
case class UserGraphqlReconnectionForcedEvtMsgBody(middlewareUID: String, sessionToken: String, browserConnectionId: String)
|
||||
|
||||
object UserGraphqlConnectionEstablishedSysMsg { val NAME = "UserGraphqlConnectionEstablishedSysMsg" }
|
||||
case class UserGraphqlConnectionEstablishedSysMsg(
|
||||
header: BbbCoreBaseHeader,
|
||||
body: UserGraphqlConnectionEstablishedSysMsgBody
|
||||
) extends BbbCoreMsg
|
||||
case class UserGraphqlConnectionEstablishedSysMsgBody(sessionToken: String, browserConnectionId: String)
|
||||
case class UserGraphqlConnectionEstablishedSysMsgBody(middlewareUID: String, sessionToken: String, browserConnectionId: String)
|
||||
|
||||
object UserGraphqlConnectionClosedSysMsg { val NAME = "UserGraphqlConnectionClosedSysMsg" }
|
||||
case class UserGraphqlConnectionClosedSysMsg(
|
||||
header: BbbCoreBaseHeader,
|
||||
body: UserGraphqlConnectionClosedSysMsgBody
|
||||
) extends BbbCoreMsg
|
||||
case class UserGraphqlConnectionClosedSysMsgBody(sessionToken: String, browserConnectionId: String)
|
||||
case class UserGraphqlConnectionClosedSysMsgBody(middlewareUID: String, sessionToken: String, browserConnectionId: String)
|
||||
|
||||
/**
|
||||
* Sent from akka-apps to bbb-web to inform a summary of the meeting activities
|
||||
|
@ -143,7 +143,7 @@ case class SetRecordingStatusCmdMsg(header: BbbClientMsgHeader, body: SetRecordi
|
||||
case class SetRecordingStatusCmdMsgBody(recording: Boolean, setBy: String)
|
||||
|
||||
/**
|
||||
* Sent by user to start recording mark and ignore previsous marks
|
||||
* Sent by user to start recording mark and ignore previous marks
|
||||
*/
|
||||
object RecordAndClearPreviousMarkersCmdMsg { val NAME = "RecordAndClearPreviousMarkersCmdMsg" }
|
||||
case class RecordAndClearPreviousMarkersCmdMsg(header: BbbClientMsgHeader, body: RecordAndClearPreviousMarkersCmdMsgBody) extends StandardMsg
|
||||
@ -293,6 +293,20 @@ object ChangeUserMobileFlagReqMsg { val NAME = "ChangeUserMobileFlagReqMsg" }
|
||||
case class ChangeUserMobileFlagReqMsg(header: BbbClientMsgHeader, body: ChangeUserMobileFlagReqMsgBody) extends StandardMsg
|
||||
case class ChangeUserMobileFlagReqMsgBody(userId: String, mobile: Boolean)
|
||||
|
||||
/**
|
||||
* Sent from client to inform the connection is alive.
|
||||
*/
|
||||
object UserConnectionAliveReqMsg { val NAME = "UserConnectionAliveReqMsg" }
|
||||
case class UserConnectionAliveReqMsg(header: BbbClientMsgHeader, body: UserConnectionAliveReqMsgBody) extends StandardMsg
|
||||
case class UserConnectionAliveReqMsgBody(userId: String)
|
||||
|
||||
/**
|
||||
* Sent from client to inform the RTT (time it took to send the Alive and receive confirmation).
|
||||
*/
|
||||
object UserConnectionUpdateRttReqMsg { val NAME = "UserConnectionUpdateRttReqMsg" }
|
||||
case class UserConnectionUpdateRttReqMsg(header: BbbClientMsgHeader, body: UserConnectionUpdateRttReqMsgBody) extends StandardMsg
|
||||
case class UserConnectionUpdateRttReqMsgBody(userId: String, networkRttInMs: Double)
|
||||
|
||||
/**
|
||||
* Sent to all clients about a user mobile flag.
|
||||
*/
|
||||
@ -511,20 +525,6 @@ object UserActivitySignCmdMsg { val NAME = "UserActivitySignCmdMsg" }
|
||||
case class UserActivitySignCmdMsg(header: BbbClientMsgHeader, body: UserActivitySignCmdMsgBody) extends StandardMsg
|
||||
case class UserActivitySignCmdMsgBody(userId: String)
|
||||
|
||||
/**
|
||||
* Sent from client to randomly select a viewer
|
||||
*/
|
||||
object SelectRandomViewerReqMsg { val NAME = "SelectRandomViewerReqMsg" }
|
||||
case class SelectRandomViewerReqMsg(header: BbbClientMsgHeader, body: SelectRandomViewerReqMsgBody) extends StandardMsg
|
||||
case class SelectRandomViewerReqMsgBody(requestedBy: String)
|
||||
|
||||
/**
|
||||
* Response to request for a random viewer
|
||||
*/
|
||||
object SelectRandomViewerRespMsg { val NAME = "SelectRandomViewerRespMsg" }
|
||||
case class SelectRandomViewerRespMsg(header: BbbClientMsgHeader, body: SelectRandomViewerRespMsgBody) extends StandardMsg
|
||||
case class SelectRandomViewerRespMsgBody(requestedBy: String, userIds: Vector[String], choice: String)
|
||||
|
||||
object SetUserSpeechLocaleReqMsg { val NAME = "SetUserSpeechLocaleReqMsg" }
|
||||
case class SetUserSpeechLocaleReqMsg(header: BbbClientMsgHeader, body: SetUserSpeechLocaleReqMsgBody) extends StandardMsg
|
||||
case class SetUserSpeechLocaleReqMsgBody(locale: String, provider: String)
|
||||
|
@ -22,6 +22,7 @@ case class ExportJob(
|
||||
jobId: String,
|
||||
jobType: String,
|
||||
filename: String,
|
||||
serverSideFilename: String,
|
||||
presId: String,
|
||||
presLocation: String,
|
||||
allPages: Boolean,
|
||||
|
@ -106,7 +106,7 @@ libraryDependencies ++= Seq(
|
||||
"org.springframework.boot" % "spring-boot-starter-validation" % "2.7.17",
|
||||
"org.springframework.data" % "spring-data-commons" % "2.7.6",
|
||||
"org.apache.httpcomponents" % "httpclient" % "4.5.13",
|
||||
"org.postgresql" % "postgresql" % "42.4.3",
|
||||
"org.postgresql" % "postgresql" % "42.7.2",
|
||||
"org.hibernate" % "hibernate-core" % "5.6.1.Final",
|
||||
"org.flywaydb" % "flyway-core" % "7.8.2",
|
||||
"com.zaxxer" % "HikariCP" % "4.0.3",
|
||||
|
@ -111,7 +111,7 @@ public class ApiParams {
|
||||
public static final String RECORD_FULL_DURATION_MEDIA = "recordFullDurationMedia";
|
||||
|
||||
private ApiParams() {
|
||||
throw new IllegalStateException("ApiParams is a utility class. Instanciation is forbidden.");
|
||||
throw new IllegalStateException("ApiParams is a utility class. Instantiation is forbidden.");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -75,6 +75,7 @@ public class MeetingService implements MessageListener {
|
||||
*/
|
||||
private final ConcurrentMap<String, Meeting> meetings;
|
||||
private final ConcurrentMap<String, UserSession> sessions;
|
||||
private final ConcurrentMap<String, UserSessionBasicData> removedSessions;
|
||||
|
||||
private RecordingService recordingService;
|
||||
private LearningDashboardService learningDashboardService;
|
||||
@ -88,6 +89,7 @@ public class MeetingService implements MessageListener {
|
||||
|
||||
private long usersTimeout;
|
||||
private long waitingGuestUsersTimeout;
|
||||
private int sessionsCleanupDelayInMinutes;
|
||||
private long enteredUsersTimeout;
|
||||
|
||||
private ParamsProcessorUtil paramsProcessorUtil;
|
||||
@ -100,6 +102,7 @@ public class MeetingService implements MessageListener {
|
||||
public MeetingService() {
|
||||
meetings = new ConcurrentHashMap<String, Meeting>(8, 0.9f, 1);
|
||||
sessions = new ConcurrentHashMap<String, UserSession>(8, 0.9f, 1);
|
||||
removedSessions = new ConcurrentHashMap<String, UserSessionBasicData>(8, 0.9f, 1);
|
||||
uploadAuthzTokens = new HashMap<String, PresentationUploadToken>();
|
||||
}
|
||||
|
||||
@ -149,12 +152,16 @@ public class MeetingService implements MessageListener {
|
||||
return null;
|
||||
}
|
||||
|
||||
public UserSession getUserSessionWithAuthToken(String token) {
|
||||
public UserSession getUserSessionWithSessionToken(String token) {
|
||||
return sessions.get(token);
|
||||
}
|
||||
|
||||
public UserSessionBasicData getRemovedUserSessionWithSessionToken(String sessionToken) {
|
||||
return removedSessions.get(sessionToken);
|
||||
}
|
||||
|
||||
public Boolean getAllowRequestsWithoutSession(String token) {
|
||||
UserSession us = getUserSessionWithAuthToken(token);
|
||||
UserSession us = getUserSessionWithSessionToken(token);
|
||||
if (us == null) {
|
||||
return false;
|
||||
} else {
|
||||
@ -164,12 +171,22 @@ public class MeetingService implements MessageListener {
|
||||
}
|
||||
}
|
||||
|
||||
public UserSession removeUserSessionWithAuthToken(String token) {
|
||||
UserSession user = sessions.remove(token);
|
||||
if (user != null) {
|
||||
log.debug("Found user {} token={} to meeting {}", user.fullname, token, user.meetingID);
|
||||
public void removeUserSessionWithSessionToken(String token) {
|
||||
log.debug("Removing token={}", token);
|
||||
UserSession us = getUserSessionWithSessionToken(token);
|
||||
if (us != null) {
|
||||
log.debug("Found user {} token={} to meeting {}", us.fullname, token, us.meetingID);
|
||||
|
||||
UserSessionBasicData removedUser = new UserSessionBasicData();
|
||||
removedUser.meetingId = us.meetingID;
|
||||
removedUser.userId = us.internalUserId;
|
||||
removedUser.sessionToken = us.authToken;
|
||||
removedUser.role = us.role;
|
||||
removedSessions.put(token, removedUser);
|
||||
sessions.remove(token);
|
||||
} else {
|
||||
log.debug("Not found token={}", token);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -295,16 +312,40 @@ public class MeetingService implements MessageListener {
|
||||
notifier.sendUploadFileTooLargeMessage(presUploadToken, uploadedFileSize, maxUploadFileSize);
|
||||
}
|
||||
|
||||
private void removeUserSessions(String meetingId) {
|
||||
Iterator<Map.Entry<String, UserSession>> iterator = sessions.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<String, UserSession> entry = iterator.next();
|
||||
UserSession userSession = entry.getValue();
|
||||
|
||||
private void removeUserSessionsFromMeeting(String meetingId) {
|
||||
for (String token : sessions.keySet()) {
|
||||
UserSession userSession = sessions.get(token);
|
||||
if (userSession.meetingID.equals(meetingId)) {
|
||||
iterator.remove();
|
||||
System.out.println(token + " = " + userSession.authToken);
|
||||
removeUserSessionWithSessionToken(token);
|
||||
}
|
||||
}
|
||||
|
||||
scheduleRemovedSessionsCleanUp(meetingId);
|
||||
}
|
||||
|
||||
private void scheduleRemovedSessionsCleanUp(String meetingId) {
|
||||
Calendar cleanUpDelayCalendar = Calendar.getInstance();
|
||||
cleanUpDelayCalendar.add(Calendar.MINUTE, sessionsCleanupDelayInMinutes);
|
||||
|
||||
log.debug("Sessions for meeting={} will be removed within {} minutes.", meetingId, sessionsCleanupDelayInMinutes);
|
||||
new java.util.Timer().schedule(
|
||||
new java.util.TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
Iterator<Map.Entry<String, UserSessionBasicData>> iterator = removedSessions.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<String, UserSessionBasicData> entry = iterator.next();
|
||||
UserSessionBasicData removedUserSession = entry.getValue();
|
||||
|
||||
if (removedUserSession.meetingId.equals(meetingId)) {
|
||||
log.debug("Removed user {} session for meeting {}.",removedUserSession.userId, removedUserSession.meetingId);
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, cleanUpDelayCalendar.getTime()
|
||||
);
|
||||
}
|
||||
|
||||
private void destroyMeeting(String meetingId) {
|
||||
@ -411,8 +452,8 @@ public class MeetingService implements MessageListener {
|
||||
m.getUserInactivityInspectTimerInMinutes(), m.getUserInactivityThresholdInMinutes(),
|
||||
m.getUserActivitySignResponseDelayInMinutes(), m.getEndWhenNoModerator(), m.getEndWhenNoModeratorDelayInMinutes(),
|
||||
m.getMuteOnStart(), m.getAllowModsToUnmuteUsers(), m.getAllowModsToEjectCameras(), m.getMeetingKeepEvents(),
|
||||
m.breakoutRoomsParams, m.lockSettingsParams, m.getHtml5InstanceId(), m.getLogoutUrl(),
|
||||
m.getGroups(), m.getDisabledFeatures(), m.getNotifyRecordingIsOn(),
|
||||
m.breakoutRoomsParams, m.lockSettingsParams, m.getHtml5InstanceId(), m.getLogoutUrl(), m.getCustomLogoURL(),
|
||||
m.getBannerText(), m.getBannerColor(), m.getGroups(), m.getDisabledFeatures(), m.getNotifyRecordingIsOn(),
|
||||
m.getPresentationUploadExternalDescription(), m.getPresentationUploadExternalUrl(),
|
||||
m.getOverrideClientSettings());
|
||||
}
|
||||
@ -703,7 +744,7 @@ public class MeetingService implements MessageListener {
|
||||
}
|
||||
destroyMeeting(m.getInternalId());
|
||||
meetings.remove(m.getInternalId());
|
||||
removeUserSessions(m.getInternalId());
|
||||
removeUserSessionsFromMeeting(m.getInternalId());
|
||||
|
||||
Map<String, Object> logData = new HashMap<>();
|
||||
logData.put("meetingId", m.getInternalId());
|
||||
@ -1111,7 +1152,7 @@ public class MeetingService implements MessageListener {
|
||||
user.setRole(message.role);
|
||||
String sessionToken = getTokenByUserId(user.getInternalUserId());
|
||||
if (sessionToken != null) {
|
||||
UserSession userSession = getUserSessionWithAuthToken(sessionToken);
|
||||
UserSession userSession = getUserSessionWithSessionToken(sessionToken);
|
||||
userSession.role = message.role;
|
||||
sessions.replace(sessionToken, userSession);
|
||||
}
|
||||
@ -1363,6 +1404,10 @@ public class MeetingService implements MessageListener {
|
||||
waitingGuestUsersTimeout = value;
|
||||
}
|
||||
|
||||
public void setSessionsCleanupDelayInMinutes(int value) {
|
||||
sessionsCleanupDelayInMinutes = value;
|
||||
}
|
||||
|
||||
public void setEnteredUsersTimeout(long value) {
|
||||
enteredUsersTimeout = value;
|
||||
}
|
||||
|
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.api.domain;
|
||||
|
||||
public class UserSessionBasicData {
|
||||
public String sessionToken = null;
|
||||
public String userId = null;
|
||||
public String meetingId = null;
|
||||
public String role = null;
|
||||
|
||||
public Boolean isModerator() {
|
||||
return "MODERATOR".equalsIgnoreCase(this.role);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return meetingId + " " + userId + " " + sessionToken;
|
||||
}
|
||||
}
|
@ -22,7 +22,7 @@ public class GuestPolicyValidator implements ConstraintValidator<GuestPolicyCons
|
||||
}
|
||||
|
||||
MeetingService meetingService = ServiceUtils.getMeetingService();
|
||||
UserSession userSession = meetingService.getUserSessionWithAuthToken(sessionToken);
|
||||
UserSession userSession = meetingService.getUserSessionWithSessionToken(sessionToken);
|
||||
|
||||
if(userSession == null || !userSession.guestStatus.equals(GuestPolicy.ALLOW)) {
|
||||
return false;
|
||||
|
@ -19,7 +19,7 @@ public class UserSessionValidator implements ConstraintValidator<UserSessionCons
|
||||
return false;
|
||||
}
|
||||
|
||||
UserSession userSession = ServiceUtils.getMeetingService().getUserSessionWithAuthToken(sessionToken);
|
||||
UserSession userSession = ServiceUtils.getMeetingService().getUserSessionWithSessionToken(sessionToken);
|
||||
|
||||
if(userSession == null) {
|
||||
return false;
|
||||
|
@ -22,7 +22,7 @@ public class SessionService {
|
||||
|
||||
private void getUserSessionWithToken() {
|
||||
if(sessionToken != null) {
|
||||
userSession = meetingService.getUserSessionWithAuthToken(sessionToken);
|
||||
userSession = meetingService.getUserSessionWithSessionToken(sessionToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -124,7 +124,7 @@ public class RecordingServiceFileImpl implements RecordingService {
|
||||
if (!doneFile.exists())
|
||||
log.error("Failed to create {} file.", done);
|
||||
} catch (IOException e) {
|
||||
log.error("Exception occured when trying to create {} file", done);
|
||||
log.error("Exception occurred when trying to create {} file", done);
|
||||
}
|
||||
} else {
|
||||
log.error("{} file already exists.", done);
|
||||
@ -141,7 +141,7 @@ public class RecordingServiceFileImpl implements RecordingService {
|
||||
if (!doneFile.exists())
|
||||
log.error("Failed to create {} file.", done);
|
||||
} catch (IOException e) {
|
||||
log.error("Exception occured when trying to create {} file.", done);
|
||||
log.error("Exception occurred when trying to create {} file.", done);
|
||||
}
|
||||
} else {
|
||||
log.error("{} file already exists.", done);
|
||||
@ -158,7 +158,7 @@ public class RecordingServiceFileImpl implements RecordingService {
|
||||
if (!doneFile.exists())
|
||||
log.error("Failed to create " + done + " file.");
|
||||
} catch (IOException e) {
|
||||
log.error("Exception occured when trying to create {} file.", done);
|
||||
log.error("Exception occurred when trying to create {} file.", done);
|
||||
}
|
||||
} else {
|
||||
log.error(done + " file already exists.");
|
||||
|
@ -37,7 +37,7 @@ public class ResponseBuilder {
|
||||
try {
|
||||
cfg.setDirectoryForTemplateLoading(templatesLoc);
|
||||
} catch (IOException e) {
|
||||
log.error("Exception occured creating ResponseBuilder", e);
|
||||
log.error("Exception occurred creating ResponseBuilder", e);
|
||||
}
|
||||
setUpConfiguration();
|
||||
}
|
||||
@ -97,12 +97,12 @@ public class ResponseBuilder {
|
||||
return xmlText.toString();
|
||||
}
|
||||
|
||||
public String buildErrors(ArrayList erros, String returnCode) {
|
||||
public String buildErrors(ArrayList errors, String returnCode) {
|
||||
StringWriter xmlText = new StringWriter();
|
||||
|
||||
Map<String, Object> data = new HashMap<String, Object>();
|
||||
data.put("returnCode", returnCode);
|
||||
data.put("errorsList", erros);
|
||||
data.put("errorsList", errors);
|
||||
|
||||
processData(getTemplate("api-errors.ftlx"), data, xmlText);
|
||||
|
||||
|
@ -43,6 +43,9 @@ public interface IBbbWebApiGWApp {
|
||||
LockSettingsParams lockSettingsParams,
|
||||
Integer html5InstanceId,
|
||||
String logoutUrl,
|
||||
String customLogoURL,
|
||||
String bannerText,
|
||||
String bannerColor,
|
||||
ArrayList<Group> groups,
|
||||
ArrayList<String> disabledFeatures,
|
||||
Boolean notifyRecordingIsOn,
|
||||
|
@ -41,6 +41,6 @@ public class ConversionMessageConstants {
|
||||
public static final String CONVERSION_TIMEOUT_KEY = "CONVERSION_TIMEOUT";
|
||||
|
||||
private ConversionMessageConstants() {
|
||||
throw new IllegalStateException("ConversionMessageConstants is a utility class. Instanciation is forbidden.");
|
||||
throw new IllegalStateException("ConversionMessageConstants is a utility class. Instantiation is forbidden.");
|
||||
}
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ public class ExternalProcessExecutor {
|
||||
|
||||
try {
|
||||
if (!proc.waitFor(timeout.toMillis(), TimeUnit.MILLISECONDS)) {
|
||||
log.warn("TIMEDOUT excuting: {}", String.join(" ", cmd));
|
||||
log.warn("TIMEDOUT executing: {}", String.join(" ", cmd));
|
||||
proc.destroy();
|
||||
}
|
||||
return !proc.isAlive() && proc.exitValue() == 0;
|
||||
|
@ -150,6 +150,9 @@ class BbbWebApiGWApp(
|
||||
lockSettingsParams: LockSettingsParams,
|
||||
html5InstanceId: java.lang.Integer,
|
||||
logoutUrl: String,
|
||||
customLogoURL: String,
|
||||
bannerText: String,
|
||||
bannerColor: String,
|
||||
groups: java.util.ArrayList[Group],
|
||||
disabledFeatures: java.util.ArrayList[String],
|
||||
notifyRecordingIsOn: java.lang.Boolean,
|
||||
@ -232,7 +235,16 @@ class BbbWebApiGWApp(
|
||||
|
||||
val systemProps = SystemProps(
|
||||
html5InstanceId,
|
||||
logoutUrl
|
||||
logoutUrl,
|
||||
customLogoURL,
|
||||
bannerText match {
|
||||
case t: String => t
|
||||
case _ => ""
|
||||
},
|
||||
bannerColor match {
|
||||
case c: String => c
|
||||
case _ => ""
|
||||
},
|
||||
)
|
||||
|
||||
val groupsAsVector: Vector[GroupProps] = groups.asScala.toVector.map(g => GroupProps(g.getGroupId(), g.getName(), g.getUsersExtId().asScala.toVector))
|
||||
|
@ -73,6 +73,7 @@ class NewPresFileAvailableMsg {
|
||||
annotatedFileURI: link,
|
||||
originalFileURI: '',
|
||||
convertedFileURI: '',
|
||||
fileName: exportJob.filename,
|
||||
presId: exportJob.presId,
|
||||
fileStateType: 'Annotated',
|
||||
},
|
||||
|
@ -129,9 +129,9 @@ async function collectSharedNotes(retries = 3) {
|
||||
const padId = exportJob.presId;
|
||||
const notesFormat = 'pdf';
|
||||
|
||||
const filename = `${sanitize(exportJob.filename.replace(/\s/g, '_'))}.${notesFormat}`;
|
||||
const serverSideFilename = `${sanitize(exportJob.serverSideFilename.replace(/\s/g, '_'))}.${notesFormat}`;
|
||||
const notes_endpoint = `${config.bbbPadsAPI}/p/${padId}/export/${notesFormat}`;
|
||||
const filePath = path.join(dropbox, filename);
|
||||
const filePath = path.join(dropbox, serverSideFilename);
|
||||
|
||||
const finishedDownload = promisify(stream.finished);
|
||||
const writer = fs.createWriteStream(filePath);
|
||||
@ -157,7 +157,7 @@ async function collectSharedNotes(retries = 3) {
|
||||
}
|
||||
}
|
||||
|
||||
const notifier = new WorkerStarter({jobType, jobId, filename});
|
||||
const notifier = new WorkerStarter({jobType, jobId, serverSideFilename, filename: exportJob.filename});
|
||||
notifier.notify();
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,7 @@ const path = require('path');
|
||||
const {NewPresFileAvailableMsg} = require('../lib/utils/message-builder');
|
||||
|
||||
const {workerData} = require('worker_threads');
|
||||
const [jobType, jobId, filename] = [workerData.jobType, workerData.jobId, workerData.filename];
|
||||
const [jobType, jobId, serverSideFilename] = [workerData.jobType, workerData.jobId, workerData.serverSideFilename];
|
||||
|
||||
const logger = new Logger('presAnn Notifier Worker');
|
||||
|
||||
@ -30,13 +30,14 @@ async function notifyMeetingActor() {
|
||||
|
||||
const link = path.join('presentation',
|
||||
exportJob.parentMeetingId, exportJob.parentMeetingId,
|
||||
exportJob.presId, 'pdf', jobId, filename);
|
||||
exportJob.presId, 'pdf', jobId, serverSideFilename);
|
||||
|
||||
const notification = new NewPresFileAvailableMsg(exportJob, link);
|
||||
|
||||
logger.info(`Annotated PDF available at ${link}`);
|
||||
await client.publish(config.redis.channels.publish, notification.build());
|
||||
client.disconnect();
|
||||
|
||||
}
|
||||
|
||||
/** Upload PDF to a BBB room
|
||||
@ -63,10 +64,10 @@ async function upload(filePath) {
|
||||
if (jobType == 'PresentationWithAnnotationDownloadJob') {
|
||||
notifyMeetingActor();
|
||||
} else if (jobType == 'PresentationWithAnnotationExportJob') {
|
||||
const filePath = `${exportJob.presLocation}/pdfs/${jobId}/${filename}`;
|
||||
const filePath = `${exportJob.presLocation}/pdfs/${jobId}/${serverSideFilename}`;
|
||||
upload(filePath);
|
||||
} else if (jobType == 'PadCaptureJob') {
|
||||
const filePath = `${dropbox}/${filename}`;
|
||||
const filePath = `${dropbox}/${serverSideFilename}`;
|
||||
upload(filePath);
|
||||
} else {
|
||||
logger.error(`Notifier received unknown job type ${jobType}`);
|
||||
|
@ -703,6 +703,10 @@ function overlay_triangle(svg, annotation) {
|
||||
}
|
||||
|
||||
function overlay_text(svg, annotation) {
|
||||
if (annotation.size == null || annotation.size.length < 2) {
|
||||
return
|
||||
}
|
||||
|
||||
const [textBoxWidth, textBoxHeight] = annotation.size;
|
||||
const fontColor = color_to_hex(annotation.style.color);
|
||||
const font = determine_font_from_family(annotation.style.font);
|
||||
@ -731,30 +735,34 @@ function overlay_text(svg, annotation) {
|
||||
}
|
||||
|
||||
function overlay_annotation(svg, currentAnnotation) {
|
||||
switch (currentAnnotation.type) {
|
||||
case 'arrow':
|
||||
overlay_arrow(svg, currentAnnotation);
|
||||
break;
|
||||
case 'draw':
|
||||
overlay_draw(svg, currentAnnotation);
|
||||
break;
|
||||
case 'ellipse':
|
||||
overlay_ellipse(svg, currentAnnotation);
|
||||
break;
|
||||
case 'rectangle':
|
||||
overlay_rectangle(svg, currentAnnotation);
|
||||
break;
|
||||
case 'sticky':
|
||||
overlay_sticky(svg, currentAnnotation);
|
||||
break;
|
||||
case 'triangle':
|
||||
overlay_triangle(svg, currentAnnotation);
|
||||
break;
|
||||
case 'text':
|
||||
overlay_text(svg, currentAnnotation);
|
||||
break;
|
||||
default:
|
||||
logger.info(`Unknown annotation type ${currentAnnotation.type}.`);
|
||||
try {
|
||||
switch (currentAnnotation.type) {
|
||||
case 'arrow':
|
||||
overlay_arrow(svg, currentAnnotation);
|
||||
break;
|
||||
case 'draw':
|
||||
overlay_draw(svg, currentAnnotation);
|
||||
break;
|
||||
case 'ellipse':
|
||||
overlay_ellipse(svg, currentAnnotation);
|
||||
break;
|
||||
case 'rectangle':
|
||||
overlay_rectangle(svg, currentAnnotation);
|
||||
break;
|
||||
case 'sticky':
|
||||
overlay_sticky(svg, currentAnnotation);
|
||||
break;
|
||||
case 'triangle':
|
||||
overlay_triangle(svg, currentAnnotation);
|
||||
break;
|
||||
case 'text':
|
||||
overlay_text(svg, currentAnnotation);
|
||||
break;
|
||||
default:
|
||||
logger.info(`Unknown annotation type ${currentAnnotation.type}.`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn("Failed to overlay annotation", { failedAnnotation: currentAnnotation, error: error });
|
||||
}
|
||||
}
|
||||
|
||||
@ -886,7 +894,7 @@ async function process_presentation_annotations() {
|
||||
fs.mkdirSync(outputDir, {recursive: true});
|
||||
}
|
||||
|
||||
const filename_with_extension = `${sanitize(exportJob.filename.replace(/\s/g, '_'))}.pdf`;
|
||||
const filename_with_extension = `${sanitize(exportJob.serverSideFilename.replace(/\s/g, '_'))}.pdf`;
|
||||
|
||||
const mergePDFs = [
|
||||
'-dNOPAUSE',
|
||||
@ -904,7 +912,8 @@ async function process_presentation_annotations() {
|
||||
// Launch Notifier Worker depending on job type
|
||||
logger.info(`Saved PDF at ${outputDir}/${jobId}/${filename_with_extension}`);
|
||||
|
||||
const notifier = new WorkerStarter({jobType: exportJob.jobType, jobId, filename: filename_with_extension});
|
||||
const notifier = new WorkerStarter({jobType: exportJob.jobType, jobId,
|
||||
serverSideFilename: filename_with_extension, filename: exportJob.filename});
|
||||
notifier.notify();
|
||||
await client.disconnect();
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ object Dependencies {
|
||||
|
||||
// Libraries
|
||||
val netty = "3.2.10.Final"
|
||||
val logback = "1.2.10"
|
||||
val logback = "1.2.13"
|
||||
|
||||
// Test
|
||||
val junit = "4.12"
|
||||
|
@ -157,7 +157,7 @@ public class Client
|
||||
* Sends a FreeSWITCH API command to the server and blocks, waiting for an immediate response from the
|
||||
* server.
|
||||
* <p/>
|
||||
* The outcome of the command from the server is retured in an {@link EslMessage} object.
|
||||
* The outcome of the command from the server is returned in an {@link EslMessage} object.
|
||||
*
|
||||
* @param command API command to send
|
||||
* @param arg command arguments
|
||||
@ -454,7 +454,7 @@ public class Client
|
||||
public void run() {
|
||||
try {
|
||||
/**
|
||||
* Custom extra parsing to get conference Events for BigBlueButton / FreeSwitch intergration
|
||||
* Custom extra parsing to get conference Events for BigBlueButton / FreeSwitch integration
|
||||
*/
|
||||
//FIXME: make the conference headers constants
|
||||
if (event.getEventSubclass().equals("conference::maintenance")) {
|
||||
@ -495,7 +495,7 @@ public class Client
|
||||
listener.conferenceEventPlayFile(uniqueId, confName, confSize, event);
|
||||
return;
|
||||
} else if (eventFunc.equals("conf_api_sub_transfer") || eventFunc.equals("conference_api_sub_transfer")) {
|
||||
//Member transfered to another conf...
|
||||
//Member transferred to another conf...
|
||||
listener.conferenceEventTransfer(uniqueId, confName, confSize, event);
|
||||
return;
|
||||
} else if (eventFunc.equals("conference_add_member") || eventFunc.equals("conference_member_add")) {
|
||||
|
@ -27,7 +27,7 @@ import org.slf4j.LoggerFactory;
|
||||
/**
|
||||
* a {@link Runnable} which sends the specified {@link ChannelEvent} upstream.
|
||||
* Most users will not see this type at all because it is used by
|
||||
* {@link Executor} implementors only
|
||||
* {@link Executor} implementers only
|
||||
*
|
||||
* @author The Netty Project (netty-dev@lists.jboss.org)
|
||||
* @author Trustin Lee (tlee@redhat.com)
|
||||
|
@ -26,7 +26,7 @@ import org.slf4j.LoggerFactory;
|
||||
* the following:
|
||||
* <pre>
|
||||
<extension>
|
||||
<condition field="destination_number" expresssion="444">
|
||||
<condition field="destination_number" expression="444">
|
||||
<action application="socket" data="192.168.100.88:8084 async full"/>
|
||||
</condition>
|
||||
</extension>
|
||||
|
@ -21,7 +21,7 @@ export default function buildRedisMessage(sessionVariables: Record<string, unkno
|
||||
recording: input.recording
|
||||
};
|
||||
|
||||
//TODO check if backend velidate it
|
||||
//TODO check if backend validates it
|
||||
|
||||
// const recordObject = await RecordMeetings.findOneAsync({ meetingId });
|
||||
//
|
||||
|
@ -1,9 +1,6 @@
|
||||
import { RedisMessage } from '../types';
|
||||
import { ValidationError } from '../types/ValidationError';
|
||||
import {throwErrorIfNotPresenter} from "../imports/validation";
|
||||
|
||||
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
|
||||
throwErrorIfNotPresenter(sessionVariables);
|
||||
const eventName = `DeleteWhiteboardAnnotationsPubMsg`;
|
||||
|
||||
const routing = {
|
||||
|
@ -1,9 +1,6 @@
|
||||
import { RedisMessage } from '../types';
|
||||
import { ValidationError } from '../types/ValidationError';
|
||||
import {throwErrorIfNotPresenter} from "../imports/validation";
|
||||
|
||||
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
|
||||
throwErrorIfNotPresenter(sessionVariables);
|
||||
const eventName = `DeleteWhiteboardAnnotationsPubMsg`;
|
||||
|
||||
const routing = {
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { RedisMessage } from '../types';
|
||||
import { ValidationError } from '../types/ValidationError';
|
||||
import {throwErrorIfNotPresenter} from "../imports/validation";
|
||||
|
||||
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
|
||||
throwErrorIfNotPresenter(sessionVariables);
|
||||
const eventName = `SendWhiteboardAnnotationsPubMsg`;
|
||||
|
||||
const routing = {
|
||||
|
@ -3,21 +3,24 @@ import {throwErrorIfNotPresenter} from "../imports/validation";
|
||||
|
||||
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
|
||||
throwErrorIfNotPresenter(sessionVariables);
|
||||
const eventName = `SelectRandomViewerReqMsg`;
|
||||
const eventName = `MakePresentationDownloadReqMsg`;
|
||||
|
||||
const routing = {
|
||||
meetingId: sessionVariables['x-hasura-meetingid'] as String,
|
||||
userId: sessionVariables['x-hasura-userid'] as String
|
||||
};
|
||||
|
||||
const header = {
|
||||
const header = {
|
||||
name: eventName,
|
||||
meetingId: routing.meetingId,
|
||||
userId: routing.userId
|
||||
};
|
||||
|
||||
const body = {
|
||||
requestedBy: routing.userId
|
||||
presId: input.presentationId,
|
||||
allPages: true,
|
||||
fileStateType: input.fileStateType,
|
||||
pages: [],
|
||||
};
|
||||
|
||||
return { eventName, routing, header, body };
|
24
bbb-graphql-actions/src/actions/presentationPublishCursor.ts
Normal file
24
bbb-graphql-actions/src/actions/presentationPublishCursor.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { RedisMessage } from '../types';
|
||||
|
||||
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
|
||||
const eventName = `SendCursorPositionPubMsg`;
|
||||
|
||||
const routing = {
|
||||
meetingId: sessionVariables['x-hasura-meetingid'] as String,
|
||||
userId: sessionVariables['x-hasura-userid'] as String
|
||||
};
|
||||
|
||||
const header = {
|
||||
name: eventName,
|
||||
meetingId: routing.meetingId,
|
||||
userId: routing.userId
|
||||
};
|
||||
|
||||
const body = {
|
||||
whiteboardId: input.whiteboardId,
|
||||
xPercent: input.xPercent,
|
||||
yPercent: input.yPercent,
|
||||
};
|
||||
|
||||
return { eventName, routing, header, body };
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import { RedisMessage } from '../types';
|
||||
import { ValidationError } from '../types/ValidationError';
|
||||
import {throwErrorIfNotPresenter} from "../imports/validation";
|
||||
|
||||
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
|
||||
|
22
bbb-graphql-actions/src/actions/userSendActivitySign.ts
Normal file
22
bbb-graphql-actions/src/actions/userSendActivitySign.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { RedisMessage } from '../types';
|
||||
|
||||
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
|
||||
const eventName = `UserActivitySignCmdMsg`;
|
||||
|
||||
const routing = {
|
||||
meetingId: sessionVariables['x-hasura-meetingid'] as String,
|
||||
userId: sessionVariables['x-hasura-userid'] as String
|
||||
};
|
||||
|
||||
const header = {
|
||||
name: eventName,
|
||||
meetingId: routing.meetingId,
|
||||
userId: routing.userId
|
||||
};
|
||||
|
||||
const body = {
|
||||
userId: routing.userId
|
||||
};
|
||||
|
||||
return { eventName, routing, header, body };
|
||||
}
|
22
bbb-graphql-actions/src/actions/userSetConnectionAlive.ts
Normal file
22
bbb-graphql-actions/src/actions/userSetConnectionAlive.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { RedisMessage } from '../types';
|
||||
|
||||
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
|
||||
const eventName = `UserConnectionAliveReqMsg`;
|
||||
|
||||
const routing = {
|
||||
meetingId: sessionVariables['x-hasura-meetingid'] as String,
|
||||
userId: sessionVariables['x-hasura-userid'] as String
|
||||
};
|
||||
|
||||
const header = {
|
||||
name: eventName,
|
||||
meetingId: routing.meetingId,
|
||||
userId: routing.userId
|
||||
};
|
||||
|
||||
const body = {
|
||||
userId: routing.userId,
|
||||
};
|
||||
|
||||
return { eventName, routing, header, body };
|
||||
}
|
23
bbb-graphql-actions/src/actions/userSetConnectionRtt.ts
Normal file
23
bbb-graphql-actions/src/actions/userSetConnectionRtt.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { RedisMessage } from '../types';
|
||||
|
||||
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
|
||||
const eventName = `UserConnectionUpdateRttReqMsg`;
|
||||
|
||||
const routing = {
|
||||
meetingId: sessionVariables['x-hasura-meetingid'] as String,
|
||||
userId: sessionVariables['x-hasura-userid'] as String
|
||||
};
|
||||
|
||||
const header = {
|
||||
name: eventName,
|
||||
meetingId: routing.meetingId,
|
||||
userId: routing.userId
|
||||
};
|
||||
|
||||
const body = {
|
||||
userId: routing.userId,
|
||||
networkRttInMs: input.networkRttInMs
|
||||
};
|
||||
|
||||
return { eventName, routing, header, body };
|
||||
}
|
@ -89,6 +89,10 @@ export default function Auth() {
|
||||
}
|
||||
meeting {
|
||||
name
|
||||
ended
|
||||
learningDashboard {
|
||||
learningDashboardAccessToken
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
@ -107,7 +111,12 @@ export default function Auth() {
|
||||
{data.user_current.map((curr) => {
|
||||
console.log('user_current', curr);
|
||||
|
||||
if(curr.loggedOut) {
|
||||
if(curr.meeting.ended) {
|
||||
return <div>
|
||||
{curr.meeting.name}
|
||||
<br/><br/>
|
||||
Meeting has ended.</div>
|
||||
} else if(curr.ejected) {
|
||||
return <div>
|
||||
{curr.meeting.name}
|
||||
<br/><br/>
|
||||
@ -125,6 +134,12 @@ export default function Auth() {
|
||||
<span>{curr.guestStatusDetails.guestLobbyMessage}</span>
|
||||
<span>Your position is: {curr.guestStatusDetails.positionInWaitingQueue}</span>
|
||||
</div>
|
||||
} else if(curr.loggedOut) {
|
||||
return <div>
|
||||
{curr.meeting.name}
|
||||
<br/><br/>
|
||||
<span>You left the meeting.</span>
|
||||
</div>
|
||||
} else if(!curr.joined) {
|
||||
return <div>
|
||||
{curr.meeting.name}
|
||||
@ -139,11 +154,11 @@ export default function Auth() {
|
||||
<span>You are online, welcome {curr.name} ({curr.userId})</span>
|
||||
<button onClick={() => handleDispatchUserLeave()}>Leave Now!</button>
|
||||
|
||||
{/*<MyInfo userAuthToken={curr.authToken} />*/}
|
||||
{/*<br />*/}
|
||||
<MyInfo userAuthToken={curr.authToken} />
|
||||
<br />
|
||||
|
||||
{/*<MeetingInfo />*/}
|
||||
{/*<br />*/}
|
||||
<MeetingInfo />
|
||||
<br />
|
||||
|
||||
<TotalOfUsers />
|
||||
<TotalOfModerators />
|
||||
|
@ -1,8 +1,10 @@
|
||||
import {gql, useMutation, useSubscription} from '@apollo/client';
|
||||
import React, {useEffect} from "react";
|
||||
import React, {useEffect, useState, useRef } from "react";
|
||||
import {applyPatch} from "fast-json-patch";
|
||||
|
||||
export default function UserConnectionStatus() {
|
||||
const networkRttInMs = useRef(null); // Ref to store the current timeout
|
||||
const lastStatusUpdatedAtReceived = useRef(null); // Ref to store the current timeout
|
||||
|
||||
//example specifying where and time (new Date().toISOString())
|
||||
//but its not necessary
|
||||
@ -18,41 +20,55 @@ export default function UserConnectionStatus() {
|
||||
// `);
|
||||
|
||||
|
||||
const timeoutRef = useRef(null); // Ref to store the current timeout
|
||||
|
||||
|
||||
|
||||
//where is not necessary once user can update only its own status
|
||||
//Hasura accepts "now()" as value to timestamp fields
|
||||
const [updateUserClientResponseAtToMeAsNow] = useMutation(gql`
|
||||
mutation UpdateConnectionAliveAt($userId: String, $userClientResponseAt: timestamp) {
|
||||
update_user_connectionStatus(
|
||||
where: {userClientResponseAt: {_is_null: true}}
|
||||
_set: { userClientResponseAt: "now()" }
|
||||
) {
|
||||
affected_rows
|
||||
}
|
||||
mutation UpdateConnectionRtt($networkRttInMs: Float!) {
|
||||
userSetConnectionRtt(
|
||||
networkRttInMs: $networkRttInMs
|
||||
)
|
||||
}
|
||||
`);
|
||||
|
||||
const handleUpdateUserClientResponseAt = () => {
|
||||
updateUserClientResponseAtToMeAsNow();
|
||||
updateUserClientResponseAtToMeAsNow({
|
||||
variables: {
|
||||
networkRttInMs: networkRttInMs.current
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const [updateConnectionAliveAtToMeAsNow] = useMutation(gql`
|
||||
mutation UpdateConnectionAliveAt($userId: String, $connectionAliveAt: timestamp) {
|
||||
update_user_connectionStatus(
|
||||
where: {},
|
||||
_set: { connectionAliveAt: "now()" }
|
||||
) {
|
||||
affected_rows
|
||||
}
|
||||
mutation UpdateConnectionAliveAt {
|
||||
userSetConnectionAlive
|
||||
}
|
||||
`);
|
||||
|
||||
const handleUpdateConnectionAliveAt = () => {
|
||||
updateConnectionAliveAtToMeAsNow();
|
||||
const startTime = performance.now();
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
updateConnectionAliveAtToMeAsNow().then(result => {
|
||||
const endTime = performance.now();
|
||||
networkRttInMs.current = endTime - startTime;
|
||||
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error performing mutation:', error);
|
||||
}
|
||||
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
handleUpdateConnectionAliveAt();
|
||||
}, 25000);
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@ -66,7 +82,8 @@ export default function UserConnectionStatus() {
|
||||
user_connectionStatus {
|
||||
connectionAliveAt
|
||||
userClientResponseAt
|
||||
rttInMs
|
||||
applicationRttInMs
|
||||
networkRttInMs
|
||||
status
|
||||
statusUpdatedAt
|
||||
}
|
||||
@ -83,7 +100,8 @@ export default function UserConnectionStatus() {
|
||||
{/*<th>Id</th>*/}
|
||||
<th>connectionAliveAt</th>
|
||||
<th>userClientResponseAt</th>
|
||||
<th>rttInMs</th>
|
||||
<th>applicationRttInMs</th>
|
||||
<th>networkRttInMs</th>
|
||||
<th>status</th>
|
||||
<th>statusUpdatedAt</th>
|
||||
</tr>
|
||||
@ -92,12 +110,17 @@ export default function UserConnectionStatus() {
|
||||
{data.user_connectionStatus.map((curr) => {
|
||||
// console.log('user_connectionStatus', curr);
|
||||
|
||||
if(curr.userClientResponseAt == null) {
|
||||
// handleUpdateUserClientResponseAt();
|
||||
const delay = 500;
|
||||
setTimeout(() => {
|
||||
handleUpdateUserClientResponseAt();
|
||||
},delay);
|
||||
console.log('curr.statusUpdatedAt',curr.statusUpdatedAt);
|
||||
console.log('lastStatusUpdatedAtReceived.current',lastStatusUpdatedAtReceived.current);
|
||||
|
||||
if(curr.userClientResponseAt == null
|
||||
&& (curr.statusUpdatedAt == null || curr.statusUpdatedAt !== lastStatusUpdatedAtReceived.current)) {
|
||||
|
||||
|
||||
|
||||
lastStatusUpdatedAtReceived.current = curr.statusUpdatedAt;
|
||||
// setLastStatusUpdatedAtReceived(curr.statusUpdatedAt);
|
||||
handleUpdateUserClientResponseAt();
|
||||
}
|
||||
|
||||
return (
|
||||
@ -106,7 +129,8 @@ export default function UserConnectionStatus() {
|
||||
<button onClick={() => handleUpdateConnectionAliveAt()}>Update now!</button>
|
||||
</td>
|
||||
<td>{curr.userClientResponseAt}</td>
|
||||
<td>{curr.rttInMs}</td>
|
||||
<td>{curr.applicationRttInMs}</td>
|
||||
<td>{curr.networkRttInMs}</td>
|
||||
<td>{curr.status}</td>
|
||||
<td>{curr.statusUpdatedAt}</td>
|
||||
</tr>
|
||||
|
@ -33,7 +33,7 @@ export default function UserConnectionStatusReport() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.user_connectionStatusReport.map((curr) => {
|
||||
console.log('user_connectionStatusReport', curr);
|
||||
//console.log('user_connectionStatusReport', curr);
|
||||
return (
|
||||
<tr key={curr.user.userId}>
|
||||
<td>{curr.user.name}</td>
|
||||
|
@ -1,7 +1,9 @@
|
||||
BBB_GRAPHQL_MIDDLEWARE_LISTEN_IP=127.0.0.1
|
||||
BBB_GRAPHQL_MIDDLEWARE_LISTEN_PORT=8378
|
||||
BBB_GRAPHQL_MIDDLEWARE_REDIS_ADDRESS=127.0.0.1:6379
|
||||
BBB_GRAPHQL_MIDDLEWARE_REDIS_PASSWORD=
|
||||
BBB_GRAPHQL_MIDDLEWARE_HASURA_WS=ws://127.0.0.1:8080/v1/graphql
|
||||
BBB_GRAPHQL_MIDDLEWARE_MAX_CONN_PER_SECOND=10
|
||||
|
||||
# If you are running a cluster proxy setup, you need to configure the Origin of
|
||||
# the frontend. See https://docs.bigbluebutton.org/administration/cluster-proxy
|
||||
|
@ -18,7 +18,6 @@ RestartSec=60
|
||||
SuccessExitStatus=143
|
||||
TimeoutStopSec=5
|
||||
PermissionsStartOnly=true
|
||||
LimitNOFILE=1024
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target bigbluebutton.target
|
||||
|
@ -1,13 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/iMDT/bbb-graphql-middleware/internal/common"
|
||||
"github.com/iMDT/bbb-graphql-middleware/internal/msgpatch"
|
||||
"github.com/iMDT/bbb-graphql-middleware/internal/websrv"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -21,6 +25,9 @@ func main() {
|
||||
log.SetFormatter(&log.JSONFormatter{})
|
||||
log := log.WithField("_routine", "main")
|
||||
|
||||
common.InitUniqueID()
|
||||
log = log.WithField("graphql-middleware-uid", common.GetUniqueID())
|
||||
|
||||
log.Infof("Logger level=%v", log.Logger.Level)
|
||||
|
||||
//Clear cache from last exec
|
||||
@ -30,21 +37,46 @@ func main() {
|
||||
go websrv.StartRedisListener()
|
||||
|
||||
// Websocket listener
|
||||
// set default port
|
||||
var listenPort = 8378
|
||||
|
||||
// Check if the environment variable BBB_GRAPHQL_MIDDLEWARE_LISTEN_PORT exists
|
||||
envListenPort := os.Getenv("BBB_GRAPHQL_MIDDLEWARE_LISTEN_PORT")
|
||||
if envListenPort != "" {
|
||||
envListenPortAsInt, err := strconv.Atoi(envListenPort)
|
||||
if err == nil {
|
||||
//Define IP to listen
|
||||
listenIp := "127.0.0.1"
|
||||
if envListenIp := os.Getenv("BBB_GRAPHQL_MIDDLEWARE_LISTEN_IP"); envListenIp != "" {
|
||||
listenIp = envListenIp
|
||||
}
|
||||
|
||||
// Define port to listen on
|
||||
listenPort := 8378
|
||||
if envListenPort := os.Getenv("BBB_GRAPHQL_MIDDLEWARE_LISTEN_PORT"); envListenPort != "" {
|
||||
if envListenPortAsInt, err := strconv.Atoi(envListenPort); err == nil {
|
||||
listenPort = envListenPortAsInt
|
||||
}
|
||||
}
|
||||
|
||||
http.HandleFunc("/", websrv.ConnectionHandler)
|
||||
//Define new Connections Rate Limit
|
||||
maxConnPerSecond := 10
|
||||
if envMaxConnPerSecond := os.Getenv("BBB_GRAPHQL_MIDDLEWARE_MAX_CONN_PER_SECOND"); envMaxConnPerSecond != "" {
|
||||
if envMaxConnPerSecondAsInt, err := strconv.Atoi(envMaxConnPerSecond); err == nil {
|
||||
maxConnPerSecond = envMaxConnPerSecondAsInt
|
||||
}
|
||||
}
|
||||
rateLimiter := common.NewCustomRateLimiter(maxConnPerSecond)
|
||||
|
||||
log.Infof("listening on port %v", listenPort)
|
||||
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", listenPort), nil))
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 120*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := rateLimiter.Wait(ctx); err != nil {
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
http.Error(w, "Request cancelled or rate limit exceeded", http.StatusTooManyRequests)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
websrv.ConnectionHandler(w, r)
|
||||
})
|
||||
|
||||
log.Infof("listening on %v:%v", listenIp, listenPort)
|
||||
log.Fatal(http.ListenAndServe(fmt.Sprintf("%v:%v", listenIp, listenPort), nil))
|
||||
|
||||
}
|
||||
|
@ -14,5 +14,7 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/evanphx/json-patch v0.5.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
)
|
||||
|
@ -9,6 +9,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
|
||||
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/mattbaird/jsonpatch v0.0.0-20230413205102-771768614e91 h1:JnZSkFP1/GLwKCEuuWVhsacvbDQIVa5BRwAwd+9k2Vw=
|
||||
github.com/mattbaird/jsonpatch v0.0.0-20230413205102-771768614e91/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0=
|
||||
@ -25,6 +27,8 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
64
bbb-graphql-middleware/internal/common/CustomRateLimiter.go
Normal file
64
bbb-graphql-middleware/internal/common/CustomRateLimiter.go
Normal file
@ -0,0 +1,64 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CustomRateLimiter struct {
|
||||
tokens chan struct{}
|
||||
requestQueue chan context.Context
|
||||
}
|
||||
|
||||
func NewCustomRateLimiter(requestsPerSecond int) *CustomRateLimiter {
|
||||
rl := &CustomRateLimiter{
|
||||
tokens: make(chan struct{}, requestsPerSecond),
|
||||
requestQueue: make(chan context.Context, 20000), // Adjust the size accordingly
|
||||
}
|
||||
|
||||
go rl.refillTokens(requestsPerSecond)
|
||||
go rl.processQueue()
|
||||
|
||||
return rl
|
||||
}
|
||||
|
||||
func (rl *CustomRateLimiter) refillTokens(requestsPerSecond int) {
|
||||
ticker := time.NewTicker(time.Second / time.Duration(requestsPerSecond))
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
// Try to add a token, skip if full
|
||||
select {
|
||||
case rl.tokens <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *CustomRateLimiter) processQueue() {
|
||||
for ctx := range rl.requestQueue {
|
||||
select {
|
||||
case <-rl.tokens:
|
||||
if ctx.Err() == nil {
|
||||
// Token acquired and context not cancelled, proceed
|
||||
// Simulate processing by calling a dummy function
|
||||
// processRequest() or similar
|
||||
}
|
||||
case <-ctx.Done():
|
||||
// Context cancelled, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *CustomRateLimiter) Wait(ctx context.Context) error {
|
||||
rl.requestQueue <- ctx
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Request cancelled
|
||||
return ctx.Err()
|
||||
case <-rl.tokens:
|
||||
// Acquired token, proceed
|
||||
return nil
|
||||
}
|
||||
}
|
15
bbb-graphql-middleware/internal/common/GlobalState.go
Normal file
15
bbb-graphql-middleware/internal/common/GlobalState.go
Normal file
@ -0,0 +1,15 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var uniqueID string
|
||||
|
||||
func InitUniqueID() {
|
||||
uniqueID = uuid.New().String()
|
||||
}
|
||||
|
||||
func GetUniqueID() string {
|
||||
return uniqueID
|
||||
}
|
@ -37,6 +37,13 @@ func (s *SafeChannel) ReceiveChannel() <-chan interface{} {
|
||||
return s.ch
|
||||
}
|
||||
|
||||
func (s *SafeChannel) Closed() bool {
|
||||
s.mux.Lock()
|
||||
defer s.mux.Unlock()
|
||||
|
||||
return s.closed
|
||||
}
|
||||
|
||||
func (s *SafeChannel) Close() {
|
||||
s.mux.Lock()
|
||||
defer s.mux.Unlock()
|
||||
@ -47,6 +54,10 @@ func (s *SafeChannel) Close() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SafeChannel) Frozen() bool {
|
||||
return s.freezeFlag
|
||||
}
|
||||
|
||||
func (s *SafeChannel) FreezeChannel() {
|
||||
if !s.freezeFlag {
|
||||
s.mux.Lock()
|
||||
|
@ -18,15 +18,16 @@ const (
|
||||
)
|
||||
|
||||
type GraphQlSubscription struct {
|
||||
Id string
|
||||
Message map[string]interface{}
|
||||
Type QueryType
|
||||
OperationName string
|
||||
StreamCursorField string
|
||||
StreamCursorVariableName string
|
||||
StreamCursorCurrValue interface{}
|
||||
JsonPatchSupported bool // indicate if client support Json Patch for this subscription
|
||||
LastSeenOnHasuraConnetion string // id of the hasura connection that this query was active
|
||||
Id string
|
||||
Message map[string]interface{}
|
||||
Type QueryType
|
||||
OperationName string
|
||||
StreamCursorField string
|
||||
StreamCursorVariableName string
|
||||
StreamCursorCurrValue interface{}
|
||||
LastReceivedDataChecksum uint32
|
||||
JsonPatchSupported bool // indicate if client support Json Patch for this subscription
|
||||
LastSeenOnHasuraConnection string // id of the hasura connection that this query was active
|
||||
}
|
||||
|
||||
type BrowserConnection struct {
|
||||
@ -42,9 +43,10 @@ type BrowserConnection struct {
|
||||
}
|
||||
|
||||
type HasuraConnection struct {
|
||||
Id string // hasura connection id
|
||||
Browserconn *BrowserConnection // browser connection that originated this hasura connection
|
||||
Websocket *websocket.Conn // websocket used to connect to hasura
|
||||
Context context.Context // hasura connection context (child of browser connection context)
|
||||
ContextCancelFunc context.CancelFunc // function to cancel the hasura context (and so, the hasura connection)
|
||||
Id string // hasura connection id
|
||||
BrowserConn *BrowserConnection // browser connection that originated this hasura connection
|
||||
Websocket *websocket.Conn // websocket used to connect to hasura
|
||||
Context context.Context // hasura connection context (child of browser connection context)
|
||||
ContextCancelFunc context.CancelFunc // function to cancel the hasura context (and so, the hasura connection)
|
||||
FreezeMsgFromBrowserChan *SafeChannel // indicate that it's waiting for the return of mutations before closing connection
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user