Merge pull request #19927 from antobinary/merge-dev-alpha5

chore: Merge 3.0.0-alpha.5 into develop
This commit is contained in:
Ramón Souza 2024-03-28 13:01:05 -03:00 committed by GitHub
commit 99dc23a6f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
919 changed files with 21893 additions and 12950 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

@ -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] = {
@ -52,6 +55,33 @@ object ClientSettings extends SystemConfiguration {
} else clientSettingsFromFile
}
def getConfigPropertyValueByPathAsIntOrElse(map: Map[String, Any], path: String, alternativeValue: Int): Int = {
getConfigPropertyValueByPath(map, path) match {
case Some(configValue: Int) => configValue
case _ =>
logger.debug(s"Config `$path` with type Integer not found in clientSettings.")
alternativeValue
}
}
def getConfigPropertyValueByPathAsStringOrElse(map: Map[String, Any], path: String, alternativeValue: String): String = {
getConfigPropertyValueByPath(map, path) match {
case Some(configValue: String) => configValue
case _ =>
logger.debug(s"Config `$path` with type String not found in clientSettings.")
alternativeValue
}
}
def getConfigPropertyValueByPathAsBooleanOrElse(map: Map[String, Any], path: String, alternativeValue: Boolean): Boolean = {
getConfigPropertyValueByPath(map, path) match {
case Some(configValue: Boolean) => configValue
case _ =>
logger.debug(s"Config `$path` with type Boolean found in clientSettings.")
alternativeValue
}
}
def getConfigPropertyValueByPath(map: Map[String, Any], path: String): Option[Any] = {
val keys = path.split("\\.")
@ -90,14 +120,38 @@ object ClientSettings extends SystemConfiguration {
for {
dataChannel <- dataChannels
} yield {
if (dataChannel.contains("name") && dataChannel.contains("writePermission")) {
if (dataChannel.contains("name")) {
val channelName = dataChannel("name").toString
val writePermission = dataChannel("writePermission")
writePermission match {
case wPerm: List[String] => pluginDataChannels += (channelName -> DataChannel(channelName, wPerm))
case _ => logger.warn(s"Invalid writePermission for channel $channelName in plugin $pluginName")
val writePermission = {
if (dataChannel.contains("writePermission")) {
dataChannel("writePermission") match {
case wPerm: List[String] => wPerm
case _ => {
logger.warn(s"Invalid writePermission for channel $channelName in plugin $pluginName")
List()
}
}
} else {
logger.warn(s"Missing config writePermission for channel $channelName in plugin $pluginName")
List()
}
}
val deletePermission = {
if (dataChannel.contains("deletePermission")) {
dataChannel("deletePermission") match {
case dPerm: List[String] => dPerm
case _ => {
logger.warn(s"Invalid deletePermission for channel $channelName in plugin $pluginName")
List()
}
}
} else {
List()
}
}
pluginDataChannels += (channelName -> DataChannel(channelName, writePermission, deletePermission))
}
}
case _ => logger.warn(s"Plugin $pluginName has an invalid dataChannels format")
}
@ -112,7 +166,7 @@ object ClientSettings extends SystemConfiguration {
pluginsFromConfig
}
case class DataChannel(name: String, writePermission: List[String])
case class DataChannel(name: String, writePermission: List[String], deletePermission: List[String])
case class Plugin(name: String, url: String, dataChannels: Map[String, DataChannel])
}

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

@ -45,6 +45,14 @@ object TimerModel {
}
def setRunning(model: TimerModel, running: Boolean): Unit = {
//If it is running and will stop, calculate new Accumulated
if(getRunning(model) && !running) {
val now = System.currentTimeMillis()
val accumulated = getAccumulated(model) + Math.abs(now - getStartedAt(model)).toInt
this.setAccumulated(model, accumulated)
}
model.running = running
}

View File

@ -47,18 +47,30 @@ class WhiteboardModel extends SystemConfiguration {
}).toMap
def addAnnotations(wbId: String, userId: String, annotations: Array[AnnotationVO], isPresenter: Boolean, isModerator: Boolean): Array[AnnotationVO] = {
var annotationsAdded = Array[AnnotationVO]()
val wb = getWhiteboard(wbId)
var annotationsAdded = Array[AnnotationVO]()
var newAnnotationsMap = wb.annotationsMap
for (annotation <- annotations) {
val oldAnnotation = wb.annotationsMap.get(annotation.id)
if (!oldAnnotation.isEmpty) {
val hasPermission = isPresenter || isModerator || oldAnnotation.get.userId == userId
if (hasPermission) {
val newAnnotation = oldAnnotation.get.copy(annotationInfo = deepMerge(oldAnnotation.get.annotationInfo, annotation.annotationInfo))
// Merge old and new annotation properties
val mergedAnnotationInfo = deepMerge(oldAnnotation.get.annotationInfo, annotation.annotationInfo)
// Apply cleaning if it's an arrow annotation
val finalAnnotationInfo = if (annotation.annotationInfo.get("type").contains("arrow")) {
cleanArrowAnnotationProps(mergedAnnotationInfo)
} else {
mergedAnnotationInfo
}
val newAnnotation = oldAnnotation.get.copy(annotationInfo = finalAnnotationInfo)
newAnnotationsMap += (annotation.id -> newAnnotation)
annotationsAdded :+= annotation
PresAnnotationDAO.insertOrUpdate(newAnnotation, annotation)
annotationsAdded :+= newAnnotation
PresAnnotationDAO.insertOrUpdate(newAnnotation, newAnnotation)
println(s"Updated annotation on page [${wb.id}]. After numAnnotations=[${newAnnotationsMap.size}].")
} else {
println(s"User $userId doesn't have permission to edit annotation ${annotation.id}, ignoring...")
@ -69,40 +81,67 @@ class WhiteboardModel extends SystemConfiguration {
PresAnnotationDAO.insertOrUpdate(annotation, annotation)
println(s"Adding annotation to page [${wb.id}]. After numAnnotations=[${newAnnotationsMap.size}].")
} else {
println(s"New annotation [${annotation.id}] with no type, ignoring (probably received a remove message before and now the shape is incomplete, ignoring...")
println(s"New annotation [${annotation.id}] with no type, ignoring...")
}
}
val newWb = wb.copy(annotationsMap = newAnnotationsMap)
saveWhiteboard(newWb)
annotationsAdded
}
private def cleanArrowAnnotationProps(annotationInfo: Map[String, _]): Map[String, _] = {
annotationInfo.get("props") match {
case Some(props: Map[String, _]) =>
val cleanedProps = props.map {
case ("end", endProps: Map[String, _]) => "end" -> cleanEndOrStartProps(endProps)
case ("start", startProps: Map[String, _]) => "start" -> cleanEndOrStartProps(startProps)
case other => other
}
annotationInfo + ("props" -> cleanedProps)
case _ => annotationInfo
}
}
private def cleanEndOrStartProps(props: Map[String, _]): Map[String, _] = {
props.get("type") match {
case Some("binding") => props - ("x", "y") // Remove 'x' and 'y' for 'binding' type
case Some("point") => props - ("boundShapeId", "normalizedAnchor", "isExact") // Remove unwanted properties for 'point' type
case _ => props
}
}
def getHistory(wbId: String): Array[AnnotationVO] = {
val wb = getWhiteboard(wbId)
wb.annotationsMap.values.toArray
}
def deleteAnnotations(wbId: String, userId: String, annotationsIds: Array[String], isPresenter: Boolean, isModerator: Boolean): Array[String] = {
var annotationsIdsRemoved = Array[String]()
val wb = getWhiteboard(wbId)
var annotationsIdsRemoved = Array[String]()
var newAnnotationsMap = wb.annotationsMap
for (annotationId <- annotationsIds) {
val annotation = wb.annotationsMap.get(annotationId)
if (!annotation.isEmpty) {
if (annotation.isDefined) {
val hasPermission = isPresenter || isModerator || annotation.get.userId == userId
if (hasPermission) {
newAnnotationsMap -= annotationId
println("Removing annotation on page [" + wb.id + "]. After numAnnotations=[" + newAnnotationsMap.size + "].")
println(s"Removed annotation $annotationId on page [${wb.id}]. After numAnnotations=[${newAnnotationsMap.size}].")
annotationsIdsRemoved :+= annotationId
} else {
println("User doesn't have permission to remove this annotation, ignoring...")
println(s"User $userId doesn't have permission to remove annotation $annotationId, ignoring...")
}
} else {
println(s"Annotation $annotationId not found while trying to delete it.")
}
}
}
val newWb = wb.copy(annotationsMap = newAnnotationsMap)
saveWhiteboard(newWb)
// Update whiteboard and save
val updatedWb = wb.copy(annotationsMap = newAnnotationsMap)
saveWhiteboard(updatedWb)
annotationsIdsRemoved.map(PresAnnotationDAO.delete(wbId, userId, _))

View File

@ -4,6 +4,7 @@ import org.apache.pekko.actor.ActorContext
class AudioCaptionsApp2x(implicit val context: ActorContext)
extends UpdateTranscriptPubMsgHdlr
with TranscriptionProviderErrorMsgHdlr
with AudioFloorChangedVoiceConfEvtMsgHdlr {
}

View File

@ -0,0 +1,31 @@
package org.bigbluebutton.core.apps.audiocaptions
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.db.UserTranscriptionErrorDAO
import org.bigbluebutton.core.models.AudioCaptions
import org.bigbluebutton.core.running.LiveMeeting
trait TranscriptionProviderErrorMsgHdlr {
this: AudioCaptionsApp2x =>
def handleTranscriptionProviderErrorMsg(msg: TranscriptionProviderErrorMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
val meetingId = liveMeeting.props.meetingProp.intId
def broadcastEvent(userId: String, errorCode: String, errorMessage: String): Unit = {
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, "nodeJSapp")
val envelope = BbbCoreEnvelope(TranscriptionProviderErrorEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(TranscriptionProviderErrorEvtMsg.NAME, meetingId, userId)
val body = TranscriptionProviderErrorEvtMsgBody(errorCode, errorMessage)
val event = TranscriptionProviderErrorEvtMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
bus.outGW.send(msgEvent)
}
broadcastEvent(msg.header.userId, msg.body.errorCode, msg.body.errorMessage)
UserTranscriptionErrorDAO.insert(msg.header.userId, msg.header.meetingId, msg.body.errorCode, msg.body.errorMessage)
}
}

View File

@ -1,12 +1,13 @@
package org.bigbluebutton.core.apps.audiocaptions
import org.bigbluebutton.ClientSettings.getConfigPropertyValueByPathAsStringOrElse
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.db.CaptionDAO
import org.bigbluebutton.core.models.{AudioCaptions, Users2x}
import org.bigbluebutton.core.models.{AudioCaptions, UserState, Users2x}
import org.bigbluebutton.core.running.LiveMeeting
import java.sql.Timestamp
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
trait UpdateTranscriptPubMsgHdlr {
this: AudioCaptionsApp2x =>
@ -25,6 +26,17 @@ trait UpdateTranscriptPubMsgHdlr {
bus.outGW.send(msgEvent)
}
def sendPadUpdatePubMsg(userId: String, defaultPad: String, text: String, transcript: Boolean): Unit = {
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, "nodeJSapp")
val envelope = BbbCoreEnvelope(PadUpdatePubMsg.NAME, routing)
val header = BbbClientMsgHeader(PadUpdatePubMsg.NAME, meetingId, userId)
val body = PadUpdatePubMsgBody(defaultPad, text, transcript)
val event = PadUpdatePubMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
bus.outGW.send(msgEvent)
}
// Adapt to the current captions' recording process
def editTranscript(
userId: String,
@ -80,6 +92,28 @@ trait UpdateTranscriptPubMsgHdlr {
msg.body.locale,
msg.body.result,
)
if(msg.body.result) {
val userName = Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId).get match {
case u: UserState => u.name
case _ => "???"
}
val now = LocalDateTime.now()
val formatter = DateTimeFormatter.ofPattern("HH:mm:ss")
val formattedTime = now.format(formatter)
val userSpoke = s"\n $userName ($formattedTime): $transcript"
val defaultPad = getConfigPropertyValueByPathAsStringOrElse(
liveMeeting.clientSettings,
"public.captions.defaultPad",
alternativeValue = ""
)
sendPadUpdatePubMsg(msg.header.userId, defaultPad, userSpoke, transcript = true)
}
}
}
}

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

@ -1,5 +1,6 @@
package org.bigbluebutton.core.apps.breakout
import org.bigbluebutton.ClientSettings.{getConfigPropertyValueByPath, getConfigPropertyValueByPathAsIntOrElse}
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.{BreakoutModel, PermissionCheck, RightsManagementTrait}
import org.bigbluebutton.core.db.BreakoutRoomDAO
@ -16,6 +17,10 @@ trait CreateBreakoutRoomsCmdMsgHdlr extends RightsManagementTrait {
def handleCreateBreakoutRoomsCmdMsg(msg: CreateBreakoutRoomsCmdMsg, state: MeetingState2x): MeetingState2x = {
val minOfRooms = 2
val maxOfRooms = getConfigPropertyValueByPathAsIntOrElse(liveMeeting.clientSettings, "public.app.breakouts.breakoutRoomLimit", 16)
if (liveMeeting.props.meetingProp.disabledFeatures.contains("breakoutRooms")) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "Breakout rooms is disabled for this meeting."
@ -27,6 +32,15 @@ trait CreateBreakoutRoomsCmdMsgHdlr extends RightsManagementTrait {
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId,
reason, outGW, liveMeeting)
state
} else if(msg.body.rooms.length > maxOfRooms || msg.body.rooms.length < minOfRooms) {
log.warning(
"Attempt to create breakout rooms with invalid number of rooms (rooms: {}, max: {}, min: {}) in meeting {}",
msg.body.rooms.size,
maxOfRooms,
minOfRooms,
liveMeeting.props.meetingProp.intId
)
state
} else {
state.breakout match {
case Some(breakout) =>

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

@ -18,8 +18,8 @@ trait SendMessageToBreakoutRoomInternalMsgHdlr {
sender <- GroupChatApp.findGroupChatUser(SystemUser.ID, liveMeeting.users2x)
chat <- state.groupChats.find(GroupChatApp.MAIN_PUBLIC_CHAT)
} yield {
val groupChatMsgFromUser = GroupChatMsgFromUser(sender.id, sender.copy(name = msg.senderName), true, msg.msg)
val gcm = GroupChatApp.toGroupChatMessage(sender.copy(name = msg.senderName), groupChatMsgFromUser)
val groupChatMsgFromUser = GroupChatMsgFromUser(sender.id, sender.copy(name = msg.senderName), msg.msg)
val gcm = GroupChatApp.toGroupChatMessage(sender.copy(name = msg.senderName), groupChatMsgFromUser, emphasizedText = true)
val gcs = GroupChatApp.addGroupChatMessage(liveMeeting.props.meetingProp.intId, chat, state.groupChats, gcm, GroupChatMessageType.BREAKOUTROOM_MOD_MSG)
val event = buildGroupChatMessageBroadcastEvtMsg(

View File

@ -59,7 +59,7 @@ trait CreateGroupChatReqMsgHdlr extends SystemConfiguration {
val newState = for {
createdBy <- GroupChatApp.findGroupChatUser(msg.header.userId, liveMeeting.users2x)
} yield {
val msgs = msg.body.msg.map(m => GroupChatApp.toGroupChatMessage(createdBy, m))
val msgs = msg.body.msg.map(m => GroupChatApp.toGroupChatMessage(createdBy, m, emphasizedText = false))
val users = {
if (msg.body.access == GroupChatAccess.PRIVATE) {
val cu = msg.body.users.toSet + msg.header.userId

View File

@ -20,10 +20,10 @@ object GroupChatApp {
GroupChatFactory.create(gcId, access, createBy, users, msgs)
}
def toGroupChatMessage(sender: GroupChatUser, msg: GroupChatMsgFromUser): GroupChatMessage = {
def toGroupChatMessage(sender: GroupChatUser, msg: GroupChatMsgFromUser, emphasizedText: Boolean): GroupChatMessage = {
val now = System.currentTimeMillis()
val id = GroupChatFactory.genId()
GroupChatMessage(id, now, msg.correlationId, now, now, sender, msg.chatEmphasizedText, msg.message)
GroupChatMessage(id, now, msg.correlationId, now, now, sender, emphasizedText, msg.message)
}
def toMessageToUser(msg: GroupChatMessage): GroupChatMsgToUser = {
@ -80,8 +80,8 @@ object GroupChatApp {
sender <- GroupChatApp.findGroupChatUser(userId, liveMeeting.users2x)
chat <- state.groupChats.find(chatId)
} yield {
val gcm1 = GroupChatApp.toGroupChatMessage(sender, msg)
val emphasizedText = sender.role == Roles.MODERATOR_ROLE
val gcm1 = GroupChatApp.toGroupChatMessage(sender, msg, emphasizedText)
val gcs1 = GroupChatApp.addGroupChatMessage(liveMeeting.props.meetingProp.intId, chat, state.groupChats, gcm1)
state.update(gcs1)
}

View File

@ -1,5 +1,6 @@
package org.bigbluebutton.core.apps.groupchats
import org.bigbluebutton.ClientSettings.{ getConfigPropertyValueByPath, getConfigPropertyValueByPathAsBooleanOrElse }
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.PermissionCheck
import org.bigbluebutton.core.bus.MessageBus
@ -48,7 +49,17 @@ trait SendGroupChatMessageMsgHdlr extends HandlerHelpers {
val userIsAParticipant = chat.users.filter(u => u.id == sender.id).length > 0;
if ((chatIsPrivate && userIsAParticipant) || !chatIsPrivate) {
val gcm = GroupChatApp.toGroupChatMessage(sender, msg.body.msg)
val moderatorChatEmphasizedEnabled = getConfigPropertyValueByPathAsBooleanOrElse(
liveMeeting.clientSettings,
"public.chat.moderatorChatEmphasized",
alternativeValue = true
)
val emphasizedText = moderatorChatEmphasizedEnabled &&
!chatIsPrivate &&
sender.role == Roles.MODERATOR_ROLE
val gcm = GroupChatApp.toGroupChatMessage(sender, msg.body.msg, emphasizedText)
val gcs = GroupChatApp.addGroupChatMessage(liveMeeting.props.meetingProp.intId, chat, state.groupChats, gcm)
val event = buildGroupChatMessageBroadcastEvtMsg(

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

@ -21,7 +21,7 @@ trait PadUpdatePubMsgHdlr {
bus.outGW.send(msgEvent)
}
if (Pads.hasAccess(liveMeeting, msg.body.externalId, msg.header.userId)) {
if (Pads.hasAccess(liveMeeting, msg.body.externalId, msg.header.userId) || msg.body.transcript == true) {
Pads.getGroup(liveMeeting.pads, msg.body.externalId) match {
case Some(group) => broadcastEvent(group.groupId, msg.body.externalId, msg.body.text)
case _ =>

View File

@ -0,0 +1,60 @@
package org.bigbluebutton.core.apps.plugin
import org.bigbluebutton.ClientSettings
import org.bigbluebutton.common2.msgs.PluginDataChannelDeleteMessageMsg
import org.bigbluebutton.core.db.PluginDataChannelMessageDAO
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.models.{ Roles, Users2x }
import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting }
trait PluginDataChannelDeleteMessageMsgHdlr extends HandlerHelpers {
def handle(msg: PluginDataChannelDeleteMessageMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = {
val pluginsDisabled: Boolean = liveMeeting.props.meetingProp.disabledFeatures.contains("plugins")
val meetingId = liveMeeting.props.meetingProp.intId
for {
_ <- if (!pluginsDisabled) Some(()) else None
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId)
} yield {
val pluginsConfig = ClientSettings.getPluginsFromConfig(ClientSettings.clientSettingsFromFile)
if (!pluginsConfig.contains(msg.body.pluginName)) {
println(s"Plugin '${msg.body.pluginName}' not found.")
} else if (!pluginsConfig(msg.body.pluginName).dataChannels.contains(msg.body.dataChannel)) {
println(s"Data channel '${msg.body.dataChannel}' not found in plugin '${msg.body.pluginName}'.")
} else {
val hasPermission = for {
deletePermission <- pluginsConfig(msg.body.pluginName).dataChannels(msg.body.dataChannel).deletePermission
} yield {
deletePermission.toLowerCase match {
case "all" => true
case "moderator" => user.role == Roles.MODERATOR_ROLE
case "presenter" => user.presenter
case "sender" => {
val senderUserId = PluginDataChannelMessageDAO.getMessageSender(
meetingId,
msg.body.pluginName,
msg.body.dataChannel,
msg.body.messageId
)
senderUserId == msg.header.userId
}
case _ => false
}
}
if (!hasPermission.contains(true)) {
println(s"No permission to delete in plugin: '${msg.body.pluginName}', data channel: '${msg.body.dataChannel}'.")
} else {
PluginDataChannelMessageDAO.delete(
meetingId,
msg.body.pluginName,
msg.body.dataChannel,
msg.body.messageId
)
}
}
}
}
}

View File

@ -7,9 +7,9 @@ import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.models.{ Roles, Users2x }
import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting }
trait DispatchPluginDataChannelMessageMsgHdlr extends HandlerHelpers {
trait PluginDataChannelDispatchMessageMsgHdlr extends HandlerHelpers {
def handle(msg: DispatchPluginDataChannelMessageMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = {
def handle(msg: PluginDataChannelDispatchMessageMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = {
val pluginsDisabled: Boolean = liveMeeting.props.meetingProp.disabledFeatures.contains("plugins")
val meetingId = liveMeeting.props.meetingProp.intId

View File

@ -0,0 +1,50 @@
package org.bigbluebutton.core.apps.plugin
import org.bigbluebutton.ClientSettings
import org.bigbluebutton.common2.msgs.PluginDataChannelResetMsg
import org.bigbluebutton.core.db.PluginDataChannelMessageDAO
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.models.{ Roles, Users2x }
import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting }
trait PluginDataChannelResetMsgHdlr extends HandlerHelpers {
def handle(msg: PluginDataChannelResetMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = {
val pluginsDisabled: Boolean = liveMeeting.props.meetingProp.disabledFeatures.contains("plugins")
val meetingId = liveMeeting.props.meetingProp.intId
for {
_ <- if (!pluginsDisabled) Some(()) else None
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId)
} yield {
val pluginsConfig = ClientSettings.getPluginsFromConfig(ClientSettings.clientSettingsFromFile)
if (!pluginsConfig.contains(msg.body.pluginName)) {
println(s"Plugin '${msg.body.pluginName}' not found.")
} else if (!pluginsConfig(msg.body.pluginName).dataChannels.contains(msg.body.dataChannel)) {
println(s"Data channel '${msg.body.dataChannel}' not found in plugin '${msg.body.pluginName}'.")
} else {
val hasPermission = for {
deletePermission <- pluginsConfig(msg.body.pluginName).dataChannels(msg.body.dataChannel).deletePermission
} yield {
deletePermission.toLowerCase match {
case "all" => true
case "moderator" => user.role == Roles.MODERATOR_ROLE
case "presenter" => user.presenter
case _ => false
}
}
if (!hasPermission.contains(true)) {
println(s"No permission to delete (reset) in plugin: '${msg.body.pluginName}', data channel: '${msg.body.dataChannel}'.")
} else {
PluginDataChannelMessageDAO.reset(
meetingId,
msg.body.pluginName,
msg.body.dataChannel
)
}
}
}
}
}

View File

@ -4,7 +4,9 @@ import org.apache.pekko.actor.ActorContext
import org.apache.pekko.event.Logging
class PluginHdlrs(implicit val context: ActorContext)
extends DispatchPluginDataChannelMessageMsgHdlr {
extends PluginDataChannelDispatchMessageMsgHdlr
with PluginDataChannelDeleteMessageMsgHdlr
with PluginDataChannelResetMsgHdlr {
val log = Logging(context.system, getClass)
}

View File

@ -0,0 +1,56 @@
package org.bigbluebutton.core.apps.polls
import org.bigbluebutton.common2.domain.SimplePollResultOutVO
import org.bigbluebutton.common2.msgs.{ BbbClientMsgHeader, BbbCommonEnvCoreMsg, BbbCoreEnvelope, MessageTypes, PollUpdatedEvtMsg, PollUpdatedEvtMsgBody, Routing, UserRespondedToPollRecordMsg, UserRespondedToPollRecordMsgBody, UserRespondedToPollRespMsg, UserRespondedToPollRespMsgBody, UserRespondedToTypedPollRespMsg, UserRespondedToTypedPollRespMsgBody }
import org.bigbluebutton.core.running.OutMsgRouter
object PollHdlrHelpers {
def broadcastPollUpdatedEvent(outGW: OutMsgRouter, meetingId: String, userId: String, pollId: String, poll: SimplePollResultOutVO): Unit = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId)
val envelope = BbbCoreEnvelope(PollUpdatedEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(PollUpdatedEvtMsg.NAME, meetingId, userId)
val body = PollUpdatedEvtMsgBody(pollId, poll)
val event = PollUpdatedEvtMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
outGW.send(msgEvent)
}
def broadcastUserRespondedToTypedPollRespMsg(outGW: OutMsgRouter, meetingId: String, userId: String,
pollId: String, answer: String, sendToId: String): Unit = {
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, sendToId)
val envelope = BbbCoreEnvelope(UserRespondedToTypedPollRespMsg.NAME, routing)
val header = BbbClientMsgHeader(UserRespondedToTypedPollRespMsg.NAME, meetingId, sendToId)
val body = UserRespondedToTypedPollRespMsgBody(pollId, userId, answer)
val event = UserRespondedToTypedPollRespMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
outGW.send(msgEvent)
}
def broadcastUserRespondedToPollRecordMsg(outGW: OutMsgRouter, meetingId: String, userId: String,
pollId: String, answerId: Int, answer: String, isSecret: Boolean): Unit = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId)
val envelope = BbbCoreEnvelope(UserRespondedToPollRecordMsg.NAME, routing)
val header = BbbClientMsgHeader(UserRespondedToPollRecordMsg.NAME, meetingId, userId)
val body = UserRespondedToPollRecordMsgBody(pollId, answerId, answer, isSecret)
val event = UserRespondedToPollRecordMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
outGW.send(msgEvent)
}
def broadcastUserRespondedToPollRespMsg(outGW: OutMsgRouter, meetingId: String, userId: String,
pollId: String, answerIds: Seq[Int], sendToId: String): Unit = {
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, sendToId)
val envelope = BbbCoreEnvelope(UserRespondedToPollRespMsg.NAME, routing)
val header = BbbClientMsgHeader(UserRespondedToPollRespMsg.NAME, meetingId, sendToId)
val body = UserRespondedToPollRespMsgBody(pollId, userId, answerIds)
val event = UserRespondedToPollRespMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
outGW.send(msgEvent)
}
}

View File

@ -4,7 +4,7 @@ import org.bigbluebutton.common2.domain.SimplePollResultOutVO
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.models.Polls
import org.bigbluebutton.core.running.{ LiveMeeting }
import org.bigbluebutton.core.running.LiveMeeting
import org.bigbluebutton.core.models.Users2x
trait RespondToPollReqMsgHdlr {
@ -12,45 +12,12 @@ trait RespondToPollReqMsgHdlr {
def handle(msg: RespondToPollReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
def broadcastPollUpdatedEvent(msg: RespondToPollReqMsg, pollId: String, poll: SimplePollResultOutVO): Unit = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, liveMeeting.props.meetingProp.intId, msg.header.userId)
val envelope = BbbCoreEnvelope(PollUpdatedEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(PollUpdatedEvtMsg.NAME, liveMeeting.props.meetingProp.intId, msg.header.userId)
val body = PollUpdatedEvtMsgBody(pollId, poll)
val event = PollUpdatedEvtMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
bus.outGW.send(msgEvent)
}
def broadcastUserRespondedToPollRecordMsg(msg: RespondToPollReqMsg, pollId: String, answerId: Int, answer: String, isSecret: Boolean): Unit = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, liveMeeting.props.meetingProp.intId, msg.header.userId)
val envelope = BbbCoreEnvelope(UserRespondedToPollRecordMsg.NAME, routing)
val header = BbbClientMsgHeader(UserRespondedToPollRecordMsg.NAME, liveMeeting.props.meetingProp.intId, msg.header.userId)
val body = UserRespondedToPollRecordMsgBody(pollId, answerId, answer, isSecret)
val event = UserRespondedToPollRecordMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
bus.outGW.send(msgEvent)
}
def broadcastUserRespondedToPollRespMsg(msg: RespondToPollReqMsg, pollId: String, answerIds: Seq[Int], sendToId: String): Unit = {
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, liveMeeting.props.meetingProp.intId, sendToId)
val envelope = BbbCoreEnvelope(UserRespondedToPollRespMsg.NAME, routing)
val header = BbbClientMsgHeader(UserRespondedToPollRespMsg.NAME, liveMeeting.props.meetingProp.intId, sendToId)
val body = UserRespondedToPollRespMsgBody(pollId, msg.header.userId, answerIds)
val event = UserRespondedToPollRespMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
bus.outGW.send(msgEvent)
}
if (Polls.checkUserResponded(msg.body.pollId, msg.header.userId, liveMeeting.polls) == false) {
if (!Polls.hasUserAlreadyResponded(msg.body.pollId, msg.header.userId, liveMeeting.polls)) {
for {
(pollId: String, updatedPoll: SimplePollResultOutVO) <- Polls.handleRespondToPollReqMsg(msg.header.userId, msg.body.pollId,
msg.body.questionId, msg.body.answerIds, liveMeeting)
} yield {
broadcastPollUpdatedEvent(msg, pollId, updatedPoll)
PollHdlrHelpers.broadcastPollUpdatedEvent(bus.outGW, liveMeeting.props.meetingProp.intId, msg.header.userId, pollId, updatedPoll)
for {
poll <- Polls.getPoll(pollId, liveMeeting.polls)
} yield {
@ -58,14 +25,14 @@ trait RespondToPollReqMsgHdlr {
answerId <- msg.body.answerIds
} yield {
val answerText = poll.questions(0).answers.get(answerId).key
broadcastUserRespondedToPollRecordMsg(msg, pollId, answerId, answerText, poll.isSecret)
PollHdlrHelpers.broadcastUserRespondedToPollRecordMsg(bus.outGW, liveMeeting.props.meetingProp.intId, msg.header.userId, pollId, answerId, answerText, poll.isSecret)
}
}
for {
presenter <- Users2x.findPresenter(liveMeeting.users2x)
} yield {
broadcastUserRespondedToPollRespMsg(msg, pollId, msg.body.answerIds, presenter.intId)
PollHdlrHelpers.broadcastUserRespondedToPollRespMsg(bus.outGW, liveMeeting.props.meetingProp.intId, msg.header.userId, pollId, msg.body.answerIds, presenter.intId)
}
}
} else {

View File

@ -1,10 +1,11 @@
package org.bigbluebutton.core.apps.polls
import org.bigbluebutton.ClientSettings.getConfigPropertyValueByPathAsIntOrElse
import org.bigbluebutton.common2.domain.SimplePollResultOutVO
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.models.Polls
import org.bigbluebutton.core.running.{ LiveMeeting }
import org.bigbluebutton.core.running.LiveMeeting
import org.bigbluebutton.core.models.Users2x
trait RespondToTypedPollReqMsgHdlr {
@ -12,43 +13,60 @@ trait RespondToTypedPollReqMsgHdlr {
def handle(msg: RespondToTypedPollReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
def broadcastPollUpdatedEvent(msg: RespondToTypedPollReqMsg, pollId: String, poll: SimplePollResultOutVO): Unit = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, liveMeeting.props.meetingProp.intId, msg.header.userId)
val envelope = BbbCoreEnvelope(PollUpdatedEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(PollUpdatedEvtMsg.NAME, liveMeeting.props.meetingProp.intId, msg.header.userId)
val body = PollUpdatedEvtMsgBody(pollId, poll)
val event = PollUpdatedEvtMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
bus.outGW.send(msgEvent)
}
def broadcastUserRespondedToTypedPollRespMsg(msg: RespondToTypedPollReqMsg, pollId: String, answer: String, sendToId: String): Unit = {
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, liveMeeting.props.meetingProp.intId, sendToId)
val envelope = BbbCoreEnvelope(UserRespondedToTypedPollRespMsg.NAME, routing)
val header = BbbClientMsgHeader(UserRespondedToTypedPollRespMsg.NAME, liveMeeting.props.meetingProp.intId, sendToId)
val body = UserRespondedToTypedPollRespMsgBody(pollId, msg.header.userId, answer)
val event = UserRespondedToTypedPollRespMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
bus.outGW.send(msgEvent)
}
if (Polls.isResponsePollType(msg.body.pollId, liveMeeting.polls) &&
Polls.checkUserResponded(msg.body.pollId, msg.header.userId, liveMeeting.polls) == false &&
Polls.checkUserAddedQuestion(msg.body.pollId, msg.header.userId, liveMeeting.polls) == false) {
!Polls.hasUserAlreadyResponded(msg.body.pollId, msg.header.userId, liveMeeting.polls) &&
!Polls.hasUserAlreadyAddedTypedAnswer(msg.body.pollId, msg.header.userId, liveMeeting.polls)) {
//Truncate answer case it is longer than `maxTypedAnswerLength`
val maxTypedAnswerLength = getConfigPropertyValueByPathAsIntOrElse(liveMeeting.clientSettings, "public.poll.maxTypedAnswerLength", 45)
val answer = msg.body.answer.substring(0, Math.min(msg.body.answer.length, maxTypedAnswerLength))
val answerExists = Polls.findAnswerWithText(msg.body.pollId, msg.body.questionId, answer, liveMeeting.polls)
//Create answer if it doesn't exist
answerExists match {
case None => {
for {
(pollId: String, updatedPoll: SimplePollResultOutVO) <- Polls.handleRespondToTypedPollReqMsg(msg.header.userId, msg.body.pollId,
msg.body.questionId, msg.body.answer, liveMeeting)
msg.body.questionId, answer, liveMeeting)
} yield {
broadcastPollUpdatedEvent(msg, pollId, updatedPoll)
PollHdlrHelpers.broadcastPollUpdatedEvent(bus.outGW, liveMeeting.props.meetingProp.intId, msg.header.userId, pollId, updatedPoll)
for {
presenter <- Users2x.findPresenter(liveMeeting.users2x)
} yield {
broadcastUserRespondedToTypedPollRespMsg(msg, pollId, msg.body.answer, presenter.intId)
PollHdlrHelpers.broadcastUserRespondedToTypedPollRespMsg(bus.outGW, liveMeeting.props.meetingProp.intId, msg.header.userId, pollId, answer, presenter.intId)
}
}
}
case _ => //Do nothing, answer with same text exists already
}
//Submit the answer
Polls.findAnswerWithText(msg.body.pollId, msg.body.questionId, answer, liveMeeting.polls) match {
case Some(answerId) => {
for {
(pollId: String, updatedPoll: SimplePollResultOutVO) <- Polls.handleRespondToPollReqMsg(msg.header.userId, msg.body.pollId,
msg.body.questionId, Seq(answerId), liveMeeting)
} yield {
PollHdlrHelpers.broadcastPollUpdatedEvent(bus.outGW, liveMeeting.props.meetingProp.intId, msg.header.userId, pollId, updatedPoll)
for {
poll <- Polls.getPoll(pollId, liveMeeting.polls)
} yield {
val answerText = poll.questions(0).answers.get(answerId).key
PollHdlrHelpers.broadcastUserRespondedToPollRecordMsg(bus.outGW, liveMeeting.props.meetingProp.intId, msg.header.userId, pollId, answerId, answerText, poll.isSecret)
}
for {
presenter <- Users2x.findPresenter(liveMeeting.users2x)
} yield {
PollHdlrHelpers.broadcastUserRespondedToPollRespMsg(bus.outGW, liveMeeting.props.meetingProp.intId, msg.header.userId, pollId, Seq(answerId), presenter.intId)
}
}
}
case None => log.error("Error while trying to answer the poll {} in meeting {}: Answer not found or something went wrong while trying to create the answer.", msg.body.pollId, msg.header.meetingId)
}
} else {
log.info("Ignoring typed answer from user {} once user already added an answer to this poll {} in meeting {}", msg.header.userId, msg.body.pollId, msg.header.meetingId)
}

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

@ -30,7 +30,10 @@ trait DeactivateTimerReqMsgHdlr extends RightsManagementTrait {
val reason = "You need to be the presenter or moderator to deactivate timer"
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
} else {
TimerModel.setIsActive(liveMeeting.timerModel, false)
TimerModel.setRunning(liveMeeting.timerModel, running = false)
TimerModel.setIsActive(liveMeeting.timerModel, active = false)
TimerModel.setStopwatch(liveMeeting.timerModel, stopwatch = true)
TimerModel.reset(liveMeeting.timerModel)
TimerDAO.update(liveMeeting.props.meetingProp.intId, liveMeeting.timerModel)
broadcastEvent()
}

View File

@ -31,7 +31,7 @@ trait StartTimerReqMsgHdlr extends RightsManagementTrait {
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
} else {
TimerModel.setStartedAt(liveMeeting.timerModel, System.currentTimeMillis())
TimerModel.setRunning(liveMeeting.timerModel, true)
TimerModel.setRunning(liveMeeting.timerModel, running = true)
TimerDAO.update(liveMeeting.props.meetingProp.intId, liveMeeting.timerModel)
broadcastEvent()
}

View File

@ -33,10 +33,9 @@ trait StopTimerReqMsgHdlr extends RightsManagementTrait {
val reason = "You need to be the presenter or moderator to stop timer"
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
} else {
TimerModel.setAccumulated(liveMeeting.timerModel, msg.body.accumulated)
TimerModel.setRunning(liveMeeting.timerModel, false)
TimerModel.setRunning(liveMeeting.timerModel, running = false)
TimerDAO.update(liveMeeting.props.meetingProp.intId, liveMeeting.timerModel)
broadcastEvent(msg.body.accumulated)
broadcastEvent(TimerModel.getAccumulated(liveMeeting.timerModel))
}
}
}

View File

@ -34,6 +34,7 @@ trait SwitchTimerReqMsgHdlr extends RightsManagementTrait {
} else {
if (TimerModel.getStopwatch(liveMeeting.timerModel) != msg.body.stopwatch) {
TimerModel.setStopwatch(liveMeeting.timerModel, msg.body.stopwatch)
TimerModel.setRunning(liveMeeting.timerModel, running = false)
TimerModel.reset(liveMeeting.timerModel) //Reset on switch Stopwatch/Timer
if (msg.body.stopwatch) {
TimerModel.setTrack(liveMeeting.timerModel, "noTrack")

View File

@ -9,7 +9,7 @@ import org.bigbluebutton.core.running.OutMsgRouter
import org.bigbluebutton.core.running.MeetingActor
import org.bigbluebutton.core2.MeetingStatus2x
import org.bigbluebutton.core2.Permissions
import org.bigbluebutton.core2.message.senders.MsgBuilder
import org.bigbluebutton.core2.message.senders.{ MsgBuilder, Sender }
trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
this: MeetingActor =>
@ -237,6 +237,16 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
)
outGW.send(BbbCommonEnvCoreMsg(envelope, LockSettingsInMeetingChangedEvtMsg(header, body)))
//Refresh graphql session for all locked viewers
for {
user <- Users2x.findAll(liveMeeting.users2x)
if user.locked
if user.role == Roles.VIEWER_ROLE
regUser <- RegisteredUsers.findWithUserId(user.intId, liveMeeting.registeredUsers)
} yield {
Sender.sendForceUserGraphqlReconnectionSysMsg(liveMeeting.props.meetingProp.intId, regUser.id, regUser.sessionToken, "lockSettings_changed", outGW)
}
}
}
}

View File

@ -1,6 +1,6 @@
package org.bigbluebutton.core.apps.users
import org.bigbluebutton.ClientSettings.getConfigPropertyValueByPath
import org.bigbluebutton.ClientSettings.{ getConfigPropertyValueByPath, getConfigPropertyValueByPathAsIntOrElse }
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.RightsManagementTrait
import org.bigbluebutton.core.models.{ UserState, Users2x }
@ -31,13 +31,7 @@ trait ChangeUserReactionEmojiReqMsgHdlr extends RightsManagementTrait {
}
//Get durationInSeconds from Client config
val userReactionExpire =
getConfigPropertyValueByPath(liveMeeting.clientSettings, "public.userReaction.expire") match {
case Some(durationInSeconds: Int) => durationInSeconds
case _ =>
log.debug("Config `public.userReaction.expire` not found.")
30
}
val userReactionExpire = getConfigPropertyValueByPathAsIntOrElse(liveMeeting.clientSettings, "public.userReaction.expire", 30)
for {
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId)
newUserState <- Users2x.setReactionEmoji(liveMeeting.users2x, user.intId, msg.body.reactionEmoji, userReactionExpire)

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,41 @@
package org.bigbluebutton.core.apps.users
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.models.{ UserState, Users2x }
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
import org.bigbluebutton.core.domain.MeetingState2x
trait SetUserSpeechOptionsMsgHdlr extends RightsManagementTrait {
this: UsersApp =>
val liveMeeting: LiveMeeting
val outGW: OutMsgRouter
def handleSetUserSpeechOptionsReqMsg(msg: SetUserSpeechOptionsReqMsg): Unit = {
log.info("handleSetUserSpeechOptionsReqMsg: partialUtterances={} minUtteranceLength={} userId={}", msg.body.partialUtterances, msg.body.minUtteranceLength, msg.header.userId)
def broadcastUserSpeechOptionsChanged(user: UserState, partialUtterances: Boolean, minUtteranceLength: Int): Unit = {
val routingChange = Routing.addMsgToClientRouting(
MessageTypes.BROADCAST_TO_MEETING,
liveMeeting.props.meetingProp.intId, user.intId
)
val envelopeChange = BbbCoreEnvelope(UserSpeechOptionsChangedEvtMsg.NAME, routingChange)
val headerChange = BbbClientMsgHeader(UserSpeechOptionsChangedEvtMsg.NAME, liveMeeting.props.meetingProp.intId, user.intId)
val bodyChange = UserSpeechOptionsChangedEvtMsgBody(partialUtterances, minUtteranceLength)
val eventChange = UserSpeechOptionsChangedEvtMsg(headerChange, bodyChange)
val msgEventChange = BbbCommonEnvCoreMsg(envelopeChange, eventChange)
outGW.send(msgEventChange)
}
for {
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId)
} yield {
var changeLocale: Option[UserState] = None;
//changeLocale = Users2x.setUserSpeechLocale(liveMeeting.users2x, msg.header.userId, msg.body.locale)
broadcastUserSpeechOptionsChanged(user, msg.body.partialUtterances, msg.body.minUtteranceLength)
}
}
}

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

@ -158,16 +158,18 @@ class UsersApp(
with RegisterUserReqMsgHdlr
with ChangeUserRoleCmdMsgHdlr
with SetUserSpeechLocaleMsgHdlr
with SetUserSpeechOptionsMsgHdlr
with SyncGetUsersMeetingRespMsgHdlr
with LogoutAndEndMeetingCmdMsgHdlr
with SetRecordingStatusCmdMsgHdlr
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

@ -4,8 +4,9 @@ import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.PermissionCheck
import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.db.MeetingUsersPoliciesDAO
import org.bigbluebutton.core.models.{ RegisteredUsers, Roles, Users2x }
import org.bigbluebutton.core.running.LiveMeeting
import org.bigbluebutton.core2.message.senders.MsgBuilder
import org.bigbluebutton.core2.message.senders.{ MsgBuilder, Sender }
trait UpdateWebcamsOnlyForModeratorCmdMsgHdlr {
this: WebcamApp2x =>
@ -76,6 +77,16 @@ trait UpdateWebcamsOnlyForModeratorCmdMsgHdlr {
}
broadcastEvent(meetingId, msg.body.setBy, value)
//Refresh graphql session for all locked viewers
for {
user <- Users2x.findAll(liveMeeting.users2x)
if user.locked
if user.role == Roles.VIEWER_ROLE
regUser <- RegisteredUsers.findWithUserId(user.intId, liveMeeting.registeredUsers)
} yield {
Sender.sendForceUserGraphqlReconnectionSysMsg(liveMeeting.props.meetingProp.intId, regUser.id, regUser.sessionToken, "webcamOnlyForMod_changed", bus.outGW)
}
}
case _ =>
}

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,8 +98,25 @@ 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 {
@ -126,4 +167,50 @@ object MeetingDAO {
}
}
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

@ -1,9 +1,13 @@
package org.bigbluebutton.core.db
import PostgresProfile.api._
import org.bigbluebutton.core.db.DatabaseConnection.{db, logger}
import spray.json.JsValue
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.{Await, Future}
import scala.util.{Failure, Success}
import scala.concurrent.duration.Duration
object Permission {
val allowedRoles = List("MODERATOR","VIEWER","PRESENTER")
@ -19,6 +23,7 @@ case class PluginDataChannelMessageDbModel(
toRoles: Option[List[String]],
toUserIds: Option[List[String]],
createdAt: java.sql.Timestamp,
deletedAt: Option[java.sql.Timestamp],
)
class PluginDataChannelMessageDbTableDef(tag: Tag) extends Table[PluginDataChannelMessageDbModel](tag, None, "pluginDataChannelMessage") {
@ -31,7 +36,8 @@ class PluginDataChannelMessageDbTableDef(tag: Tag) extends Table[PluginDataChann
val toRoles = column[Option[List[String]]]("toRoles")
val toUserIds = column[Option[List[String]]]("toUserIds")
val createdAt = column[java.sql.Timestamp]("createdAt")
override def * = (meetingId, pluginName, dataChannel, payloadJson, fromUserId, toRoles, toUserIds, createdAt) <> (PluginDataChannelMessageDbModel.tupled, PluginDataChannelMessageDbModel.unapply)
val deletedAt = column[Option[java.sql.Timestamp]]("deletedAt")
override def * = (meetingId, pluginName, dataChannel, payloadJson, fromUserId, toRoles, toUserIds, createdAt, deletedAt) <> (PluginDataChannelMessageDbModel.tupled, PluginDataChannelMessageDbModel.unapply)
}
object PluginDataChannelMessageDAO {
@ -49,7 +55,8 @@ object PluginDataChannelMessageDAO {
case filtered => Some(filtered)
},
toUserIds = if(toUserIds.isEmpty) None else Some(toUserIds),
createdAt = new java.sql.Timestamp(System.currentTimeMillis())
createdAt = new java.sql.Timestamp(System.currentTimeMillis()),
deletedAt = None
)
)
).onComplete {
@ -57,4 +64,51 @@ object PluginDataChannelMessageDAO {
case Failure(e) => DatabaseConnection.logger.debug(s"Error inserting PluginDataChannelMessage: $e")
}
}
def reset(meetingId: String, pluginName: String, dataChannel: String) = {
DatabaseConnection.db.run(
TableQuery[PluginDataChannelMessageDbTableDef]
.filter(_.meetingId === meetingId)
.filter(_.pluginName === pluginName)
.filter(_.dataChannel === dataChannel)
.map(u => (u.deletedAt))
.update(Some(new java.sql.Timestamp(System.currentTimeMillis())))
).onComplete {
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated deleted=now() on pluginDataChannelMessage table!")
case Failure(e) => DatabaseConnection.logger.error(s"Error updating deleted=now() pluginDataChannelMessage: $e")
}
}
def getMessageSender(meetingId: String, pluginName: String, dataChannel: String, messageId: String): String = {
val query = sql"""SELECT "fromUserId"
FROM "pluginDataChannelMessage"
WHERE "deletedAt" is null
AND "meetingId" = ${meetingId}
AND "pluginName" = ${pluginName}
AND "dataChannel" = ${dataChannel}
AND "messageId" = ${messageId}""".as[String].headOption
Await.result(DatabaseConnection.db.run(query), Duration.Inf) match {
case Some(userId) => userId
case None => {
logger.debug("Message {} not found in database (maybe it was deleted).", messageId)
""
}
}
}
def delete(meetingId: String, pluginName: String, dataChannel: String, messageId: String) = {
DatabaseConnection.db.run(
sqlu"""UPDATE "pluginDataChannelMessage" SET
"deletedAt" = current_timestamp
WHERE "meetingId" = ${meetingId}
AND "pluginName" = ${pluginName}
AND "dataChannel" = ${dataChannel}
AND "messageId" = ${messageId}"""
).onComplete {
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated deleted=now() on pluginDataChannelMessage table!")
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating deleted=now() pluginDataChannelMessage: $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

@ -7,16 +7,20 @@ import scala.util.{ Failure, Success }
case class UserConnectionStatusDbModel(
userId: String,
meetingId: String,
connectionAliveAt: Option[java.sql.Timestamp]
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

@ -14,6 +14,7 @@ case class UserDbModel(
avatar: String = "",
color: String = "",
sessionToken: String = "",
authToken: String = "",
authed: Boolean = false,
joined: Boolean = false,
joinErrorMessage: Option[String],
@ -31,7 +32,7 @@ case class UserDbModel(
class UserDbTableDef(tag: Tag) extends Table[UserDbModel](tag, None, "user") {
override def * = (
userId,extId,meetingId,name,role,avatar,color, sessionToken, authed,joined,joinErrorCode, joinErrorMessage, banned,loggedOut,guest,guestStatus,registeredOn,excludeFromDashboard, enforceLayout) <> (UserDbModel.tupled, UserDbModel.unapply)
userId,extId,meetingId,name,role,avatar,color, sessionToken, authToken, authed,joined,joinErrorCode, joinErrorMessage, banned,loggedOut,guest,guestStatus,registeredOn,excludeFromDashboard, enforceLayout) <> (UserDbModel.tupled, UserDbModel.unapply)
val userId = column[String]("userId", O.PrimaryKey)
val extId = column[String]("extId")
val meetingId = column[String]("meetingId")
@ -40,6 +41,7 @@ class UserDbTableDef(tag: Tag) extends Table[UserDbModel](tag, None, "user") {
val avatar = column[String]("avatar")
val color = column[String]("color")
val sessionToken = column[String]("sessionToken")
val authToken = column[String]("authToken")
val authed = column[Boolean]("authed")
val joined = column[Boolean]("joined")
val joinErrorCode = column[Option[String]]("joinErrorCode")
@ -60,6 +62,7 @@ object UserDAO {
UserDbModel(
userId = regUser.id,
extId = regUser.externId,
authToken = regUser.authToken,
meetingId = meetingId,
name = regUser.name,
role = regUser.role,
@ -132,7 +135,7 @@ object UserDAO {
}
def delete(intId: String) = {
def softDelete(intId: String) = {
DatabaseConnection.db.run(
TableQuery[UserDbTableDef]
.filter(_.userId === intId)
@ -144,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

@ -0,0 +1,48 @@
package org.bigbluebutton.core.db
import org.bigbluebutton.core.models.{ VoiceUserState }
import slick.jdbc.PostgresProfile.api._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{ Failure, Success }
case class UserTranscriptionErrorDbModel(
userId: String,
meetingId: String,
errorCode: String,
errorMessage: String,
lastUpdatedAt: java.sql.Timestamp = new java.sql.Timestamp(System.currentTimeMillis())
)
class UserTranscriptionErrorDbTableDef(tag: Tag) extends Table[UserTranscriptionErrorDbModel](tag, None, "user_transcriptionError") {
override def * = (
userId, meetingId, errorCode, errorMessage, lastUpdatedAt
) <> (UserTranscriptionErrorDbModel.tupled, UserTranscriptionErrorDbModel.unapply)
val userId = column[String]("userId", O.PrimaryKey)
val meetingId = column[String]("meetingId")
val errorCode = column[String]("errorCode")
val errorMessage = column[String]("errorMessage")
val lastUpdatedAt = column[java.sql.Timestamp]("lastUpdatedAt")
}
object UserTranscriptionErrorDAO {
def insert(userId: String, meetingId: String, errorCode: String, errorMessage: String) = {
DatabaseConnection.db.run(
TableQuery[UserTranscriptionErrorDbTableDef].insertOrUpdate(
UserTranscriptionErrorDbModel(
userId = userId,
meetingId = meetingId,
errorCode = errorCode,
errorMessage = errorMessage,
lastUpdatedAt = new java.sql.Timestamp(System.currentTimeMillis()),
)
)
).onComplete {
case Success(rowsAffected) => {
DatabaseConnection.logger.debug(s"$rowsAffected row(s) inserted on user_transcriptionError table!")
}
case Failure(e) => DatabaseConnection.logger.debug(s"Error inserting user_transcriptionError: $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

@ -7,12 +7,10 @@ import org.bigbluebutton.SystemConfiguration
object AudioCaptions extends SystemConfiguration {
def setFloor(audioCaptions: AudioCaptions, userId: String) = audioCaptions.floor = userId
def isFloor(audioCaptions: AudioCaptions, userId: String) = audioCaptions.floor == userId
def isFloor(audioCaptions: AudioCaptions, userId: String) = true
def parseTranscript(transcript: String): String = {
val words = transcript.split("\\s+") // Split on whitespaces
val lines = words.grouped(transcriptWords).toArray // Group each X words into lines
lines.takeRight(transcriptLines).map(l => l.mkString(" ")).mkString("\n") // Join the last X lines
transcript
}
/*

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
}
}
@ -243,7 +243,6 @@ object Polls {
private def handleRespondToTypedPoll(poll: SimplePollResultOutVO, requesterId: String, pollId: String, questionId: Int,
answer: String, lm: LiveMeeting): Option[SimplePollResultOutVO] = {
addQuestionResponse(poll.id, questionId, answer, requesterId, lm.polls)
for {
updatedPoll <- getSimplePollResult(poll.id, lm.polls)
@ -254,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
}
@ -362,10 +362,10 @@ object Polls {
pvo
}
def checkUserResponded(pollId: String, userId: String, polls: Polls): Boolean = {
def hasUserAlreadyResponded(pollId: String, userId: String, polls: Polls): Boolean = {
polls.polls.get(pollId) match {
case Some(p) => {
if (p.getResponders().filter(p => p.userId == userId).length > 0) {
if (p.getResponders().exists(p => p.userId == userId)) {
true
} else {
false
@ -375,10 +375,10 @@ object Polls {
}
}
def checkUserAddedQuestion(pollId: String, userId: String, polls: Polls): Boolean = {
def hasUserAlreadyAddedTypedAnswer(pollId: String, userId: String, polls: Polls): Boolean = {
polls.polls.get(pollId) match {
case Some(p) => {
if (p.getTypedPollResponders().filter(responderId => responderId == userId).length > 0) {
if (p.getTypedPollResponders().contains(userId)) {
true
} else {
false
@ -401,6 +401,17 @@ object Polls {
}
}
def findAnswerWithText(pollId: String, questionId: Int, answerText: String, polls: Polls): Option[Int] = {
for {
poll <- Polls.getPoll(pollId, polls)
question <- poll.questions.find(q => q.id == questionId)
answers <- question.answers
equalAnswer <- answers.find(ans => ans.text.getOrElse("") == answerText)
} yield {
equalAnswer.id
}
}
def showPollResult(pollId: String, polls: Polls) {
polls.get(pollId) foreach {
p =>

View File

@ -176,8 +176,8 @@ case class PresentationPod(id: String, currentPresenter: String,
// 100D-checkedWidth is the maximum the page can be moved over
val checkedWidth = Math.min(widthRatio, 100D) //if (widthRatio <= 100D) widthRatio else 100D
val checkedHeight = Math.min(heightRatio, 100D)
val checkedXOffset = Math.min(xOffset, 0D)
val checkedYOffset = Math.min(yOffset, 0D)
val checkedXOffset = xOffset
val checkedYOffset = yOffset
for {
pres <- presentations.get(presentationId)

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,14 @@ 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)
case SetUserSpeechOptionsReqMsg.NAME =>
routeGenericMsg[SetUserSpeechOptionsReqMsg](envelope, jsonNode)
// Poll
case StartCustomPollReqMsg.NAME =>
@ -404,6 +408,8 @@ class ReceivedJsonMsgHandlerActor(
// AudioCaptions
case UpdateTranscriptPubMsg.NAME =>
routeGenericMsg[UpdateTranscriptPubMsg](envelope, jsonNode)
case TranscriptionProviderErrorMsg.NAME =>
routeGenericMsg[TranscriptionProviderErrorMsg](envelope, jsonNode)
// GroupChats
case GetGroupChatsReqMsg.NAME =>
@ -416,8 +422,14 @@ class ReceivedJsonMsgHandlerActor(
routeGenericMsg[CreateGroupChatReqMsg](envelope, jsonNode)
//Plugin
case DispatchPluginDataChannelMessageMsg.NAME =>
routeGenericMsg[DispatchPluginDataChannelMessageMsg](envelope, jsonNode)
case PluginDataChannelDispatchMessageMsg.NAME =>
routeGenericMsg[PluginDataChannelDispatchMessageMsg](envelope, jsonNode)
case PluginDataChannelDeleteMessageMsg.NAME =>
routeGenericMsg[PluginDataChannelDeleteMessageMsg](envelope, jsonNode)
case PluginDataChannelResetMsg.NAME =>
routeGenericMsg[PluginDataChannelResetMsg](envelope, jsonNode)
// ExternalVideo
case StartExternalVideoPubMsg.NAME =>
@ -456,6 +468,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,6 +267,12 @@ 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: SendTimeRemainingAuditInternalMsg =>
@ -395,10 +402,12 @@ 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)
case m: SetUserSpeechOptionsReqMsg => usersApp.handleSetUserSpeechOptionsReqMsg(m)
// Client requested to eject user
case m: EjectUserFromMeetingCmdMsg =>
@ -582,6 +591,7 @@ class MeetingActor(
// AudioCaptions
case m: UpdateTranscriptPubMsg => audioCaptionsApp2x.handle(m, liveMeeting, msgBus)
case m: TranscriptionProviderErrorMsg => audioCaptionsApp2x.handleTranscriptionProviderErrorMsg(m, liveMeeting, msgBus)
// GroupChat
case m: CreateGroupChatReqMsg =>
@ -594,7 +604,9 @@ class MeetingActor(
updateUserLastActivity(m.body.msg.sender.id)
// Plugin
case m: DispatchPluginDataChannelMessageMsg => pluginHdlrs.handle(m, state, liveMeeting)
case m: PluginDataChannelDispatchMessageMsg => pluginHdlrs.handle(m, state, liveMeeting)
case m: PluginDataChannelDeleteMessageMsg => pluginHdlrs.handle(m, state, liveMeeting)
case m: PluginDataChannelResetMsg => pluginHdlrs.handle(m, state, liveMeeting)
// Webcams
case m: UserBroadcastCamStartMsg => webcamApp2x.handle(m, liveMeeting, msgBus)
@ -973,6 +985,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

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

@ -1,5 +1,12 @@
package org.bigbluebutton.common2.msgs
object TranscriptionProviderErrorMsg { val NAME = "TranscriptionProviderErrorMsg" }
case class TranscriptionProviderErrorMsg(header: BbbClientMsgHeader, body: TranscriptionProviderErrorMsgBody) extends StandardMsg
case class TranscriptionProviderErrorMsgBody(
errorCode: String,
errorMessage: String,
)
// In messages
object UpdateTranscriptPubMsg { val NAME = "UpdateTranscriptPubMsg" }
case class UpdateTranscriptPubMsg(header: BbbClientMsgHeader, body: UpdateTranscriptPubMsgBody) extends StandardMsg
@ -14,6 +21,10 @@ case class UpdateTranscriptPubMsgBody(
)
// Out messages
object TranscriptionProviderErrorEvtMsg { val NAME = "TranscriptionProviderErrorEvtMsg" }
case class TranscriptionProviderErrorEvtMsg(header: BbbClientMsgHeader, body: TranscriptionProviderErrorEvtMsgBody) extends BbbCoreMsg
case class TranscriptionProviderErrorEvtMsgBody(errorCode: String, errorMessage: String)
object TranscriptUpdatedEvtMsg { val NAME = "TranscriptUpdatedEvtMsg" }
case class TranscriptUpdatedEvtMsg(header: BbbClientMsgHeader, body: TranscriptUpdatedEvtMsgBody) extends BbbCoreMsg
case class TranscriptUpdatedEvtMsgBody(transcriptId: String, transcript: String, locale: String, result: Boolean)

View File

@ -15,7 +15,7 @@ object GroupChatMessageType {
}
case class GroupChatUser(id: String, name: String = "", role: String = "VIEWER")
case class GroupChatMsgFromUser(correlationId: String, sender: GroupChatUser, chatEmphasizedText: Boolean = false, message: String)
case class GroupChatMsgFromUser(correlationId: String, sender: GroupChatUser, message: String)
case class GroupChatMsgToUser(id: String, timestamp: Long, correlationId: String, sender: GroupChatUser, chatEmphasizedText: Boolean = false, message: String)
case class GroupChatInfo(id: String, access: String, createdBy: GroupChatUser, users: Vector[GroupChatUser])

View File

@ -107,7 +107,7 @@ case class PadTailEvtMsgBody(externalId: String, tail: String)
// client -> apps
object PadUpdatePubMsg { val NAME = "PadUpdatePubMsg" }
case class PadUpdatePubMsg(header: BbbClientMsgHeader, body: PadUpdatePubMsgBody) extends StandardMsg
case class PadUpdatePubMsgBody(externalId: String, text: String)
case class PadUpdatePubMsgBody(externalId: String, text: String, transcript: Boolean)
// apps -> pads
object PadUpdateCmdMsg { val NAME = "PadUpdateCmdMsg" }

View File

@ -5,12 +5,28 @@ package org.bigbluebutton.common2.msgs
/**
* Sent from graphql-actions to bbb-akka
*/
object DispatchPluginDataChannelMessageMsg { val NAME = "DispatchPluginDataChannelMessageMsg" }
case class DispatchPluginDataChannelMessageMsg(header: BbbClientMsgHeader, body: DispatchPluginDataChannelMessageMsgBody) extends StandardMsg
case class DispatchPluginDataChannelMessageMsgBody(
object PluginDataChannelDispatchMessageMsg { val NAME = "PluginDataChannelDispatchMessageMsg" }
case class PluginDataChannelDispatchMessageMsg(header: BbbClientMsgHeader, body: PluginDataChannelDispatchMessageMsgBody) extends StandardMsg
case class PluginDataChannelDispatchMessageMsgBody(
pluginName: String,
dataChannel: String,
payloadJson: String,
toRoles: List[String],
toUserIds: List[String],
)
object PluginDataChannelDeleteMessageMsg { val NAME = "PluginDataChannelDeleteMessageMsg" }
case class PluginDataChannelDeleteMessageMsg(header: BbbClientMsgHeader, body: PluginDataChannelDeleteMessageMsgBody) extends StandardMsg
case class PluginDataChannelDeleteMessageMsgBody(
pluginName: String,
dataChannel: String,
messageId: String
)
object PluginDataChannelResetMsg { val NAME = "PluginDataChannelResetMsg" }
case class PluginDataChannelResetMsg(header: BbbClientMsgHeader, body: PluginDataChannelResetMsgBody) extends StandardMsg
case class PluginDataChannelResetMsgBody(
pluginName: String,
dataChannel: String
)

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

@ -19,7 +19,7 @@ case class StartTimerReqMsgBody()
object StopTimerReqMsg { val NAME = "StopTimerReqMsg" }
case class StopTimerReqMsg(header: BbbClientMsgHeader, body: StopTimerReqMsgBody) extends StandardMsg
case class StopTimerReqMsgBody(accumulated: Int)
case class StopTimerReqMsgBody()
object SwitchTimerReqMsg { val NAME = "SwitchTimerReqMsg" }
case class SwitchTimerReqMsg(header: BbbClientMsgHeader, body: SwitchTimerReqMsgBody) extends StandardMsg

View File

@ -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)
@ -532,3 +532,11 @@ case class SetUserSpeechLocaleReqMsgBody(locale: String, provider: String)
object UserSpeechLocaleChangedEvtMsg { val NAME = "UserSpeechLocaleChangedEvtMsg" }
case class UserSpeechLocaleChangedEvtMsg(header: BbbClientMsgHeader, body: UserSpeechLocaleChangedEvtMsgBody) extends BbbCoreMsg
case class UserSpeechLocaleChangedEvtMsgBody(locale: String, provider: String)
object SetUserSpeechOptionsReqMsg { val NAME = "SetUserSpeechOptionsReqMsg" }
case class SetUserSpeechOptionsReqMsg(header: BbbClientMsgHeader, body: SetUserSpeechOptionsReqMsgBody) extends StandardMsg
case class SetUserSpeechOptionsReqMsgBody(partialUtterances: Boolean, minUtteranceLength: Int)
object UserSpeechOptionsChangedEvtMsg { val NAME = "UserSpeechOptionsChangedEvtMsg" }
case class UserSpeechOptionsChangedEvtMsg(header: BbbClientMsgHeader, body: UserSpeechOptionsChangedEvtMsgBody) extends BbbCoreMsg
case class UserSpeechOptionsChangedEvtMsgBody(partialUtterances: Boolean, minUtteranceLength: Int)

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

@ -103,10 +103,10 @@ homepage := Some(url("http://www.bigbluebutton.org"))
libraryDependencies ++= Seq(
"javax.validation" % "validation-api" % "2.0.1.Final",
"org.springframework.boot" % "spring-boot-starter-validation" % "2.7.12",
"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

@ -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,17 +312,41 @@ 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)) {
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) {
gw.destroyMeeting(new DestroyMeetingMessage(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);
}
@ -1184,6 +1225,10 @@ public class MeetingService implements MessageListener {
processGuestStatusChangedEventMsg((GuestStatusChangedEventMsg) message);
} else if (message instanceof GuestPolicyChanged) {
processGuestPolicyChanged((GuestPolicyChanged) message);
} else if (message instanceof LockSettingsChanged) {
processLockSettingsChanged((LockSettingsChanged) message);
} else if (message instanceof WebcamsOnlyForModeratorChanged) {
processWebcamsOnlyForModeratorChanged((WebcamsOnlyForModeratorChanged) message);
} else if (message instanceof GuestLobbyMessageChanged) {
processGuestLobbyMessageChanged((GuestLobbyMessageChanged) message);
} else if (message instanceof PrivateGuestLobbyMessageChanged) {
@ -1210,6 +1255,32 @@ public class MeetingService implements MessageListener {
}
}
public void processLockSettingsChanged(LockSettingsChanged msg) {
Meeting m = getMeeting(msg.meetingId);
if (m != null) {
m.setLockSettings(
new LockSettingsParams(
msg.disableCam,
msg.disableMic,
msg.disablePrivateChat,
msg.disablePublicChat,
msg.disableNotes,
msg.hideUserList,
msg.lockOnJoin,
msg.lockOnJoinConfigurable,
msg.hideViewersCursor,
msg.hideViewersAnnotation)
);
}
}
public void processWebcamsOnlyForModeratorChanged(WebcamsOnlyForModeratorChanged msg) {
Meeting m = getMeeting(msg.meetingId);
if (m != null) {
m.setWebcamsOnlyForModerator(msg.webcamsOnlyForModerator);
}
}
public void processPositionInWaitingQueueUpdated(PositionInWaitingQueueUpdated msg) {
Meeting m = getMeeting(msg.meetingId);
HashMap<String,String> guestUsers = msg.guests;
@ -1333,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

@ -70,6 +70,8 @@ public class ParamsProcessorUtil {
private String defaultServerUrl;
private int defaultNumDigitsForTelVoice;
private String defaultHTML5ClientUrl;
private String graphqlWebsocketUrl;
private String defaultGuestWaitURL;
private Boolean allowRequestsWithoutSession = false;
private Integer defaultHttpSessionTimeout = 14400;
@ -864,6 +866,10 @@ public class ParamsProcessorUtil {
return defaultHTML5ClientUrl;
}
public String getGraphqlWebsocketUrl() {
return graphqlWebsocketUrl;
}
public String getDefaultGuestWaitURL() {
return defaultGuestWaitURL;
}
@ -1217,6 +1223,10 @@ public class ParamsProcessorUtil {
this.defaultHTML5ClientUrl = defaultHTML5ClientUrl;
}
public void setGraphqlWebsocketUrl(String graphqlWebsocketUrl) {
this.graphqlWebsocketUrl = graphqlWebsocketUrl.replace("https://","wss://");
}
public void setDefaultGuestWaitURL(String url) {
this.defaultGuestWaitURL = url;
}

View File

@ -110,7 +110,7 @@ public class Meeting {
private Integer endWhenNoModeratorDelayInMinutes = 1;
public final BreakoutRoomsParams breakoutRoomsParams;
public final LockSettingsParams lockSettingsParams;
public LockSettingsParams lockSettingsParams;
public final Integer maxUserConcurrentAccesses;
@ -475,6 +475,14 @@ public class Meeting {
return guestPolicy;
}
public void setLockSettings(LockSettingsParams lockSettingsParams) {
this.lockSettingsParams = lockSettingsParams;
}
public void setWebcamsOnlyForModerator(Boolean webcamsOnlyForModerator) {
this.webcamsOnlyForModerator = webcamsOnlyForModerator;
}
public void setGuestLobbyMessage(String message) {
guestLobbyMessage = message;
}

View File

@ -138,6 +138,10 @@ public class User {
return "MODERATOR".equalsIgnoreCase(this.role);
}
public boolean isLocked() {
return this.locked;
}
public void setStatus(String key, String value){
this.status.put(key, 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

@ -0,0 +1,40 @@
package org.bigbluebutton.api.messaging.messages;
public class LockSettingsChanged implements IMessage {
public final String meetingId;
public final Boolean disableCam;
public final Boolean disableMic;
public final Boolean disablePrivateChat;
public final Boolean disablePublicChat;
public final Boolean disableNotes;
public final Boolean hideUserList;
public final Boolean lockOnJoin;
public final Boolean lockOnJoinConfigurable;
public final Boolean hideViewersCursor;
public final Boolean hideViewersAnnotation;
public LockSettingsChanged(String meetingId,
Boolean disableCam,
Boolean disableMic,
Boolean disablePrivateChat,
Boolean disablePublicChat,
Boolean disableNotes,
Boolean hideUserList,
Boolean lockOnJoin,
Boolean lockOnJoinConfigurable,
Boolean hideViewersCursor,
Boolean hideViewersAnnotation) {
this.meetingId = meetingId;
this.disableCam = disableCam;
this.disableMic = disableMic;
this.disablePrivateChat = disablePrivateChat;
this.disablePublicChat = disablePublicChat;
this.disableNotes = disableNotes;
this.hideUserList = hideUserList;
this.lockOnJoin = lockOnJoin;
this.lockOnJoinConfigurable = lockOnJoinConfigurable;
this.hideViewersCursor = hideViewersCursor;
this.hideViewersAnnotation = hideViewersAnnotation;
}
}

View File

@ -0,0 +1,11 @@
package org.bigbluebutton.api.messaging.messages;
public class WebcamsOnlyForModeratorChanged implements IMessage {
public final String meetingId;
public final Boolean webcamsOnlyForModerator;
public WebcamsOnlyForModeratorChanged(String meetingId, Boolean webcamsOnlyForModerator) {
this.meetingId = meetingId;
this.webcamsOnlyForModerator = webcamsOnlyForModerator;
}
}

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

@ -14,6 +14,9 @@ import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
@ -76,6 +79,11 @@ public class ValidationService {
if(request == null) {
violations.put("validationError", "Request not recognized");
} else if(params.containsKey("presentationUploadExternalUrl")) {
String urlToValidate = params.get("presentationUploadExternalUrl")[0];
if(!this.isValidURL(urlToValidate)) {
violations.put("validationError", "Param 'presentationUploadExternalUrl' is not a valid URL");
}
} else {
request.populateFromParamsMap(params);
violations = performValidation(request);
@ -84,6 +92,15 @@ public class ValidationService {
return violations;
}
boolean isValidURL(String url) {
try {
new URL(url).toURI();
return true;
} catch (MalformedURLException | URISyntaxException e) {
return false;
}
}
private Request initializeRequest(ApiCall apiCall, Map<String, String[]> params, String queryString) {
Request request = null;
Checksum checksum;

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