Merge branch 'v3.0.x-release' into cleanup/runuser

This commit is contained in:
Anton Georgiev 2024-02-20 13:38:33 -05:00 committed by GitHub
commit c9ce945aa5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
875 changed files with 24478 additions and 39685 deletions

View File

@ -13,7 +13,7 @@ This issue tracker is only for bbb development or docs related issues.-->
**Link to the portion of the docs that is out of date**
If applicable, link to the section of the docs that is out of date.
**Describe what you belive the correct version should be**
**Describe what you believe the correct version should be**
**Screenshots**
If applicable, add screenshots to help explain your concern.

106
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,106 @@
version: 2
updates:
# maintaining legacy branch
# no configuration for now
# current branch
## excluding bigbluebutton-tests/playwright, bigbluebutton-tests/puppeteer, docs
- package-ecosystem: npm
directory: "/bbb-export-annotations"
target-branch: "v2.7.x-release"
schedule:
interval: daily
open-pull-requests-limit: 0 # zero means only security pull requests and no (optional) version updates
- package-ecosystem: npm
directory: "/bigbluebutton-html5"
target-branch: "v2.7.x-release"
schedule:
interval: daily
open-pull-requests-limit: 0 # zero means only security pull requests and no (optional) version updates
- package-ecosystem: npm
directory: "/bbb-learning-dashboard"
target-branch: "v2.7.x-release"
schedule:
interval: daily
open-pull-requests-limit: 0 # zero means only security pull requests and no (optional) version updates
- package-ecosystem: gradle
directory: "/bigbluebutton-web"
target-branch: "v2.7.x-release"
schedule:
interval: daily
open-pull-requests-limit: 0 # zero means only security pull requests and no (optional) version updates
- package-ecosystem: bundler
directory: "/record-and-playback/core"
target-branch: "v2.7.x-release"
schedule:
interval: daily
open-pull-requests-limit: 0 # zero means only security pull requests and no (optional) version updates
vendor: true
- package-ecosystem: maven
directory: "/bbb-fsesl-client"
target-branch: "v2.7.x-release"
schedule:
interval: daily
open-pull-requests-limit: 0 # zero means only security pull requests and no (optional) version updates
# upcoming release branch
## excluding bigbluebutton-tests/playwright, bigbluebutton-tests/puppeteer, docs, bbb-graphql-client-test
- package-ecosystem: npm
directory: "/bbb-graphql-actions"
target-branch: "v3.0.x-release"
schedule:
interval: daily
open-pull-requests-limit: 20 # both security and versions updates
- package-ecosystem: npm
directory: "/bbb-export-annotations"
target-branch: "v3.0.x-release"
schedule:
interval: daily
open-pull-requests-limit: 20 # both security and versions updates
- package-ecosystem: npm
directory: "/bigbluebutton-html5"
target-branch: "v3.0.x-release"
schedule:
interval: daily
open-pull-requests-limit: 20 # both security and versions updates
- package-ecosystem: npm
directory: "/bbb-learning-dashboard"
target-branch: "v3.0.x-release"
schedule:
interval: daily
open-pull-requests-limit: 20 # both security and versions updates
- package-ecosystem: gradle
directory: "/bigbluebutton-web"
target-branch: "v3.0.x-release"
schedule:
interval: daily
open-pull-requests-limit: 20 # both security and versions updates
- package-ecosystem: bundler
directory: "/record-and-playback/core"
target-branch: "v3.0.x-release"
schedule:
interval: daily
open-pull-requests-limit: 20 # both security and versions updates
- package-ecosystem: maven
directory: "/bbb-fsesl-client"
target-branch: "v3.0.x-release"
schedule:
interval: daily
open-pull-requests-limit: 20 # both security and versions updates
- package-ecosystem: gomod
directory: "/bbb-graphql-middleware"
target-branch: "v3.0.x-release"
schedule:
interval: daily
open-pull-requests-limit: 20 # both security and versions updates
- package-ecosystem: gomod
directory: "/bbb-graphql-server"
target-branch: "v3.0.x-release"
schedule:
interval: daily
open-pull-requests-limit: 20 # both security and versions updates
# upstream (default) branch
# no configuration for now

View File

@ -54,7 +54,7 @@ jobs:
build-list: bbb-playback bbb-playback-notes bbb-playback-podcast bbb-playback-presentation bbb-playback-screenshare bbb-playback-video bbb-record-core
- package: bbb-graphql-server
build-name: bbb-graphql-server
build-list: bbb-graphql-server bbb-graphql-middleware bbb-graphql-actions-adapter-server
build-list: bbb-graphql-server bbb-graphql-middleware bbb-graphql-actions
- package: bbb-etherpad
cache-files-list: bbb-etherpad.placeholder.sh
cache-urls-list: https://api.github.com/repos/mconf/ep_pad_ttl/commits https://api.github.com/repos/alangecker/bbb-etherpad-plugin/commits https://api.github.com/repos/mconf/ep_redis_publisher/commits https://api.github.com/repos/alangecker/bbb-etherpad-skin/commits
@ -279,13 +279,18 @@ jobs:
npx playwright install
'
- name: Run tests
working-directory: ./bigbluebutton-tests/playwright
uses: nick-fields/retry@v2
with:
timeout_minutes: 25
max_attempts: 3
command: |
cd ./bigbluebutton-tests/playwright
npm run test-chromium-ci -- --shard ${{ matrix.shard }}
env:
NODE_EXTRA_CA_CERTS: /usr/local/share/ca-certificates/bbb-dev/bbb-dev-ca.crt
ACTIONS_RUNNER_DEBUG: true
BBB_URL: https://bbb-ci.test/bigbluebutton/api
BBB_SECRET: bbbci
run: npm run test-chromium-ci -- --shard ${{ matrix.shard }}
- name: Run Firefox tests
working-directory: ./bigbluebutton-tests/playwright
if: |

1
.gitignore vendored
View File

