Merge remote-tracking branch 'upstream/v3.0.x-release' into migrate-poll-creation

This commit is contained in:
Tainan Felipe 2024-03-27 09:40:02 -03:00
commit 3859a7c854
720 changed files with 13163 additions and 7455 deletions

2
.gitignore vendored
View File

@ -23,3 +23,5 @@ cache/*
artifacts/*
bbb-presentation-video.zip
bbb-presentation-video
bbb-graphql-actions-adapter-server/
bigbluebutton-html5/public/locales/index.json

View File

@ -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.

View File

@ -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")

View File

@ -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"

View File

@ -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)

View File

@ -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
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 = {

View File

@ -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)

View File

@ -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)
}
}
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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
}
}
}

View File

@ -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

View File

@ -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)
}

View File

@ -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
}
}
}

View File

@ -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

View File

@ -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)

View File

@ -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)
}

View File

@ -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 {

View File

@ -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")
}
}
}

View File

@ -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 {

View File

@ -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")
}
}
}

View File

@ -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)

View File

@ -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())))

View File

@ -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")
}
}
}

View File

@ -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"
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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 = ""
)

View File

@ -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

View File

@ -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"

View File

@ -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 = {

View File

@ -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)
}
}

View File

@ -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) {

View File

@ -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.

View File

@ -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)

View File

@ -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)

View File

@ -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))
}
}

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -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()

View File

@ -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
}

View File

@ -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 {

View File

@ -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"

View File

@ -70,6 +70,9 @@ case class LockSettingsProps(
case class SystemProps(
html5InstanceId: Int,
logoutUrl: String,
customLogoURL: String,
bannerText: String,
bannerColor: String,
)
case class GroupProps(

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -22,6 +22,7 @@ case class ExportJob(
jobId: String,
jobType: String,
filename: String,
serverSideFilename: String,
presId: String,
presLocation: String,
allPages: Boolean,

View File

@ -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",

View File

@ -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.");
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;

View File

@ -22,7 +22,7 @@ public class SessionService {
private void getUserSessionWithToken() {
if(sessionToken != null) {
userSession = meetingService.getUserSessionWithAuthToken(sessionToken);
userSession = meetingService.getUserSessionWithSessionToken(sessionToken);
}
}

View File

@ -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.");

View File

@ -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);

View File

@ -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,

View File

@ -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.");
}
}

View File

@ -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;

View File

@ -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))

View File

@ -73,6 +73,7 @@ class NewPresFileAvailableMsg {
annotatedFileURI: link,
originalFileURI: '',
convertedFileURI: '',
fileName: exportJob.filename,
presId: exportJob.presId,
fileStateType: 'Annotated',
},

View File

@ -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();
}

View File

@ -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}`);

View File

@ -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();
}

View File

@ -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"

View File

@ -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")) {

View File

@ -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)

View File

@ -26,7 +26,7 @@ import org.slf4j.LoggerFactory;
* the following:
* <pre>
&lt;extension&gt;
&lt;condition field="destination_number" expresssion="444"&gt;
&lt;condition field="destination_number" expression="444"&gt;
&lt;action application="socket" data="192.168.100.88:8084 async full"/&gt;
&lt;/condition&gt;
&lt;/extension&gt;

View File

@ -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 });
//

View File

@ -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 = {

View File

@ -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 = {

View File

@ -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 = {

View File

@ -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 };

View 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 };
}

View File

@ -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 {

View 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 };
}

View 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 };
}

View 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 };
}

View File

@ -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 />

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -18,7 +18,6 @@ RestartSec=60
SuccessExitStatus=143
TimeoutStopSec=5
PermissionsStartOnly=true
LimitNOFILE=1024
[Install]
WantedBy=multi-user.target bigbluebutton.target

View File

@ -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))
}

View File

@ -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
)

View File

@ -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=

View 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
}
}

View 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
}

View File

@ -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()

View File

@ -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