@ -23,3 +23,4 @@ cache/*
artifacts/*
bbb-presentation-video.zip
bbb-presentation-video
bbb-graphql-actions-adapter-server/

View File

@ -1,20 +1,20 @@
BigBlueButton
=============
BigBlueButton is an open source virtual classroom designed to help teachers teach and learners learn.
BigBlueButton is an open-source virtual classroom designed to help teachers teach and learners learn.
BigBlueButton supports real-time sharing of audio, video, slides (with whiteboard annotations), chat, and the screen. Instructors can engage remote students with polling, emojis, multi-user whiteboard, shared notes, and breakout rooms. During the session, BigBlueButton generates analytics that are visible to moderators in the Learning Analytics Dashboard.
BigBlueButton supports real-time sharing of audio, video, slides (with whiteboard annotations), chat, and the screen. Instructors can engage remote students with polling, emojis, multi-user whiteboards, shared notes, and breakout rooms. During the session, BigBlueButton generates analytics that are visible to moderators in the Learning Analytics Dashboard.
Presenters can record and playback content for later sharing with others.
We designed BigBlueButton for online learning, it can be used for many other applicationsas well). The educational use cases for BigBlueButton are
We designed BigBlueButton for online learning, it can be used for many other applications as well. The educational use cases for BigBlueButton are
* Online tutoring (one-to-one)
* Flipped classrooms (recording content ahead of your session)
* Group collaboration (many-to-many)
* Online classes (one-to-many)
The latest version is BigBlueButton 2.6. You can install BigBlueButton 2.6 on Ubuntu 20.04 using [bbb-install.sh](https://github.com/bigbluebutton/bbb-install) within 30 minutes (or your money back 😉).
The latest version is BigBlueButton 2.7. You can install BigBlueButton 2.6 on Ubuntu 20.04 using [bbb-install.sh](https://github.com/bigbluebutton/bbb-install) within 30 minutes (or your money back 😉).
For full technical documentation BigBlueButton -- including architecture, features, API, and GreenLight (the default front-end) -- see [https://docs.bigbluebutton.org/](https://docs.bigbluebutton.org/).
For full technical documentation of BigBlueButton -- including architecture, features, API, and GreenLight (the default front-end) -- see [https://docs.bigbluebutton.org/](https://docs.bigbluebutton.org/).
BigBlueButton and the BigBlueButton Logo are trademarks of [BigBlueButton Inc](https://bigbluebutton.org) .
BigBlueButton and the BigBlueButton Logo are trademarks of [BigBlueButton Inc](https://bigbluebutton.org).

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

@ -52,6 +52,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,13 +117,37 @@ 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 +163,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

@ -75,15 +75,16 @@ class BigBlueButtonActor(
private def handleBbbCommonEnvCoreMsg(msg: BbbCommonEnvCoreMsg): Unit = {
msg.core match {
case m: CreateMeetingReqMsg => handleCreateMeetingReqMsg(m)
case m: RegisterUserReqMsg => handleRegisterUserReqMsg(m)
case m: GetAllMeetingsReqMsg => handleGetAllMeetingsReqMsg(m)
case m: GetRunningMeetingsReqMsg => handleGetRunningMeetingsReqMsg(m)
case m: CheckAlivePingSysMsg => handleCheckAlivePingSysMsg(m)
case m: ValidateConnAuthTokenSysMsg => handleValidateConnAuthTokenSysMsg(m)
case _: UserGraphqlConnectionStablishedSysMsg => //Ignore
case _: UserGraphqlConnectionClosedSysMsg => //Ignore
case _ => log.warning("Cannot handle " + msg.envelope.name)
case m: CreateMeetingReqMsg => handleCreateMeetingReqMsg(m)
case m: RegisterUserReqMsg => handleRegisterUserReqMsg(m)
case m: GetAllMeetingsReqMsg => handleGetAllMeetingsReqMsg(m)
case m: GetRunningMeetingsReqMsg => handleGetRunningMeetingsReqMsg(m)
case m: CheckAlivePingSysMsg => handleCheckAlivePingSysMsg(m)
case m: ValidateConnAuthTokenSysMsg => handleValidateConnAuthTokenSysMsg(m)
case _: UserGraphqlConnectionEstablishedSysMsg => //Ignore
case _: UserGraphqlConnectionClosedSysMsg => //Ignore
case _: CheckGraphqlMiddlewareAlivePongSysMsg => //Ignore
case _ => log.warning("Cannot handle " + msg.envelope.name)
}
}
@ -189,9 +190,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

@ -95,7 +95,7 @@ object PermissionCheck extends SystemConfiguration {
for {
regUser <- RegisteredUsers.findWithUserId(userId, liveMeeting.registeredUsers)
} yield {
Sender.sendInvalidateUserGraphqlConnectionSysMsg(liveMeeting.props.meetingProp.intId, regUser.id, regUser.sessionToken, reason, outGW)
Sender.sendForceUserGraphqlReconnectionSysMsg(liveMeeting.props.meetingProp.intId, regUser.id, regUser.sessionToken, reason, outGW)
}
} else {
// TODO: get this object a context so it can use the akka logging system

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,19 +47,31 @@ 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)
println(s"Updated annotation onpage [${wb.id}]. After numAnnotations=[${newAnnotationsMap.size}].")
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, _))
@ -130,4 +169,4 @@ class WhiteboardModel extends SystemConfiguration {
}
def getChangedModeOn(wbId: String): Long = getWhiteboard(wbId).changedModeOn
}
}

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) =>
@ -54,8 +68,8 @@ trait CreateBreakoutRoomsCmdMsgHdlr extends RightsManagementTrait {
val voiceConf = BreakoutRoomsUtil.createVoiceConfId(liveMeeting.props.voiceProp.voiceConf, i)
val breakout = BreakoutModel.create(parentId, internalId, externalId, room.name, room.sequence, room.shortName,
room.isDefaultName, room.freeJoin, voiceConf, room.users, msg.body.captureNotes,
msg.body.captureSlides, room.captureNotesFilename, room.captureSlidesFilename)
room.isDefaultName, room.freeJoin, voiceConf, room.users, msg.body.captureNotes,
msg.body.captureSlides, room.captureNotesFilename, room.captureSlidesFilename)
rooms = rooms + (breakout.id -> breakout)
}

View File

@ -30,13 +30,13 @@ 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)
// Force reconnection with graphql to refresh permissions
Sender.sendInvalidateUserGraphqlConnectionSysMsg(liveMeeting.props.meetingProp.intId, registeredUser.id, registeredUser.sessionToken, msg.reasonCode, outGW)
Sender.sendForceUserGraphqlReconnectionSysMsg(liveMeeting.props.meetingProp.intId, registeredUser.id, registeredUser.sessionToken, msg.reasonCode, outGW)
//send users update to parent meeting
BreakoutHdlrHelpers.updateParentMeetingWithUsers(liveMeeting, eventBus)

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

@ -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) {
for {
(pollId: String, updatedPoll: SimplePollResultOutVO) <- Polls.handleRespondToTypedPollReqMsg(msg.header.userId, msg.body.pollId,
msg.body.questionId, msg.body.answer, liveMeeting)
} yield {
broadcastPollUpdatedEvent(msg, pollId, updatedPoll)
!Polls.hasUserAlreadyResponded(msg.body.pollId, msg.header.userId, liveMeeting.polls) &&
!Polls.hasUserAlreadyAddedTypedAnswer(msg.body.pollId, msg.header.userId, liveMeeting.polls)) {
for {
presenter <- Users2x.findPresenter(liveMeeting.users2x)
} yield {
broadcastUserRespondedToTypedPollRespMsg(msg, pollId, msg.body.answer, presenter.intId)
//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, answer, liveMeeting)
} yield {
PollHdlrHelpers.broadcastPollUpdatedEvent(bus.outGW, liveMeeting.props.meetingProp.intId, msg.header.userId, pollId, updatedPoll)
for {
presenter <- Users2x.findPresenter(liveMeeting.users2x)
} yield {
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

@ -85,7 +85,7 @@ object AssignPresenterActionHandler extends RightsManagementTrait {
for {
u <- RegisteredUsers.findWithUserId(oldPres.intId, liveMeeting.registeredUsers)
} yield {
Sender.sendInvalidateUserGraphqlConnectionSysMsg(liveMeeting.props.meetingProp.intId, oldPres.intId, u.sessionToken, "role_changed", outGW)
Sender.sendForceUserGraphqlReconnectionSysMsg(liveMeeting.props.meetingProp.intId, oldPres.intId, u.sessionToken, "role_changed", outGW)
}
}
}
@ -100,7 +100,7 @@ object AssignPresenterActionHandler extends RightsManagementTrait {
for {
u <- RegisteredUsers.findWithUserId(newPres.intId, liveMeeting.registeredUsers)
} yield {
Sender.sendInvalidateUserGraphqlConnectionSysMsg(liveMeeting.props.meetingProp.intId, newPres.intId, u.sessionToken, "role_changed", outGW)
Sender.sendForceUserGraphqlReconnectionSysMsg(liveMeeting.props.meetingProp.intId, newPres.intId, u.sessionToken, "role_changed", outGW)
}
}
}

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,5 +1,6 @@
package org.bigbluebutton.core.apps.users
import org.bigbluebutton.ClientSettings.{ getConfigPropertyValueByPath, getConfigPropertyValueByPathAsIntOrElse }
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.RightsManagementTrait
import org.bigbluebutton.core.models.{ UserState, Users2x }
@ -29,9 +30,11 @@ trait ChangeUserReactionEmojiReqMsgHdlr extends RightsManagementTrait {
outGW.send(msgEventChange)
}
//Get durationInSeconds from Client config
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)
newUserState <- Users2x.setReactionEmoji(liveMeeting.users2x, user.intId, msg.body.reactionEmoji, userReactionExpire)
} yield {
if (user.reactionEmoji != msg.body.reactionEmoji) {
broadcast(newUserState, msg.body.reactionEmoji)

View File

@ -73,7 +73,7 @@ trait ChangeUserRoleCmdMsgHdlr extends RightsManagementTrait {
for {
u <- RegisteredUsers.findWithUserId(uvo.intId, liveMeeting.registeredUsers)
} yield {
Sender.sendInvalidateUserGraphqlConnectionSysMsg(liveMeeting.props.meetingProp.intId, uvo.intId, u.sessionToken, "role_changed", outGW)
Sender.sendForceUserGraphqlReconnectionSysMsg(liveMeeting.props.meetingProp.intId, uvo.intId, u.sessionToken, "role_changed", outGW)
}
}
}

View File

@ -24,7 +24,7 @@ trait ClearAllUsersReactionCmdMsgHdlr extends RightsManagementTrait {
user <- Users2x.findAll(liveMeeting.users2x)
} yield {
//Don't clear away and RaiseHand
Users2x.setReactionEmoji(liveMeeting.users2x, user.intId, "none")
Users2x.setReactionEmoji(liveMeeting.users2x, user.intId, "none", 0)
}
sendClearedAllUsersReactionEvtMsg(outGW, liveMeeting.props.meetingProp.intId, msg.header.userId)
} else {

View File

@ -74,7 +74,7 @@ trait EjectUserFromMeetingCmdMsgHdlr extends RightsManagementTrait {
Sender.sendDisconnectClientSysMsg(meetingId, ru.id, ejectedBy, EjectReasonCode.EJECT_USER, outGW)
// Force reconnection with graphql to refresh permissions
Sender.sendInvalidateUserGraphqlConnectionSysMsg(liveMeeting.props.meetingProp.intId, registeredUser.id, registeredUser.sessionToken, EjectReasonCode.EJECT_USER, outGW)
Sender.sendForceUserGraphqlReconnectionSysMsg(liveMeeting.props.meetingProp.intId, registeredUser.id, registeredUser.sessionToken, EjectReasonCode.EJECT_USER, outGW)
}
} else {
// User is ejecting self, so just eject this userid not all sessions if joined using multiple
@ -93,7 +93,7 @@ trait EjectUserFromMeetingCmdMsgHdlr extends RightsManagementTrait {
Sender.sendDisconnectClientSysMsg(meetingId, userId, ejectedBy, EjectReasonCode.EJECT_USER, outGW)
// Force reconnection with graphql to refresh permissions
Sender.sendInvalidateUserGraphqlConnectionSysMsg(liveMeeting.props.meetingProp.intId, registeredUser.id, registeredUser.sessionToken, EjectReasonCode.EJECT_USER, outGW)
Sender.sendForceUserGraphqlReconnectionSysMsg(liveMeeting.props.meetingProp.intId, registeredUser.id, registeredUser.sessionToken, EjectReasonCode.EJECT_USER, outGW)
}
}
@ -129,7 +129,7 @@ trait EjectUserFromMeetingSysMsgHdlr {
for {
regUser <- RegisteredUsers.findWithUserId(userId, liveMeeting.registeredUsers)
} yield {
Sender.sendInvalidateUserGraphqlConnectionSysMsg(liveMeeting.props.meetingProp.intId, regUser.id, regUser.sessionToken, EjectReasonCode.SYSTEM_EJECT_USER, outGW)
Sender.sendForceUserGraphqlReconnectionSysMsg(liveMeeting.props.meetingProp.intId, regUser.id, regUser.sessionToken, EjectReasonCode.SYSTEM_EJECT_USER, outGW)
}
}
}

View File

@ -44,7 +44,7 @@ trait LockUserInMeetingCmdMsgHdlr extends RightsManagementTrait {
for {
u <- RegisteredUsers.findWithUserId(uvo.intId, liveMeeting.registeredUsers)
} yield {
Sender.sendInvalidateUserGraphqlConnectionSysMsg(liveMeeting.props.meetingProp.intId, uvo.intId, u.sessionToken, "lock_user_changed", outGW)
Sender.sendForceUserGraphqlReconnectionSysMsg(liveMeeting.props.meetingProp.intId, uvo.intId, u.sessionToken, "lock_user_changed", outGW)
}
log.info("Lock user. meetingId=" + props.meetingProp.intId + " userId=" + uvo.intId + " locked=" + uvo.locked)

View File

@ -49,7 +49,7 @@ trait RegisterUserReqMsgHdlr {
Sender.sendDisconnectClientSysMsg(meetingId, userToRemove.id, SystemUser.ID, EjectReasonCode.DUPLICATE_USER, outGW)
// Force reconnection with graphql to refresh permissions
Sender.sendInvalidateUserGraphqlConnectionSysMsg(liveMeeting.props.meetingProp.intId, userToRemove.id, userToRemove.sessionToken, EjectReasonCode.DUPLICATE_USER, outGW)
Sender.sendForceUserGraphqlReconnectionSysMsg(liveMeeting.props.meetingProp.intId, userToRemove.id, userToRemove.sessionToken, EjectReasonCode.DUPLICATE_USER, outGW)
}
}
}

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

@ -51,7 +51,7 @@ trait UserJoinMeetingReqMsgHdlr extends HandlerHelpers {
notifyPreviousUsersWithSameExtId(regUser)
clearCachedVoiceUser(regUser)
clearExpiredUserState(regUser)
invalidateUserGraphqlConnection(regUser)
ForceUserGraphqlReconnection(regUser)
newState
}
@ -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)
}
@ -146,7 +146,7 @@ trait UserJoinMeetingReqMsgHdlr extends HandlerHelpers {
private def clearExpiredUserState(regUser: RegisteredUser) =
UserStateDAO.updateExpired(regUser.id, false)
private def invalidateUserGraphqlConnection(regUser: RegisteredUser) =
Sender.sendInvalidateUserGraphqlConnectionSysMsg(liveMeeting.props.meetingProp.intId, regUser.id, regUser.sessionToken, "user_joined", outGW)
private def ForceUserGraphqlReconnection(regUser: RegisteredUser) =
Sender.sendForceUserGraphqlReconnectionSysMsg(liveMeeting.props.meetingProp.intId, regUser.id, regUser.sessionToken, "user_joined", outGW)
}

View File

@ -1,9 +1,11 @@
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 }
import org.bigbluebutton.core2.message.senders.Sender
trait UserLeaveReqMsgHdlr extends HandlerHelpers {
this: MeetingActor =>
@ -11,25 +13,36 @@ 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)
}
}
state
@ -37,4 +50,5 @@ trait UserLeaveReqMsgHdlr extends HandlerHelpers {
state
}
}
}

View File

@ -13,7 +13,7 @@ trait UserReactionTimeExpiredCmdMsgHdlr extends RightsManagementTrait {
def handleUserReactionTimeExpiredCmdMsg(msg: UserReactionTimeExpiredCmdMsg) {
val isNodeUser = msg.header.userId.equals("nodeJSapp")
if (isNodeUser) {
Users2x.setReactionEmoji(liveMeeting.users2x, msg.body.userId, "none")
Users2x.setReactionEmoji(liveMeeting.users2x, msg.body.userId, "none", 0)
}
}
}

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

@ -38,7 +38,16 @@ trait DeleteWhiteboardAnnotationsPubMsgHdlr extends RightsManagementTrait {
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
}
} else {
val deletedAnnotations = deleteWhiteboardAnnotations(msg.body.whiteboardId, msg.header.userId, msg.body.annotationsIds, liveMeeting, isUserAmongPresenters, isUserModerator)
val annotationsIds = {
if (msg.body.annotationsIds.size > 0) {
msg.body.annotationsIds
} else {
getWhiteboardAnnotations(msg.body.whiteboardId, liveMeeting).map(a => a.id)
}
}
val deletedAnnotations = deleteWhiteboardAnnotations(msg.body.whiteboardId, msg.header.userId, annotationsIds, liveMeeting, isUserAmongPresenters, isUserModerator)
if (!deletedAnnotations.isEmpty) {
broadcastEvent(msg, deletedAnnotations)
}

View File

@ -88,7 +88,7 @@ object BreakoutRoomUserDAO {
def insertBreakoutRoom(userId: String, room: BreakoutRoom2x, liveMeeting: LiveMeeting) = {
for {
(redirectToHtml5JoinURL, redirectJoinURL) <- BreakoutHdlrHelpers.getRedirectUrls(liveMeeting, userId, liveMeeting.props.meetingProp.extId, room.sequence.toString)
(redirectToHtml5JoinURL, redirectJoinURL) <- BreakoutHdlrHelpers.getRedirectUrls(liveMeeting, userId, room.externalId, room.sequence.toString)
} yield {
DatabaseConnection.db.run(BreakoutRoomUserDAO.prepareInsert(room.id, userId, redirectToHtml5JoinURL))
.onComplete {

View File

@ -19,8 +19,15 @@ case class MeetingDbModel(
presentationUploadExternalDescription: String,
presentationUploadExternalUrl: String,
learningDashboardAccessToken: String,
logoutUrl: String,
customLogoUrl: Option[String],
bannerText: Option[String],
bannerColor: Option[String],
createdTime: Long,
durationInSeconds: Int
durationInSeconds: Int,
endedAt: Option[java.sql.Timestamp],
endedReasonCode: Option[String],
endedBy: Option[String],
)
class MeetingDbTableDef(tag: Tag) extends Table[MeetingDbModel](tag, None, "meeting") {
@ -36,8 +43,15 @@ class MeetingDbTableDef(tag: Tag) extends Table[MeetingDbModel](tag, None, "meet
presentationUploadExternalDescription,
presentationUploadExternalUrl,
learningDashboardAccessToken,
logoutUrl,
customLogoUrl,
bannerText,
bannerColor,
createdTime,
durationInSeconds
durationInSeconds,
endedAt,
endedReasonCode,
endedBy
) <> (MeetingDbModel.tupled, MeetingDbModel.unapply)
val meetingId = column[String]("meetingId", O.PrimaryKey)
val extId = column[String]("extId")
@ -50,8 +64,15 @@ class MeetingDbTableDef(tag: Tag) extends Table[MeetingDbModel](tag, None, "meet
val presentationUploadExternalDescription = column[String]("presentationUploadExternalDescription")
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 endedAt = column[Option[java.sql.Timestamp]]("endedAt")
val endedReasonCode = column[Option[String]]("endedReasonCode")
val endedBy = column[Option[String]]("endedBy")
}
object MeetingDAO {
@ -70,28 +91,44 @@ object MeetingDAO {
presentationUploadExternalDescription = meetingProps.meetingProp.presentationUploadExternalDescription,
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,
endedAt = None,
endedReasonCode = None,
endedBy = None
)
)
).onComplete {
case Success(rowsAffected) => {
DatabaseConnection.logger.debug(s"$rowsAffected row(s) inserted in Meeting table!")
ChatDAO.insert(meetingProps.meetingProp.intId, GroupChatApp.createDefaultPublicGroupChat())
MeetingUsersPoliciesDAO.insert(meetingProps.meetingProp.intId, meetingProps.usersProp)
MeetingLockSettingsDAO.insert(meetingProps.meetingProp.intId, meetingProps.lockSettingsProps)
MeetingMetadataDAO.insert(meetingProps.meetingProp.intId, meetingProps.metadataProp)
MeetingRecordingPoliciesDAO.insert(meetingProps.meetingProp.intId, meetingProps.recordProp)
MeetingVoiceDAO.insert(meetingProps.meetingProp.intId, meetingProps.voiceProp)
MeetingWelcomeDAO.insert(meetingProps.meetingProp.intId, meetingProps.welcomeProp)
MeetingGroupDAO.insert(meetingProps.meetingProp.intId, meetingProps.groups)
MeetingBreakoutDAO.insert(meetingProps.meetingProp.intId, meetingProps.breakoutProps)
TimerDAO.insert(meetingProps.meetingProp.intId)
LayoutDAO.insert(meetingProps.meetingProp.intId, meetingProps.usersProp.meetingLayout)
MeetingClientSettingsDAO.insert(meetingProps.meetingProp.intId, JsonUtils.mapToJson(clientSettings))
}
case Failure(e) => DatabaseConnection.logger.error(s"Error inserting Meeting: $e")
case Success(rowsAffected) => {
DatabaseConnection.logger.debug(s"$rowsAffected row(s) inserted in Meeting table!")
ChatDAO.insert(meetingProps.meetingProp.intId, GroupChatApp.createDefaultPublicGroupChat())
MeetingUsersPoliciesDAO.insert(meetingProps.meetingProp.intId, meetingProps.usersProp)
MeetingLockSettingsDAO.insert(meetingProps.meetingProp.intId, meetingProps.lockSettingsProps)
MeetingMetadataDAO.insert(meetingProps.meetingProp.intId, meetingProps.metadataProp)
MeetingRecordingPoliciesDAO.insert(meetingProps.meetingProp.intId, meetingProps.recordProp)
MeetingVoiceDAO.insert(meetingProps.meetingProp.intId, meetingProps.voiceProp)
MeetingWelcomeDAO.insert(meetingProps.meetingProp.intId, meetingProps.welcomeProp)
MeetingGroupDAO.insert(meetingProps.meetingProp.intId, meetingProps.groups)
MeetingBreakoutDAO.insert(meetingProps.meetingProp.intId, meetingProps.breakoutProps)
TimerDAO.insert(meetingProps.meetingProp.intId)
LayoutDAO.insert(meetingProps.meetingProp.intId, meetingProps.usersProp.meetingLayout)
MeetingClientSettingsDAO.insert(meetingProps.meetingProp.intId, JsonUtils.mapToJson(clientSettings))
}
case Failure(e) => DatabaseConnection.logger.error(s"Error inserting Meeting: $e")
}
}
def updateMeetingDurationByParentMeeting(parentMeetingId: String, newDurationInSeconds: Int) = {
@ -106,9 +143,9 @@ object MeetingDAO {
.map(u => u.durationInSeconds)
.update(newDurationInSeconds)
).onComplete {
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated durationInSeconds on Meeting table")
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating durationInSeconds on Meeting: $e")
}
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated durationInSeconds on Meeting table")
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating durationInSeconds on Meeting: $e")
}
}
def delete(meetingId: String) = {
@ -117,9 +154,32 @@ object MeetingDAO {
.filter(_.meetingId === meetingId)
.delete
).onComplete {
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"Meeting ${meetingId} deleted")
case Failure(e) => DatabaseConnection.logger.debug(s"Error deleting meeting ${meetingId}: $e")
}
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"Meeting ${meetingId} deleted")
case Failure(e) => DatabaseConnection.logger.debug(s"Error deleting meeting ${meetingId}: $e")
}
}
def setMeetingEnded(meetingId: String, endedReasonCode: String, endedBy: String) = {
UserDAO.softDeleteAllFromMeeting(meetingId)
DatabaseConnection.db.run(
TableQuery[MeetingDbTableDef]
.filter(_.meetingId === meetingId)
.map(a => (a.endedAt, a.endedReasonCode, a.endedBy))
.update(
(
Some(new java.sql.Timestamp(System.currentTimeMillis())),
Some(endedReasonCode),
endedBy match {
case "" => None
case c => Some(c)
}
)
)
).onComplete {
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated endedAt=now() on Meeting table!")
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating endedAt=now() Meeting: $e")
}
}
}

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

@ -2,6 +2,7 @@ package org.bigbluebutton.core.db
import com.github.tminglei.slickpg._
import org.apache.pekko.http.scaladsl.model.ParsingException
import org.bigbluebutton.common2.domain.SimpleVoteOutVO
import spray.json.{ JsArray, JsBoolean, JsNumber, JsObject, JsString, JsValue, JsonWriter, _ }
import scala.util.{ Failure, Success, Try }
@ -29,16 +30,17 @@ object PostgresProfile extends PostgresProfile
object JsonUtils {
implicit object AnyJsonWriter extends JsonWriter[Any] {
def write(x: Any): JsValue = x match {
case n: Int => JsNumber(n)
case s: String => JsString(s)
case b: Boolean => JsBoolean(b)
case f: Float => JsNumber(f)
case d: Double => JsNumber(d)
case m: Map[_, _] => JsObject(m.asInstanceOf[Map[String, Any]].map { case (k, v) => k -> write(v) })
case l: List[_] => JsArray(l.map(write).toVector)
case a: Array[_] => JsArray(a.map(write).toVector)
case null => JsNull
case _ => throw new IllegalArgumentException(s"Unsupported type: ${x.getClass.getName}")
case n: Int => JsNumber(n)
case s: String => JsString(s)
case b: Boolean => JsBoolean(b)
case f: Float => JsNumber(f)
case d: Double => JsNumber(d)
case m: Map[_, _] => JsObject(m.asInstanceOf[Map[String, Any]].map { case (k, v) => k -> write(v) })
case l: List[_] => JsArray(l.map(write).toVector)
case a: Array[_] => JsArray(a.map(write).toVector)
case v: SimpleVoteOutVO => JsObject("id" -> JsNumber(v.id), "key" -> JsString(v.key), "numVotes" -> JsNumber(v.numVotes))
case null => JsNull
case _ => throw new IllegalArgumentException(s"Unsupported type: ${x.getClass.getName}")
// case _ => JsNull
}
}

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

@ -23,13 +23,13 @@ class UserReactionDbTableDef(tag: Tag) extends Table[UserReactionDbModel](tag, "
}
object UserReactionDAO {
def insert(userId: String, reactionEmoji: String) = {
def insert(userId: String, reactionEmoji: String, durationInSeconds: Int) = {
DatabaseConnection.db.run(
TableQuery[UserReactionDbTableDef].forceInsert(
UserReactionDbModel(
userId = userId,
reactionEmoji = reactionEmoji,
durationInSeconds = 60,
durationInSeconds = durationInSeconds,
createdAt = new java.sql.Timestamp(System.currentTimeMillis())
)
)

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

@ -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)
}
@ -125,7 +125,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
}
}
@ -195,7 +195,7 @@ object Users2x {
newUser
}
}
def setReactionEmoji(users: Users2x, intId: String, reactionEmoji: String): Option[UserState] = {
def setReactionEmoji(users: Users2x, intId: String, reactionEmoji: String, durationInSeconds: Int): Option[UserState] = {
for {
u <- findWithIntId(users, intId)
} yield {
@ -203,7 +203,7 @@ object Users2x {
.modify(_.reactionChangedOn).setTo(System.currentTimeMillis())
users.save(newUser)
UserReactionDAO.insert(intId, reactionEmoji)
UserReactionDAO.insert(intId, reactionEmoji, durationInSeconds)
newUser
}
}

View File

@ -416,8 +416,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 =>
@ -450,12 +456,15 @@ class ReceivedJsonMsgHandlerActor(
routeGenericMsg[TimerEndedPubMsg](envelope, jsonNode)
// Messages from Graphql Middleware
case UserGraphqlConnectionStablishedSysMsg.NAME =>
route[UserGraphqlConnectionStablishedSysMsg](meetingManagerChannel, envelope, jsonNode)
case UserGraphqlConnectionEstablishedSysMsg.NAME =>
route[UserGraphqlConnectionEstablishedSysMsg](meetingManagerChannel, envelope, jsonNode)
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
@ -206,6 +206,8 @@ trait HandlerHelpers extends SystemConfiguration {
val endedEvnt = buildMeetingEndedEvtMsg(liveMeeting.props.meetingProp.intId)
outGW.send(endedEvnt)
MeetingDAO.setMeetingEnded(liveMeeting.props.meetingProp.intId, reason, userId)
}
def destroyMeeting(eventBus: InternalEventBus, meetingId: String): Unit = {

View File

@ -76,6 +76,7 @@ class MeetingActor(
with UserJoinMeetingReqMsgHdlr
with UserJoinMeetingAfterReconnectReqMsgHdlr
with UserEstablishedGraphqlConnectionInternalMsgHdlr
with UserConnectedToGlobalAudioMsgHdlr
with UserDisconnectedFromGlobalAudioMsgHdlr
with MuteAllExceptPresentersCmdMsgHdlr
@ -266,8 +267,14 @@ class MeetingActor(
// internal messages
case msg: MonitorNumberOfUsersInternalMsg => handleMonitorNumberOfUsers(msg)
case msg: SetPresenterInDefaultPodInternalMsg => state = presentationPodsApp.handleSetPresenterInDefaultPodInternalMsg(msg, state, liveMeeting, msgBus)
case msg: UserClosedAllGraphqlConnectionsInternalMsg =>
state = handleUserClosedAllGraphqlConnectionsInternalMsg(msg, state)
updateModeratorsPresence()
case msg: UserEstablishedGraphqlConnectionInternalMsg =>
state = handleUserEstablishedGraphqlConnectionInternalMsg(msg, state)
updateModeratorsPresence()
case msg: ExtendMeetingDuration => handleExtendMeetingDuration(msg)
case msg: ExtendMeetingDuration => handleExtendMeetingDuration(msg)
case msg: SendTimeRemainingAuditInternalMsg =>
if (!liveMeeting.props.meetingProp.isBreakout) {
// Update users of meeting remaining time.
@ -594,7 +601,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)
@ -1005,7 +1014,7 @@ class MeetingActor(
for {
regUser <- RegisteredUsers.findWithUserId(u.intId, liveMeeting.registeredUsers)
} yield {
Sender.sendInvalidateUserGraphqlConnectionSysMsg(liveMeeting.props.meetingProp.intId, regUser.id, regUser.sessionToken, EjectReasonCode.USER_INACTIVITY, outGW)
Sender.sendForceUserGraphqlReconnectionSysMsg(liveMeeting.props.meetingProp.intId, regUser.id, regUser.sessionToken, EjectReasonCode.USER_INACTIVITY, outGW)
}
}
}

View File

@ -246,12 +246,22 @@ object MsgBuilder {
BbbCommonEnvCoreMsg(envelope, event)
}
def buildInvalidateUserGraphqlConnectionSysMsg(meetingId: String, userId: String, sessionToken: String, reason: String): BbbCommonEnvCoreMsg = {
def buildForceUserGraphqlReconnectionSysMsg(meetingId: String, userId: String, sessionToken: String, reason: String): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(MessageTypes.SYSTEM, meetingId, userId)
val envelope = BbbCoreEnvelope(InvalidateUserGraphqlConnectionSysMsg.NAME, routing)
val header = BbbCoreHeaderWithMeetingId(InvalidateUserGraphqlConnectionSysMsg.NAME, meetingId)
val body = InvalidateUserGraphqlConnectionSysMsgBody(meetingId, userId, sessionToken, reason)
val event = InvalidateUserGraphqlConnectionSysMsg(header, body)
val envelope = BbbCoreEnvelope(ForceUserGraphqlReconnectionSysMsg.NAME, routing)
val header = BbbCoreHeaderWithMeetingId(ForceUserGraphqlReconnectionSysMsg.NAME, meetingId)
val body = ForceUserGraphqlReconnectionSysMsgBody(meetingId, userId, sessionToken, reason)
val event = ForceUserGraphqlReconnectionSysMsg(header, body)
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)
}

View File

@ -10,9 +10,9 @@ object Sender {
outGW.send(ejectFromMeetingSystemEvent)
}
def sendInvalidateUserGraphqlConnectionSysMsg(meetingId: String, userId: String, sessionToken: String, reason: String, outGW: OutMsgRouter): Unit = {
val invalidateUserGraphqlConnectionSysMsg = MsgBuilder.buildInvalidateUserGraphqlConnectionSysMsg(meetingId, userId, sessionToken, reason)
outGW.send(invalidateUserGraphqlConnectionSysMsg)
def sendForceUserGraphqlReconnectionSysMsg(meetingId: String, userId: String, sessionToken: String, reason: String, outGW: OutMsgRouter): Unit = {
val ForceUserGraphqlReconnectionSysMsg = MsgBuilder.buildForceUserGraphqlReconnectionSysMsg(meetingId, userId, sessionToken, reason)
outGW.send(ForceUserGraphqlReconnectionSysMsg)
}
def sendUserInactivityInspectMsg(meetingId: String, userId: String, responseDelay: Long, outGW: OutMsgRouter): Unit = {

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: UserGraphqlConnectionStablishedSysMsg => handleUserGraphqlConnectionStablishedSysMsg(m)
case m: UserGraphqlConnectionClosedSysMsg => handleUserGraphqlConnectionClosedSysMsg(m)
case _ => // message not to be handled.
}
}
private def handleUserGraphqlConnectionStablishedSysMsg(msg: UserGraphqlConnectionStablishedSysMsg) {
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)
@ -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)
}

View File

@ -68,7 +68,11 @@ case class LockSettingsProps(
)
case class SystemProps(
html5InstanceId: Int
html5InstanceId: Int,
logoutUrl: String,
customLogoURL: String,
bannerText: String,
bannerColor: String,
)
case class GroupProps(

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

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

@ -235,37 +235,57 @@ case class DeletedRecordingSysMsgBody(recordId: String)
/**
* Sent from akka-apps to graphql-middleware
*/
object InvalidateUserGraphqlConnectionSysMsg { val NAME = "InvalidateUserGraphqlConnectionSysMsg" }
case class InvalidateUserGraphqlConnectionSysMsg(
object CheckGraphqlMiddlewareAlivePingSysMsg { val NAME = "CheckGraphqlMiddlewareAlivePingSysMsg" }
case class CheckGraphqlMiddlewareAlivePingSysMsg(
header: BbbCoreHeaderWithMeetingId,
body: InvalidateUserGraphqlConnectionSysMsgBody
body: CheckGraphqlMiddlewareAlivePingSysMsgBody
) extends BbbCoreMsg
case class InvalidateUserGraphqlConnectionSysMsgBody(meetingId: String, userId: String, sessionToken: String, reason: String)
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
*/
object ForceUserGraphqlReconnectionSysMsg { val NAME = "ForceUserGraphqlReconnectionSysMsg" }
case class ForceUserGraphqlReconnectionSysMsg(
header: BbbCoreHeaderWithMeetingId,
body: ForceUserGraphqlReconnectionSysMsgBody
) extends BbbCoreMsg
case class ForceUserGraphqlReconnectionSysMsgBody(meetingId: String, userId: String, sessionToken: String, reason: String)
/**
* Sent from graphql-middleware to akka-apps
*/
object UserGraphqlConnectionInvalidatedEvtMsg { val NAME = "UserGraphqlConnectionInvalidatedEvtMsg" }
case class UserGraphqlConnectionInvalidatedEvtMsg(
object UserGraphqlReconnectionForcedEvtMsg { val NAME = "UserGraphqlReconnectionForcedEvtMsg" }
case class UserGraphqlReconnectionForcedEvtMsg(
header: BbbCoreBaseHeader,
body: UserGraphqlConnectionInvalidatedEvtMsgBody
body: UserGraphqlReconnectionForcedEvtMsgBody
) extends BbbCoreMsg
case class UserGraphqlConnectionInvalidatedEvtMsgBody(sessionToken: String, browserConnectionId: String)
case class UserGraphqlReconnectionForcedEvtMsgBody(middlewareUID: String, sessionToken: String, browserConnectionId: String)
object UserGraphqlConnectionStablishedSysMsg { val NAME = "UserGraphqlConnectionStablishedSysMsg" }
case class UserGraphqlConnectionStablishedSysMsg(
object UserGraphqlConnectionEstablishedSysMsg { val NAME = "UserGraphqlConnectionEstablishedSysMsg" }
case class UserGraphqlConnectionEstablishedSysMsg(
header: BbbCoreBaseHeader,
body: UserGraphqlConnectionStablishedSysMsgBody
body: UserGraphqlConnectionEstablishedSysMsgBody
) extends BbbCoreMsg
case class UserGraphqlConnectionStablishedSysMsgBody(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

@ -302,7 +302,7 @@ case class UserMobileFlagChangedEvtMsgBody(userId: String, mobile: Boolean)
object AssignPresenterReqMsg { val NAME = "AssignPresenterReqMsg" }
case class AssignPresenterReqMsg(header: BbbClientMsgHeader, body: AssignPresenterReqMsgBody) extends StandardMsg
case class AssignPresenterReqMsgBody(requesterId: String, newPresenterId: String, newPresenterName: String, assignedBy: String)
case class AssignPresenterReqMsgBody(assignedBy: String, newPresenterId: String)
/**
* Sent from client to change the video pin of the user in the meeting.

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

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,21 @@ 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;
removedSessions.put(token, removedUser);
sessions.remove(token);
} else {
log.debug("Not found token={}", token);
}
return user;
}
/**
@ -295,16 +311,40 @@ public class MeetingService implements MessageListener {
notifier.sendUploadFileTooLargeMessage(presUploadToken, uploadedFileSize, maxUploadFileSize);
}
private void removeUserSessions(String meetingId) {
Iterator<Map.Entry<String, UserSession>> iterator = sessions.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, UserSession> entry = iterator.next();
UserSession userSession = entry.getValue();
private void removeUserSessionsFromMeeting(String meetingId) {
for (String token : sessions.keySet()) {
UserSession userSession = sessions.get(token);
if (userSession.meetingID.equals(meetingId)) {
iterator.remove();
System.out.println(token + " = " + userSession.authToken);
removeUserSessionWithSessionToken(token);
}
}
scheduleRemovedSessionsCleanUp(meetingId);
}
private void scheduleRemovedSessionsCleanUp(String meetingId) {
Calendar cleanUpDelayCalendar = Calendar.getInstance();
cleanUpDelayCalendar.add(Calendar.MINUTE, sessionsCleanupDelayInMinutes);
log.debug("Sessions for meeting={} will be removed within {} minutes.", meetingId, sessionsCleanupDelayInMinutes);
new java.util.Timer().schedule(
new java.util.TimerTask() {
@Override
public void run() {
Iterator<Map.Entry<String, UserSessionBasicData>> iterator = removedSessions.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, UserSessionBasicData> entry = iterator.next();
UserSessionBasicData removedUserSession = entry.getValue();
if (removedUserSession.meetingId.equals(meetingId)) {
log.debug("Removed user {} session for meeting {}.",removedUserSession.userId, removedUserSession.meetingId);
iterator.remove();
}
}
}
}, cleanUpDelayCalendar.getTime()
);
}
private void destroyMeeting(String meetingId) {
@ -411,8 +451,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.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 +743,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 +1151,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 +1224,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 +1254,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 +1403,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;
@ -472,7 +472,15 @@ public class Meeting {
}
public String getGuestPolicy() {
return guestPolicy;
return guestPolicy;
}
public void setLockSettings(LockSettingsParams lockSettingsParams) {
this.lockSettingsParams = lockSettingsParams;
}
public void setWebcamsOnlyForModerator(Boolean webcamsOnlyForModerator) {
this.webcamsOnlyForModerator = webcamsOnlyForModerator;
}
public void setGuestLobbyMessage(String message) {

View File

@ -137,6 +137,10 @@ public class User {
public boolean isModerator() {
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,30 @@
/**
* 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 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;

View File

@ -21,6 +21,10 @@ public class ParamsUtil {
return text.replaceAll("\\p{Cc}", "").trim();
}
public static String stripTags(String text) {
return text.replaceAll("<[^>]*>", "");
}
public static String escapeHTMLTags(String value) {
return StringEscapeUtils.escapeHtml4(value);
}

View File

@ -52,7 +52,7 @@ public class ResponseBuilder {
return new Date(timestamp).toString();
}
public String buildMeetingVersion(String apiVersion, String bbbVersion, String returnCode) {
public String buildMeetingVersion(String apiVersion, String bbbVersion, String graphqlWebsocketUrl, String returnCode) {
StringWriter xmlText = new StringWriter();
Map<String, Object> data = new HashMap<String, Object>();
@ -60,6 +60,7 @@ public class ResponseBuilder {
data.put("version", apiVersion);
data.put("apiVersion", apiVersion);
data.put("bbbVersion", bbbVersion);
data.put("graphqlWebsocketUrl", graphqlWebsocketUrl);
processData(getTemplate("api-version.ftlx"), data, xmlText);

View File

@ -42,6 +42,10 @@ public interface IBbbWebApiGWApp {
BreakoutRoomsParams breakoutParams,
LockSettingsParams lockSettingsParams,
Integer html5InstanceId,
String logoutUrl,
String customLogoURL,
String bannerText,
String bannerColor,
ArrayList<Group> groups,
ArrayList<String> disabledFeatures,
Boolean notifyRecordingIsOn,

View File

@ -149,6 +149,10 @@ class BbbWebApiGWApp(
breakoutParams: BreakoutRoomsParams,
lockSettingsParams: LockSettingsParams,
html5InstanceId: java.lang.Integer,
logoutUrl: String,
customLogoURL: String,
bannerText: String,
bannerColor: String,
groups: java.util.ArrayList[Group],
disabledFeatures: java.util.ArrayList[String],
notifyRecordingIsOn: java.lang.Boolean,
@ -230,7 +234,17 @@ class BbbWebApiGWApp(
)
val systemProps = SystemProps(
html5InstanceId
html5InstanceId,
logoutUrl,
customLogoURL,
bannerText match {
case t: String => t
case _ => ""
},
bannerColor match {
case c: String => c
case _ => ""
},
)
val groupsAsVector: Vector[GroupProps] = groups.asScala.toVector.map(g => GroupProps(g.getGroupId(), g.getName(), g.getUsersExtId().asScala.toVector))

View File

@ -100,6 +100,10 @@ class ReceivedJsonMsgHdlrActor(val msgFromAkkaAppsEventBus: MsgFromAkkaAppsEvent
route[PosInWaitingQueueUpdatedRespMsg](envelope, jsonNode)
case GuestPolicyChangedEvtMsg.NAME =>
route[GuestPolicyChangedEvtMsg](envelope, jsonNode)
case LockSettingsInMeetingChangedEvtMsg.NAME =>
route[LockSettingsInMeetingChangedEvtMsg](envelope, jsonNode)
case WebcamsOnlyForModeratorChangedEvtMsg.NAME =>
route[WebcamsOnlyForModeratorChangedEvtMsg](envelope, jsonNode)
case GuestLobbyMessageChangedEvtMsg.NAME =>
route[GuestLobbyMessageChangedEvtMsg](envelope, jsonNode)
case PrivateGuestLobbyMsgChangedEvtMsg.NAME =>

View File

@ -1,8 +1,7 @@
package org.bigbluebutton.api2.meeting
import java.util
import org.apache.pekko.actor.{ Actor, ActorLogging, Props }
import org.apache.pekko.actor.{Actor, ActorLogging, Props}
import org.bigbluebutton.api.messaging.messages._
import org.bigbluebutton.api2.bus.OldMessageReceivedGW
import org.bigbluebutton.common2.msgs._
@ -41,6 +40,8 @@ class OldMeetingMsgHdlrActor(val olgMsgGW: OldMessageReceivedGW)
case m: GuestsWaitingApprovedEvtMsg => handleGuestsWaitingApprovedEvtMsg(m)
case m: PosInWaitingQueueUpdatedRespMsg => handlePosInWaitingQueueUpdatedRespMsg(m)
case m: GuestPolicyChangedEvtMsg => handleGuestPolicyChangedEvtMsg(m)
case m: LockSettingsInMeetingChangedEvtMsg => handleLockSettingsInMeetingChangedEvtMsg(m)
case m: WebcamsOnlyForModeratorChangedEvtMsg => handleWebcamsOnlyForModeratorChangedEvtMsg(m)
case m: GuestLobbyMessageChangedEvtMsg => handleGuestLobbyMessageChangedEvtMsg(m)
case m: PrivateGuestLobbyMsgChangedEvtMsg => handlePrivateGuestLobbyMsgChangedEvtMsg(m)
case m: RecordingChapterBreakSysMsg => handleRecordingChapterBreakSysMsg(m)
@ -55,6 +56,25 @@ class OldMeetingMsgHdlrActor(val olgMsgGW: OldMessageReceivedGW)
olgMsgGW.handle(new GuestPolicyChanged(msg.header.meetingId, msg.body.policy))
}
def handleLockSettingsInMeetingChangedEvtMsg(msg: LockSettingsInMeetingChangedEvtMsg): Unit = {
olgMsgGW.handle(new LockSettingsChanged(msg.header.meetingId,
msg.body.disableCam,
msg.body.disableMic,
msg.body.disablePrivChat,
msg.body.disablePubChat,
msg.body.disableNotes,
msg.body.hideUserList,
msg.body.lockOnJoin,
msg.body.lockOnJoinConfigurable,
msg.body.hideViewersCursor,
msg.body.hideViewersAnnotation,
))
}
def handleWebcamsOnlyForModeratorChangedEvtMsg(msg: WebcamsOnlyForModeratorChangedEvtMsg): Unit = {
olgMsgGW.handle(new WebcamsOnlyForModeratorChanged(msg.header.meetingId, msg.body.webcamsOnlyForModerator))
}
def handleGuestLobbyMessageChangedEvtMsg(msg: GuestLobbyMessageChangedEvtMsg): Unit = {
olgMsgGW.handle(new GuestLobbyMessageChanged(msg.header.meetingId, msg.body.message))
}

View File

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

View File

@ -8,7 +8,7 @@
"name": "bbb-export-annotations",
"version": "0.0.1",
"dependencies": {
"axios": "^0.26.0",
"axios": "^1.6.5",
"form-data": "^4.0.0",
"perfect-freehand": "^1.0.16",
"probe-image-size": "^7.2.3",
@ -21,8 +21,8 @@
"eslint-config-google": "^0.14.0"
},
"engines": {
"node": ">=16",
"npm": ">=8.5"
"node": ">=18.16.0",
"npm": ">=9.5.0"
}
},
"node_modules/@eslint/eslintrc": {
@ -287,11 +287,13 @@
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"node_modules/axios": {
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz",
"integrity": "sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==",
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz",
"integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==",
"dependencies": {
"follow-redirects": "^1.14.8"
"follow-redirects": "^1.15.4",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
@ -692,9 +694,9 @@
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.14.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
"funding": [
{
"type": "individual",
@ -1067,6 +1069,11 @@
"stream-parser": "~0.3.1"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
@ -1319,9 +1326,9 @@
}
},
"node_modules/word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
@ -1573,11 +1580,13 @@
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"axios": {
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz",
"integrity": "sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==",
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz",
"integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==",
"requires": {
"follow-redirects": "^1.14.8"
"follow-redirects": "^1.15.4",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"balanced-match": {
@ -1881,9 +1890,9 @@
"dev": true
},
"follow-redirects": {
"version": "1.14.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w=="
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw=="
},
"form-data": {
"version": "4.0.0",
@ -2161,6 +2170,11 @@
"stream-parser": "~0.3.1"
}
},
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
@ -2358,9 +2372,9 @@
}
},
"word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"dev": true
},
"wrappy": {

View File

@ -7,7 +7,7 @@
"lint:fix": "eslint --fix **/*.js"
},
"dependencies": {
"axios": "^0.26.0",
"axios": "^1.6.5",
"form-data": "^4.0.0",
"perfect-freehand": "^1.0.16",
"probe-image-size": "^7.2.3",
@ -20,7 +20,7 @@
"eslint-config-google": "^0.14.0"
},
"engines": {
"node": "^18.16.0",
"npm": "^9.5.0"
"node": ">=18.16.0",
"npm": ">=9.5.0"
}
}

View File

@ -129,9 +129,9 @@ async function collectSharedNotes(retries = 3) {
const padId = exportJob.presId;
const notesFormat = 'pdf';
const filename = `${sanitize(exportJob.filename.replace(/\s/g, '_'))}.${notesFormat}`;
const serverSideFilename = `${sanitize(exportJob.serverSideFilename.replace(/\s/g, '_'))}.${notesFormat}`;
const notes_endpoint = `${config.bbbPadsAPI}/p/${padId}/export/${notesFormat}`;
const filePath = path.join(dropbox, filename);
const filePath = path.join(dropbox, serverSideFilename);
const finishedDownload = promisify(stream.finished);
const writer = fs.createWriteStream(filePath);
@ -157,7 +157,7 @@ async function collectSharedNotes(retries = 3) {
}
}
const notifier = new WorkerStarter({jobType, jobId, filename});
const notifier = new WorkerStarter({jobType, jobId, serverSideFilename, filename: exportJob.filename});
notifier.notify();
}

View File

@ -8,7 +8,7 @@ const path = require('path');
const {NewPresFileAvailableMsg} = require('../lib/utils/message-builder');
const {workerData} = require('worker_threads');
const [jobType, jobId, filename] = [workerData.jobType, workerData.jobId, workerData.filename];
const [jobType, jobId, serverSideFilename] = [workerData.jobType, workerData.jobId, workerData.serverSideFilename];
const logger = new Logger('presAnn Notifier Worker');
@ -30,13 +30,14 @@ async function notifyMeetingActor() {
const link = path.join('presentation',
exportJob.parentMeetingId, exportJob.parentMeetingId,
exportJob.presId, 'pdf', jobId, filename);
exportJob.presId, 'pdf', jobId, serverSideFilename);
const notification = new NewPresFileAvailableMsg(exportJob, link);
logger.info(`Annotated PDF available at ${link}`);
await client.publish(config.redis.channels.publish, notification.build());
client.disconnect();
}
/** Upload PDF to a BBB room
@ -63,10 +64,10 @@ async function upload(filePath) {
if (jobType == 'PresentationWithAnnotationDownloadJob') {
notifyMeetingActor();
} else if (jobType == 'PresentationWithAnnotationExportJob') {
const filePath = `${exportJob.presLocation}/pdfs/${jobId}/${filename}`;
const filePath = `${exportJob.presLocation}/pdfs/${jobId}/${serverSideFilename}`;
upload(filePath);
} else if (jobType == 'PadCaptureJob') {
const filePath = `${dropbox}/${filename}`;
const filePath = `${dropbox}/${serverSideFilename}`;
upload(filePath);
} else {
logger.error(`Notifier received unknown job type ${jobType}`);

View File

@ -886,7 +886,7 @@ async function process_presentation_annotations() {
fs.mkdirSync(outputDir, {recursive: true});
}
const filename_with_extension = `${sanitize(exportJob.filename.replace(/\s/g, '_'))}.pdf`;
const filename_with_extension = `${sanitize(exportJob.serverSideFilename.replace(/\s/g, '_'))}.pdf`;
const mergePDFs = [
'-dNOPAUSE',
@ -904,7 +904,8 @@ async function process_presentation_annotations() {
// Launch Notifier Worker depending on job type
logger.info(`Saved PDF at ${outputDir}/${jobId}/${filename_with_extension}`);
const notifier = new WorkerStarter({jobType: exportJob.jobType, jobId, filename: filename_with_extension});
const notifier = new WorkerStarter({jobType: exportJob.jobType, jobId,
serverSideFilename: filename_with_extension, filename: exportJob.filename});
notifier.notify();
await client.disconnect();
}

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