Merge remote-tracking branch 'upstream/develop' into PR-11359

This commit is contained in:
Ramon Souza 2021-09-17 09:12:43 -03:00
commit 0ca1b7896e
1070 changed files with 91754 additions and 24049 deletions

View File

@ -29,7 +29,7 @@ A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**BBB version (optional):**
**BBB version:**
BigBlueButton continually evolves. Providing the version/build helps us to pinpoint when an issue was introduced.
Example:
$ sudo bbb-conf --check | grep BigBlueButton

19
.github/stale.yml vendored Normal file
View File

@ -0,0 +1,19 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 270
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 90
# Issues with these labels will never be considered stale
exemptLabels:
- "status: accepted"
- "status: verify"
- "target: security"
- "type: discussion"
# Label to use when marking an issue as stale
staleLabel: "status: stale"
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

188
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,188 @@
# set up stages
variables:
GIT_STRATEGY: fetch
stages:
- change detection
- get external dependencies
- build
- push packages
# define which docker image to use for builds
default:
image: gitlab.senfcall.de:5050/senfcall-public/docker-bbb-build:v2021-08-10
# This stage uses git to find out since when each package has been unmodified.
# it then checks an API endpoint on the package server to find out for which of
# these versions a build exists. If a viable build (from a commit where the
# package is identical) is found, that package name and .deb-filename are
# written to a file `packages_to_skip.txt` the root of the repo. This file is
# passed to the subsequent stages:
# - The jobs in the build stage check whether "their" package is listed in
# `packages_to_skip.txt` and don't build a new one if it is.
# - The bigbluebutton-build job includes the package versions listed in that
# file as version-pinned dependencies of the `bigbluebutton` package (instead
# of the current commit version)
# - The push_packages job sends the filenames of the packages that can be reused
# to the server, so they are included with the current branch. (Relevant for
# commits that start a new branch and don't change all packages)
change_detection:
stage: change detection
script: build/change_detection.sh
artifacts:
paths:
- packages_to_skip.txt
# replace placeholder files with actual external repos
# (for source and version of the package see the placeholder file)
# this step will be obsolete once dependencies can be tracked as
# git submodules
get_external_dependencies:
stage: get external dependencies
script: build/get_external_dependencies.sh
artifacts:
paths:
- bbb-etherpad
- bbb-webrtc-sfu
- freeswitch
- bbb-playback
expire_in: 1h 30min
# template job for build step
.build_job:
stage: build
artifacts:
paths:
- artifacts/*.deb
expire_in: 1h 30min
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- cache/.gradle
# jobs for all packages in the "build" stage (templated from above)
bbb-apps-akka-build:
extends: .build_job
script:
- build/setup-inside-docker.sh bbb-apps-akka
bbb-config-build:
extends: .build_job
script:
- build/setup-inside-docker.sh bbb-config
bbb-demo-build:
extends: .build_job
script:
- build/setup-inside-docker.sh bbb-demo
bbb-etherpad-build:
extends: .build_job
script:
- build/setup-inside-docker.sh bbb-etherpad
bbb-freeswitch-core-build:
extends: .build_job
script:
- build/setup-inside-docker.sh bbb-freeswitch-core
bbb-freeswitch-sounds-build:
extends: .build_job
script:
- build/setup-inside-docker.sh bbb-freeswitch-sounds
bbb-fsesl-akka-build:
extends: .build_job
script:
- build/setup-inside-docker.sh bbb-fsesl-akka
bbb-html5-build:
extends: .build_job
script:
- build/setup-inside-docker.sh bbb-html5
bbb-learning-dashboard:
extends: .build_job
script:
- build/setup-inside-docker.sh bbb-learning-dashboard
bbb-libreoffice-docker-build:
extends: .build_job
script:
- build/setup-inside-docker.sh bbb-libreoffice-docker
bbb-lti-build:
extends: .build_job
script:
- build/setup-inside-docker.sh bbb-lti
bbb-mkclean-build:
extends: .build_job
script:
- build/setup-inside-docker.sh bbb-mkclean
bbb-playback-build:
extends: .build_job
script:
- build/setup-inside-docker.sh bbb-playback
bbb-playback-notes-build:
extends: .build_job
script:
- build/setup-inside-docker.sh bbb-playback-notes
bbb-playback-podcast-build:
extends: .build_job
script:
- build/setup-inside-docker.sh bbb-playback-podcast
bbb-playback-presentation-build:
extends: .build_job
script:
- build/setup-inside-docker.sh bbb-playback-presentation
bbb-playback-screenshare-build:
extends: .build_job
script:
- build/setup-inside-docker.sh bbb-playback-screenshare
bbb-record-core-build:
extends: .build_job
script:
- build/setup-inside-docker.sh bbb-record-core
bbb-web-build:
extends: .build_job
script:
- build/setup-inside-docker.sh bbb-web
bbb-webhooks-build:
extends: .build_job
script:
- build/setup-inside-docker.sh bbb-webhooks
bbb-webrtc-sfu-build:
extends: .build_job
script:
- build/setup-inside-docker.sh bbb-webrtc-sfu
bigbluebutton-build:
extends: .build_job
script:
- build/setup-inside-docker.sh bigbluebutton
# upload packages to repo server
push_packages:
stage: push packages
script: build/push_packages.sh
resource_group: push_packages
# uncomment the lines below if you want one final
# "artifacts" dir with all packages (increases runtime, fills up space on gitlab server)
#artifacts:
# paths:
# - artifacts/*
# expire_in: 2 days

View File

@ -1,435 +0,0 @@
This document provides instructions for developers to setup their
environment and work on the upcoming BBB 2.0 (tentative release version).
## Install BBB 1.1
Follow the [install instructions](http://docs.bigbluebutton.org/install/install.html) for 1.1.
Make sure you have a working BBB 1.1 before you proceed with the instructions below.
## Setup development environment
Setup your development environment following these [instructions](http://docs.bigbluebutton.org/dev/setup.html)
## Checkout development branch
Checkout the development branch `move-java-classes-from-bbb-web-to-bbb-common-web` from this [repository](https://github.com/ritzalam/bigbluebutton)
Open nine (9) terminal windows so you will dedicate one window for each bbb-component.
You can name them client, bbb-apps, apps-common, red5, akka-apps, akka-fsesl, bbb-web, common-web, and messages.
## Building the client
On you bbb-client terminal, run the following commands.
```
cd ~/dev/bigbluebutton/bigbluebutton-client
```
Build build a specific locale (en_US default)
```
ant locale -DLOCALE=en_US
```
To build all locales
```
ant locales
```
This will take about 10 minutes (depending on the speed of your computer). Next, let's build the client
```
ant
```
This will create a build of the BigBlueButton client in the `/home/firstuser/dev/bigbluebutton/bigbluebutton-client/client` directory.
## Build BBB Red5 Applications
On your red5 terminal, turn off red5 service
```
sudo systemctl stop red5
```
You need to make `red5/webapps` writeable. Otherwise, you will get a permission error when you try to deploy into Red5.
```
sudo chmod -R 777 /usr/share/red5/webapps
```
### Build common-message
On your message terminal, run the following commands. Other components depends on this, so build this first.
```
cd ~/dev/bigbluebutton/bbb-common-message/
sbt clean
sbt publish
sbt publishLocal
```
### Build bbb-apps
We've split bbb-apps into bbb-apps-common and bigbluebutton-apps. We need to build bbb-apps-common first.
On your apps-common terminal, build the bbb-apps-common component.
```
cd ~/dev/bigbluebutton/bbb-apps-common/
# Force updating of bbb-commons-message
sbt clean
# Build and share library
sbt publish publishLocal
```
On your bbb-apps terminal, run the following commands.
```
cd ~/dev/bigbluebutton/bigbluebutton-apps/
# To make sure the lib folder is clean of old dependencies especially if you've used this
# dev environment for BBB 1.1, delete the contents of the lib directory. You can only to
# do once.
rm lib/*
# Force updating dependencies (bbb-apps-common)
gradle clean
gradle resolveDeps
gradle war deploy
```
## Manually start services
### Run Red5
On your red5 terminal, start red5.
```
cd /usr/share/red5
sudo -u red5 ./red5.sh
```
### Run Akka Apps
On your akka-apps terminal, start akka-apps
```
cd ~/dev/bigbluebutton/akka-bbb-apps
# To make sure the lib folder is clean of old dependencies especially if you've used this
# dev environment for BBB 1.1, delete the contents of the lib directory. You can only to
# do once.
rm lib_managed/*
# We need to stop the existing packaged akka-apps
sudo systemctl stop bbb-apps-akka
# Now we can run our own
sbt clean
sbt run
```
### Run Akka FSESL App
On your akka-fsesl terminal, start akka-fsesl
```
cd ~/dev/bigbluebutton/akka-bbb-fsesl
# To make sure the lib folder is clean of old dependencies especially if you've used this
# dev environment for BBB 1.1, delete the contents of the lib directory. You can only to
# do once.
rm lib_managed/*
# We need to stop the existing packaged akka-fsesl
sudo systemctl stop bbb-fsesl-akka
# Now we can run our own
sbt clean
sbt run
```
### Build bbb-web
We've split up bbb-web into bbb-common-web and bigbluebutton-web. We need to build
bbb-common-web first.
On your common-web terminal, run these commands
```
cd ~/dev/bigbluebutton/bbb-common-web/
# To make sure the lib folder is clean of old dependencies especially if you've used this
# dev environment for BBB 1.1, delete the contents of the lib directory. You can only to
# do once.
rm lib_managed/*
# Force updating of dependencies especially bbb-commons-message
sbt clean
sbt publish publishLocal
```
### Run bbb-web
First we need to remove the old `bbb-web` app from tomcat to avoid duplicate messages
```
sudo cp /var/lib/tomcat7/webapps/bigbluebutton.war /var/lib/tomcat7/webapps/bigbluebutton.war-packaged
sudo rm -r /var/lib/tomcat7/webapps/bigbluebutton
```
On your bbb-web terminal, start bbb-web
```
cd ~/dev/bigbluebutton/bigbluebutton-web
```
Get the salt and BBB URL from `/var/lib/tomcat7/webapps/demo/bbb_api_conf.jsp`
Edit `grails-app/conf/bigbluebutton.properties` and change the following with
the salt and IP you got from above.
```
bigbluebutton.web.serverURL=http://192.168.74.128
securitySalt=856d5e0197b1aa0cf79897841142a5f6
```
Start bbb-web
```
# To make sure the lib folder is clean of old dependencies especially if you've used this
# dev environment for BBB 1.1, delete the contents of the lib directory. You can only to
# do once.
rm lib/*
gradle clean
gradle resolveDeps
grails clean
sudo chmod -R ugo+rwx /var/log/bigbluebutton
grails -Dserver.port=8888 run-war
```
If things started without errors, congrats!
## Converting and Adding new messages
In bigbluebutton-apps, from [InMessages.scala](https://github.com/bigbluebutton/bigbluebutton/blob/bbb-2x-mconf/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/api/InMessages.scala) choose the message to convert.
```
case class UserShareWebcam(meetingID: String, userId: String, stream: String) extends InMessage
```
In bbb-apps-common, add new message in [BbbCoreEnvelope.scala](https://github.com/bigbluebutton/bigbluebutton/blob/bbb-2x-mconf/bbb-common-message/src/main/scala/org/bigbluebutton/common2/messages/BbbCoreEnvelope.scala)
```
object UserShareWebcamMsg { val NAME = "UserShareWebcamMsg" }
case class UserShareWebcamMsg(header: BbbClientMsgHeader, body: UserShareWebcamMsgBody)
```
Define `UserShareWebcamMsgBody` in `MessageBody.scala`
```
case class UserShareWebcamMsgBody(userId: String, stream: String)
```
From the client, send message as
```
{
"header": {
"name": "UserShareWebcamMsg",
"meetingId": "foo-meetingId",
"userId": "bar-userId"
},
"body": {
"streamId": "my-webcam-stream"
}
}
```
In [ReceivedJsonMsgHandlerActor](https://github.com/bigbluebutton/bigbluebutton/blob/bbb-2x-mconf/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala), deserialize the message with implementation in [ReceivedJsonMsgDeserializer](https://github.com/bigbluebutton/bigbluebutton/blob/bbb-2x-mconf/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgDeserializer.scala).
```
case UserShareWebcamMsg.NAME =>
for {
m <- routeUserShareWebcamMsg(jsonNode)
} yield {
send(envelope, m)
}
```
Route the message in [ReceivedMessageRouter](https://github.com/bigbluebutton/bigbluebutton/blob/bbb-2x-mconf/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/ReceivedMessageRouter.scala).
```
def send(envelope: BbbCoreEnvelope, msg: UserShareWebcamMsg): Unit = {
val event = BbbMsgEvent(msg.header.meetingId, BbbCommonEnvCoreMsg(envelope, msg))
publish(event)
}
```
Handle the message in [MeestingActor](https://github.com/bigbluebutton/bigbluebutton/blob/bbb-2x-mconf/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala) replacing the old implementation.
A complete example would be the `ValidateAuthTokenReqMsg`.
## Installing Flex SDK 4.16.0 and Playerglobal 23.0 for version 2.1 development
In the next step, you need to get the Apache Flex 4.16.0 SDK package.
Next, you need to make a directory to hold the tools needed for BigBlueButton development.
~~~
mkdir -p ~/dev/tools
cd ~/dev/tools
~~~
First, you need to download the SDK tarball from an Apache mirror site and then unpack it.
~~~
wget https://archive.apache.org/dist/flex/4.16.0/binaries/apache-flex-sdk-4.16.0-bin.tar.gz
tar xvfz apache-flex-sdk-4.16.0-bin.tar.gz
~~~
Next, create a linked directory with a shortened name for easier referencing.
~~~
ln -s ~/dev/tools/apache-flex-sdk-4.16.0-bin ~/dev/tools/flex
~~~
Once the Apache Flex SDK is unpacked, you need to download one of the dependencies manually because the file was moved from its original URL.
~~~
wget --content-disposition https://github.com/swfobject/swfobject/archive/2.2.tar.gz
tar xvfz swfobject-2.2.tar.gz
cp -r swfobject-2.2/swfobject flex/templates/
~~~
Now that we've finished with the first dependency we need to download the Adobe Flex SDK. We'll do this step manually in case the download fails (if it does, remove the incomplete file and issue the `wget` command again).
~~~
cd flex/
mkdir -p in/
wget http://download.macromedia.com/pub/flex/sdk/builds/flex4.6/flex_sdk_4.6.0.23201B.zip -P in/
~~~
Once the supplementary SDK has downloaded, we can use its `build.xml` script to automatically download the remaining third-party tools.
~~~
ant -f frameworks/build.xml thirdparty-downloads
~~~
After Flex downloads the remaining third-party tools, you need to modify their permissions.
~~~
find ~/dev/tools/flex -type d -exec chmod o+rx '{}' \;
chmod 755 ~/dev/tools/flex/bin/*
chmod -R +r ~/dev/tools/flex
~~~
The next step in setting up the Flex SDK environment is to download a Flex library for video.
~~~
mkdir -p ~/dev/tools/flex/frameworks/libs/player/23.0
cd ~/dev/tools/flex/frameworks/libs/player/23.0
wget http://fpdownload.macromedia.com/get/flashplayer/installers/archive/playerglobal/playerglobal23_0.swc
mv -f playerglobal23_0.swc playerglobal.swc
~~~
The last step to have a working Flex SDK is to configure it to work with playerglobal 23.0
~~~
cd ~/dev/tools/flex
sed -i "s/11.1/23.0/g" frameworks/flex-config.xml
sed -i "s/<swf-version>14<\/swf-version>/<swf-version>34<\/swf-version>/g" frameworks/flex-config.xml
sed -i "s/{playerglobalHome}\/{targetPlayerMajorVersion}.{targetPlayerMinorVersion}/libs\/player\/23.0/g" frameworks/flex-config.xml
~~~
With the tools installed, you need to add a set of environment variables to your `.profile` to access these tools.
~~~
vi ~/.profile
~~~
Copy-and-paste the following text at bottom of `.profile`.
~~~
export FLEX_HOME=$HOME/dev/tools/flex
export PATH=$PATH:$FLEX_HOME/bin
export ANT_OPTS="-Xmx512m -XX:MaxPermSize=512m"
~~~
Reload your profile to use these tools (this will happen automatically when you next login).
~~~
source ~/.profile
~~~
Check that the tools are now in your path by running the following command.
~~~
$ mxmlc -version
Version 4.16.0 build 20170305
~~~
## Configure the client to use CDN
It is possible to load the SWF application, modules and and locales from an external server; a CDN as a example. Let's suppose that our eternal domain is `cdn.company.org`
1.First create a `crossdomain.xml` file into `/var/www/bigbluebutton-default/`
~~~xml
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<site-control permitted-cross-domain-policies="master-only"/>
<allow-access-from domain="cdn.company.org"/>
</cross-domain-policy>
~~~
For more information about crossdomain policy please visit this link http://www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html
2.In config.xml you can configure some assets to be loaded from an external server. The same can also be done for every `url` property for `module` tag. Please check the example below
~~~xml
<language ... localesConfig="http://cdn.company.org/client/conf/locales.xml"
localesDirectory="http://cdn.company.org/client/locale/" />
<skinning url="http://cdn.company.org/client/branding/css/V2Theme.css.swf" />
<branding logo="http://cdn.company.org/client/logo.swf" ...
background="http://cdn.company.org/client/background.png" />
...
<module name="ChatModule" url="http://cdn.company.org/client/ChatModule.swf" ... />
~~~
3.Then in `BigBlueButton.html` make sure you load the main swf from your remote URL. You can make the same with all other assets.
~~~js
content.innerHTML = '<object type="application/x-shockwave-flash" id="BigBlueButton" name="BigBlueButton" tabindex="0" data="http://cdn.company.org/client/BigBlueButton.swf" style="position: relative; top: 0.5px;" width="100%" height="100%" align="middle"><param name="quality" value="high"><param name="bgcolor" value="#FFFFFF"><param name="allowfullscreen" value="true"><param name="allowfullscreeninteractive" value="true"><param name="wmode" value="window"><param name="allowscriptaccess" value="always"><param name="seamlesstabbing" value="true"></object>';
}
};
} else {
swfobject.embedSWF("http://cdn.company.org/client/BigBlueButton.swf", "altFlash", "100%", "100%", "11.0.0", "http://cdn.company.org/client/expressInstall.swf", flashvars, params, attributes, embedCallback);
}
~~~

View File

@ -13,7 +13,7 @@ We designed BigBlueButton for online learning (though it can be used for many [o
* Group collaboration (many-to-many)
* Online classes (one-to-many)
You can install on a Ubuntu 16.04 64-bit server. We provide [bbb-install.sh](https://github.com/bigbluebutton/bbb-install) to let you have a server up and running within 30 minutes (or your money back 😉).
You can install on a Ubuntu 18.04 64-bit server. We provide [bbb-install.sh](https://github.com/bigbluebutton/bbb-install) to let you have a server up and running 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/).

View File

@ -7,8 +7,12 @@ We actively support BigBlueButton through the community forums and through secur
| Version | Supported |
| ------- | ------------------ |
| 2.0.x (or earlier) | :x: |
| 2.2.x | :white_check_mark: |
| 2.3-dev | :white_check_mark: |
| 2.2.x | :x: |
| 2.3.x | :white_check_mark: |
We have released 2.3 to the community and all our support efforts are now transitioned to 2.3. Also, BigBlueButton 2.2 is running on Ubuntu 16.04 which is now end of life.
As such, we highly recommend that all administrators deploy 2.3 going forward as it is built upon Ubuntu 18.04. You'll find [many improvements](/2.3/new.html) in this newer version.
## Reporting a Vulnerability

View File

@ -3,8 +3,11 @@ package org.bigbluebutton
import akka.http.scaladsl.model._
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import akka.http.scaladsl.server.Directives._
import org.bigbluebutton.service.{ HealthzService, PubSubReceiveStatus, PubSubSendStatus, RecordingDBSendStatus }
import spray.json.DefaultJsonProtocol
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.service.{ HealthzService, MeetingInfoService, PubSubReceiveStatus, PubSubSendStatus, RecordingDBSendStatus }
import spray.json._
import scala.concurrent._
import ExecutionContext.Implicits.global
case class HealthResponse(
isHealthy: Boolean,
@ -13,14 +16,56 @@ case class HealthResponse(
recordingDbStatus: RecordingDBSendStatus
)
trait JsonSupportProtocol extends SprayJsonSupport with DefaultJsonProtocol {
case class MeetingInfoResponse(
meetingInfoResponse: Option[MeetingInfoAnalytics]
)
case class MeetingInfoAnalytics(
name: String,
externalId: String,
internalId: String,
hasUserJoined: Boolean,
isMeetingRecorded: Boolean,
webcams: Webcam,
audio: Audio,
screenshare: Screenshare,
users: List[Participant],
presentation: PresentationInfo,
breakoutRoom: BreakoutRoom
)
trait JsonSupportProtocolHealthResponse extends SprayJsonSupport with DefaultJsonProtocol {
implicit val pubSubSendStatusJsonFormat = jsonFormat2(PubSubSendStatus)
implicit val pubSubReceiveStatusJsonFormat = jsonFormat2(PubSubReceiveStatus)
implicit val recordingDbStatusJsonFormat = jsonFormat2(RecordingDBSendStatus)
implicit val healthServiceJsonFormat = jsonFormat4(HealthResponse)
}
class ApiService(healthz: HealthzService) extends JsonSupportProtocol {
trait JsonSupportProtocolMeetingInfoResponse extends SprayJsonSupport with DefaultJsonProtocol {
implicit val meetingInfoUserJsonFormat = jsonFormat2(User)
implicit val meetingInfoBroadcastJsonFormat = jsonFormat3(Broadcast)
implicit val meetingInfoWebcamStreamJsonFormat = jsonFormat2(WebcamStream)
implicit val meetingInfoWebcamJsonFormat = jsonFormat2(Webcam)
implicit val meetingInfoListenOnlyAudioJsonFormat = jsonFormat2(ListenOnlyAudio)
implicit val meetingInfoTwoWayAudioJsonFormat = jsonFormat2(TwoWayAudio)
implicit val meetingInfoPhoneAudioJsonFormat = jsonFormat2(PhoneAudio)
implicit val meetingInfoAudioJsonFormat = jsonFormat4(Audio)
implicit val meetingInfoScreenshareStreamJsonFormat = jsonFormat2(ScreenshareStream)
implicit val meetingInfoScreenshareJsonFormat = jsonFormat1(Screenshare)
implicit val meetingInfoPresentationInfoJsonFormat = jsonFormat2(PresentationInfo)
implicit val meetingInfoBreakoutRoomJsonFormat = jsonFormat2(BreakoutRoom)
implicit val meetingInfoParticipantJsonFormat = jsonFormat3(Participant)
implicit val meetingInfoAnalyticsJsonFormat = jsonFormat11(MeetingInfoAnalytics)
implicit val meetingInfoResponseJsonFormat = jsonFormat1(MeetingInfoResponse)
}
class ApiService(healthz: HealthzService, meetingInfoz: MeetingInfoService)
extends JsonSupportProtocolHealthResponse
with JsonSupportProtocolMeetingInfoResponse {
def routes =
path("healthz") {
@ -51,5 +96,33 @@ class ApiService(healthz: HealthzService) extends JsonSupportProtocol {
}
}
}
}
} ~
path("analytics") {
parameter('meetingId.as[String]) { meetingId =>
get {
val meetingAnalyticsFuture = meetingInfoz.getAnalytics(meetingId)
val entityFuture = meetingAnalyticsFuture.map { resp =>
resp.optionMeetingInfoAnalytics match {
case Some(_) =>
HttpEntity(ContentTypes.`application/json`, resp.optionMeetingInfoAnalytics.get.toJson.prettyPrint)
case None =>
HttpEntity(ContentTypes.`application/json`, s"""{ "message": "No active meeting with ID $meetingId"}""".parseJson.prettyPrint)
}
}
complete(entityFuture)
}
} ~
get {
val future = meetingInfoz.getAnalytics()
val entityFuture = future.map { res =>
res.optionMeetingsInfoAnalytics match {
case Some(_) =>
HttpEntity(ContentTypes.`application/json`, res.optionMeetingsInfoAnalytics.get.toJson.prettyPrint)
case None =>
HttpEntity(ContentTypes.`application/json`, """{ "message": "No active meetings"}""".parseJson.prettyPrint)
}
}
complete(entityFuture)
}
}
}

View File

@ -12,8 +12,9 @@ import org.bigbluebutton.core2.AnalyticsActor
import org.bigbluebutton.core2.FromAkkaAppsMsgSenderActor
import org.bigbluebutton.endpoint.redis.AppsRedisSubscriberActor
import org.bigbluebutton.endpoint.redis.RedisRecorderActor
import org.bigbluebutton.endpoint.redis.LearningDashboardActor
import org.bigbluebutton.common2.bus.IncomingJsonMessageBus
import org.bigbluebutton.service.HealthzService
import org.bigbluebutton.service.{ HealthzService, MeetingInfoActor, MeetingInfoService }
object Boot extends App with SystemConfiguration {
@ -40,21 +41,32 @@ object Boot extends App with SystemConfiguration {
)
val msgSender = new MessageSender(redisPublisher)
val bbbMsgBus = new BbbMsgRouterEventBus
val healthzService = HealthzService(system)
val apiService = new ApiService(healthzService)
val meetingInfoActorRef = system.actorOf(MeetingInfoActor.props())
outBus2.subscribe(meetingInfoActorRef, outBbbMsgMsgChannel)
bbbMsgBus.subscribe(meetingInfoActorRef, analyticsChannel)
val meetingInfoService = MeetingInfoService(system, meetingInfoActorRef)
val apiService = new ApiService(healthzService, meetingInfoService)
val redisRecorderActor = system.actorOf(
RedisRecorderActor.props(system, redisConfig, healthzService),
"redisRecorderActor"
)
val learningDashboardActor = system.actorOf(
LearningDashboardActor.props(system, outGW),
"LearningDashboardActor"
)
recordingEventBus.subscribe(redisRecorderActor, outMessageChannel)
val incomingJsonMessageBus = new IncomingJsonMessageBus
val bbbMsgBus = new BbbMsgRouterEventBus
val fromAkkaAppsMsgSenderActorRef = system.actorOf(FromAkkaAppsMsgSenderActor.props(msgSender))
val analyticsActorRef = system.actorOf(AnalyticsActor.props(analyticsIncludeChat))
@ -64,6 +76,9 @@ object Boot extends App with SystemConfiguration {
outBus2.subscribe(analyticsActorRef, outBbbMsgMsgChannel)
bbbMsgBus.subscribe(analyticsActorRef, analyticsChannel)
outBus2.subscribe(learningDashboardActor, outBbbMsgMsgChannel)
bbbMsgBus.subscribe(learningDashboardActor, analyticsChannel)
val bbbActor = system.actorOf(BigBlueButtonActor.props(system, eventBus, bbbMsgBus, outGW, healthzService), "bigbluebutton-actor")
eventBus.subscribe(bbbActor, meetingManagerChannel)

View File

@ -75,6 +75,14 @@ case class BreakoutRoomUsersUpdateInternalMsg(parentId: String, breakoutId: Stri
*/
case class EndBreakoutRoomInternalMsg(parentId: String, breakoutId: String, reason: String) extends InMessage
/**
* Sent by parent meeting to breakout room to extend time.
* @param parentId
* @param breakoutId
* @param extendTimeInMinutes
*/
case class ExtendBreakoutRoomTimeInternalMsg(parentId: String, breakoutId: String, extendTimeInMinutes: Int) 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

@ -9,11 +9,13 @@ object BreakoutModel {
externalId: String,
name: String,
sequence: Integer,
shortName: String,
isDefaultName: Boolean,
freeJoin: Boolean,
voiceConf: String,
assignedUsers: Vector[String]
): BreakoutRoom2x = {
new BreakoutRoom2x(id, externalId, name, parentId, sequence, freeJoin, voiceConf, assignedUsers, Vector(), Vector(), None, false)
new BreakoutRoom2x(id, externalId, name, parentId, sequence, shortName, isDefaultName, freeJoin, voiceConf, assignedUsers, Vector(), Vector(), None, false)
}
}
@ -75,4 +77,9 @@ case class BreakoutModel(
def removeRoom(id: String): BreakoutModel = {
copy(rooms = rooms - id)
}
def extendTime(timeToExtendInMinutes: Int): BreakoutModel = {
copy(durationInMinutes = durationInMinutes + timeToExtendInMinutes)
}
}

View File

@ -0,0 +1,27 @@
package org.bigbluebutton.core.apps
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
import org.bigbluebutton.core2.message.senders.{ MsgBuilder }
object ExternalVideoModel {
def setURL(externalVideoModel: ExternalVideoModel, externalVideoUrl: String) {
externalVideoModel.externalVideoUrl = externalVideoUrl
}
def clear(externalVideoModel: ExternalVideoModel) {
externalVideoModel.externalVideoUrl = ""
}
def stop(outGW: OutMsgRouter, liveMeeting: LiveMeeting) {
if (!liveMeeting.externalVideoModel.externalVideoUrl.isEmpty) {
liveMeeting.externalVideoModel.externalVideoUrl = ""
val event = MsgBuilder.buildStopExternalVideoEvtMsg(liveMeeting.props.meetingProp.intId)
outGW.send(event)
}
}
}
class ExternalVideoModel {
private var externalVideoUrl = ""
}

View File

@ -10,10 +10,12 @@ trait BreakoutApp2x extends BreakoutRoomCreatedMsgHdlr
with BreakoutRoomUsersUpdateMsgHdlr
with CreateBreakoutRoomsCmdMsgHdlr
with EndAllBreakoutRoomsMsgHdlr
with ExtendBreakoutRoomsTimeMsgHdlr
with RequestBreakoutJoinURLReqMsgHdlr
with SendBreakoutUsersUpdateMsgHdlr
with TransferUserToMeetingRequestHdlr
with EndBreakoutRoomInternalMsgHdlr
with ExtendBreakoutRoomTimeInternalMsgHdlr
with BreakoutRoomEndedInternalMsgHdlr {
this: MeetingActor =>

View File

@ -68,7 +68,7 @@ trait BreakoutRoomCreatedMsgHdlr {
def sendBreakoutRoomsList(breakoutModel: BreakoutModel): BreakoutModel = {
val breakoutRooms = breakoutModel.rooms.values.toVector map { r =>
new BreakoutRoomInfo(r.name, r.externalId, r.id, r.sequence, r.freeJoin)
new BreakoutRoomInfo(r.name, r.externalId, r.id, r.sequence, r.shortName, r.isDefaultName, r.freeJoin)
}
log.info("Sending breakout rooms list to {} with containing {} room(s)", liveMeeting.props.meetingProp.intId, breakoutRooms.length)
@ -95,7 +95,7 @@ trait BreakoutRoomCreatedMsgHdlr {
BbbCommonEnvCoreMsg(envelope, event)
}
val breakoutInfo = BreakoutRoomInfo(room.name, room.externalId, room.id, room.sequence, room.freeJoin)
val breakoutInfo = BreakoutRoomInfo(room.name, room.externalId, room.id, room.sequence, room.shortName, room.isDefaultName, room.freeJoin)
val event = build(liveMeeting.props.meetingProp.intId, breakoutInfo)
outGW.send(event)

View File

@ -3,6 +3,7 @@ package org.bigbluebutton.core.apps.breakout
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.api.BreakoutRoomUsersUpdateInternalMsg
import org.bigbluebutton.core.domain.{ BreakoutRoom2x, MeetingState2x }
import org.bigbluebutton.core.models.Users2x
import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter }
trait BreakoutRoomUsersUpdateMsgHdlr {
@ -30,6 +31,13 @@ trait BreakoutRoomUsersUpdateMsgHdlr {
val updatedRoom = room.copy(users = msg.users, voiceUsers = msg.voiceUsers)
val msgEvent = broadcastEvent(updatedRoom)
outGW.send(msgEvent)
//Update user lastActivityTime in parent room (to avoid be ejected while is in Breakout room)
for {
breakoutRoomUser <- updatedRoom.users
user <- Users2x.findWithBreakoutRoomId(liveMeeting.users2x, breakoutRoomUser.id)
} yield Users2x.updateLastUserActivity(liveMeeting.users2x, user)
model.update(updatedRoom)
}

View File

@ -28,7 +28,7 @@ trait BreakoutRoomsListMsgHdlr {
breakoutModel <- state.breakout
} yield {
val rooms = breakoutModel.rooms.values.toVector map { r =>
new BreakoutRoomInfo(r.name, r.externalId, r.id, r.sequence, r.freeJoin)
new BreakoutRoomInfo(r.name, r.externalId, r.id, r.sequence, r.shortName, r.isDefaultName, r.freeJoin)
}
val ready = breakoutModel.hasAllStarted()
broadcastEvent(rooms, ready)

View File

@ -47,7 +47,7 @@ trait CreateBreakoutRoomsCmdMsgHdlr extends RightsManagementTrait {
val (internalId, externalId) = BreakoutRoomsUtil.createMeetingIds(liveMeeting.props.meetingProp.intId, i)
val voiceConf = BreakoutRoomsUtil.createVoiceConfId(liveMeeting.props.voiceProp.voiceConf, i)
val breakout = BreakoutModel.create(parentId, internalId, externalId, room.name, room.sequence, room.freeJoin, voiceConf, room.users)
val breakout = BreakoutModel.create(parentId, internalId, externalId, room.name, room.sequence, room.shortName, room.isDefaultName, room.freeJoin, voiceConf, room.users)
rooms = rooms + (breakout.id -> breakout)
}
@ -56,6 +56,8 @@ trait CreateBreakoutRoomsCmdMsgHdlr extends RightsManagementTrait {
breakout.id, breakout.name,
liveMeeting.props.meetingProp.intId,
breakout.sequence,
breakout.shortName,
breakout.isDefaultName,
breakout.freeJoin,
liveMeeting.props.voiceProp.dialNumber,
breakout.voiceConf,

View File

@ -0,0 +1,27 @@
package org.bigbluebutton.core.apps.breakout
import org.bigbluebutton.core.api.{ ExtendBreakoutRoomTimeInternalMsg }
import org.bigbluebutton.core.domain.{ MeetingState2x }
import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter }
trait ExtendBreakoutRoomTimeInternalMsgHdlr {
this: MeetingActor =>
val outGW: OutMsgRouter
def handleExtendBreakoutRoomTimeInternalMsgHdlr(msg: ExtendBreakoutRoomTimeInternalMsg, state: MeetingState2x): MeetingState2x = {
val breakoutModel = for {
model <- state.breakout
} yield {
val updatedBreakoutModel = model.extendTime(msg.extendTimeInMinutes)
updatedBreakoutModel
}
breakoutModel match {
case Some(model) => state.update(Some(model))
case None => state
}
}
}

View File

@ -0,0 +1,83 @@
package org.bigbluebutton.core.apps.breakout
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.api.{ ExtendBreakoutRoomTimeInternalMsg, SendTimeRemainingAuditInternalMsg }
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
import org.bigbluebutton.core.bus.BigBlueButtonEvent
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter }
import org.bigbluebutton.core.util.TimeUtil
trait ExtendBreakoutRoomsTimeMsgHdlr extends RightsManagementTrait {
this: MeetingActor =>
val outGW: OutMsgRouter
def handleExtendBreakoutRoomsTimeMsg(msg: ExtendBreakoutRoomsTimeReqMsg, state: MeetingState2x): MeetingState2x = {
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to extend time for breakout rooms for meeting."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)
state
} else if (msg.body.extendTimeInMinutes <= 0) {
log.error("Error while trying to extend {} minutes for breakout rooms time in meeting {}. Only positive values are allowed!", msg.body.extendTimeInMinutes, props.meetingProp.intId)
state
} else {
val updatedModel = for {
breakoutModel <- state.breakout
startedOn <- breakoutModel.startedOn
} yield {
val breakoutRoomEndTime = TimeUtil.millisToSeconds(startedOn) + TimeUtil.minutesToSeconds(breakoutModel.durationInMinutes)
val breakoutRoomSecsRemaining = breakoutRoomEndTime - TimeUtil.millisToSeconds(System.currentTimeMillis())
var isExtendTimeHigherThanMeetingRemaining = false
if (state.expiryTracker.durationInMs > 0) {
val mainRoomEndTime = state.expiryTracker.startedOnInMs + state.expiryTracker.durationInMs
val mainRoomSecsRemaining = TimeUtil.millisToSeconds(mainRoomEndTime - TimeUtil.timeNowInMs())
val mainRoomTimeRemainingInMinutes = mainRoomSecsRemaining / 60
//Avoid breakout room end later than main room
//Keep 5 seconds of margin to finish breakout room and send informations to parent meeting
if (breakoutRoomSecsRemaining + TimeUtil.minutesToSeconds(msg.body.extendTimeInMinutes) > (mainRoomSecsRemaining - 5)) {
log.error("Error while trying to extend {} minutes for breakout rooms time in meeting {}. Parent meeting will end up in {} minutes!", msg.body.extendTimeInMinutes, props.meetingProp.intId, mainRoomTimeRemainingInMinutes)
isExtendTimeHigherThanMeetingRemaining = true
}
}
if (isExtendTimeHigherThanMeetingRemaining) {
breakoutModel
} else {
breakoutModel.rooms.values.foreach { room =>
eventBus.publish(BigBlueButtonEvent(room.id, ExtendBreakoutRoomTimeInternalMsg(props.breakoutProps.parentId, room.id, msg.body.extendTimeInMinutes)))
}
log.debug("Extending {} minutes for breakout rooms time in meeting {}", msg.body.extendTimeInMinutes, props.meetingProp.intId)
breakoutModel.extendTime(msg.body.extendTimeInMinutes)
}
}
val event = buildExtendBreakoutRoomsTimeEvtMsg(msg.body.extendTimeInMinutes)
outGW.send(event)
//Force Update time remaining in the clients
eventBus.publish(BigBlueButtonEvent(props.meetingProp.intId, SendTimeRemainingAuditInternalMsg(props.meetingProp.intId)))
updatedModel match {
case Some(model) => state.update(Some(model))
case None => state
}
}
}
def buildExtendBreakoutRoomsTimeEvtMsg(extendTimeInMinutes: Int): BbbCommonEnvCoreMsg = {
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
val envelope = BbbCoreEnvelope(ExtendBreakoutRoomsTimeEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(ExtendBreakoutRoomsTimeEvtMsg.NAME, liveMeeting.props.meetingProp.intId, "not-used")
val body = ExtendBreakoutRoomsTimeEvtMsgBody(props.meetingProp.intId, extendTimeInMinutes)
val event = ExtendBreakoutRoomsTimeEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
}

View File

@ -4,6 +4,7 @@ import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter }
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
import org.bigbluebutton.core.models.{ Users2x, Roles }
trait RequestBreakoutJoinURLReqMsgHdlr extends RightsManagementTrait {
this: MeetingActor =>
@ -19,15 +20,22 @@ trait RequestBreakoutJoinURLReqMsgHdlr extends RightsManagementTrait {
for {
model <- state.breakout
room <- model.find(msg.body.breakoutId)
requesterUser <- Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId)
} yield {
BreakoutHdlrHelpers.sendJoinURL(
liveMeeting,
outGW,
msg.body.userId,
room.externalId,
room.sequence.toString(),
room.id
)
if (requesterUser.role == Roles.MODERATOR_ROLE || room.freeJoin) {
BreakoutHdlrHelpers.sendJoinURL(
liveMeeting,
outGW,
msg.body.userId,
room.externalId,
room.sequence.toString(),
room.id
)
} else {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to request breakout room URL for meeting."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)
}
}
}

View File

@ -1,10 +1,11 @@
package org.bigbluebutton.core.apps.externalvideo
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait, ExternalVideoModel }
import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.running.{ LiveMeeting }
trait StartExternalVideoPubMsgHdlr {
trait StartExternalVideoPubMsgHdlr extends RightsManagementTrait {
this: ExternalVideoApp2x =>
def handle(msg: StartExternalVideoPubMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
@ -22,6 +23,13 @@ trait StartExternalVideoPubMsgHdlr {
bus.outGW.send(msgEvent)
}
broadcastEvent(msg)
if (permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "You need to be the presenter to start external videos"
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
} else {
ExternalVideoModel.setURL(liveMeeting.externalVideoModel, msg.body.externalVideoUrl)
broadcastEvent(msg)
}
}
}

View File

@ -1,10 +1,11 @@
package org.bigbluebutton.core.apps.externalvideo
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait, ExternalVideoModel }
import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.running.{ LiveMeeting }
trait StopExternalVideoPubMsgHdlr {
trait StopExternalVideoPubMsgHdlr extends RightsManagementTrait {
this: ExternalVideoApp2x =>
def handle(msg: StopExternalVideoPubMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
@ -22,6 +23,13 @@ trait StopExternalVideoPubMsgHdlr {
bus.outGW.send(msgEvent)
}
broadcastEvent()
if (permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "You need to be the presenter to stop external video"
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
} else {
ExternalVideoModel.clear(liveMeeting.externalVideoModel)
broadcastEvent()
}
}
}

View File

@ -1,10 +1,11 @@
package org.bigbluebutton.core.apps.externalvideo
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.running.{ LiveMeeting }
trait UpdateExternalVideoPubMsgHdlr {
trait UpdateExternalVideoPubMsgHdlr extends RightsManagementTrait {
def handle(msg: UpdateExternalVideoPubMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
def broadcastEvent(msg: UpdateExternalVideoPubMsg) {
@ -18,6 +19,12 @@ trait UpdateExternalVideoPubMsgHdlr {
bus.outGW.send(msgEvent)
}
broadcastEvent(msg)
if (permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "You need to be the presenter to update external video"
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
} else {
broadcastEvent(msg)
}
}
}

View File

@ -1,7 +1,7 @@
package org.bigbluebutton.core.apps.layout
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.models.Layouts
import org.bigbluebutton.core.models.{ Layouts, LayoutsType }
import org.bigbluebutton.core.running.OutMsgRouter
import org.bigbluebutton.core2.MeetingStatus2x
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
@ -17,9 +17,13 @@ trait BroadcastLayoutMsgHdlr extends RightsManagementTrait {
val reason = "No permission to broadcast layout to meeting."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)
} else {
Layouts.setCurrentLayout(liveMeeting.layouts, msg.body.layout, msg.header.userId)
if (LayoutsType.layoutsType.contains(msg.body.layout)) {
val newlayout = LayoutsType.layoutsType.getOrElse(msg.body.layout, "")
sendBroadcastLayoutEvtMsg(msg.header.userId)
Layouts.setCurrentLayout(liveMeeting.layouts, newlayout, msg.header.userId)
sendBroadcastLayoutEvtMsg(msg.header.userId)
}
}
}

View File

@ -23,12 +23,12 @@ trait RespondToPollReqMsgHdlr {
bus.outGW.send(msgEvent)
}
def broadcastUserRespondedToPollRecordMsg(msg: RespondToPollReqMsg, pollId: String, answerIds: Seq[Int]): Unit = {
def broadcastUserRespondedToPollRecordMsg(msg: RespondToPollReqMsg, pollId: String, answerIds: Seq[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, answerIds)
val body = UserRespondedToPollRecordMsgBody(pollId, answerIds, answer, isSecret)
val event = UserRespondedToPollRecordMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
bus.outGW.send(msgEvent)
@ -50,7 +50,12 @@ trait RespondToPollReqMsgHdlr {
msg.body.questionId, msg.body.answerIds, liveMeeting)
} yield {
broadcastPollUpdatedEvent(msg, pollId, updatedPoll)
broadcastUserRespondedToPollRecordMsg(msg, pollId, msg.body.answerIds)
for {
poll <- Polls.getPoll(pollId, liveMeeting.polls)
} yield {
val answerText = poll.questions(0).answers.get(msg.body.answerId).key
broadcastUserRespondedToPollRecordMsg(msg, pollId, msg.body.answerId, answerText, poll.isSecret)
}
for {
presenter <- Users2x.findPresenter(liveMeeting.users2x)

View File

@ -17,7 +17,7 @@ trait StartCustomPollReqMsgHdlr extends RightsManagementTrait {
val envelope = BbbCoreEnvelope(PollStartedEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(PollStartedEvtMsg.NAME, liveMeeting.props.meetingProp.intId, msg.header.userId)
val body = PollStartedEvtMsgBody(msg.header.userId, poll.id, msg.body.pollType, msg.body.question, poll)
val body = PollStartedEvtMsgBody(msg.header.userId, poll.id, msg.body.pollType, msg.body.secretPoll, msg.body.question, poll)
val event = PollStartedEvtMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
bus.outGW.send(msgEvent)
@ -29,7 +29,7 @@ trait StartCustomPollReqMsgHdlr extends RightsManagementTrait {
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
} else {
for {
pvo <- Polls.handleStartCustomPollReqMsg(state, msg.header.userId, msg.body.pollId, msg.body.pollType, msg.body.isMultipleResponse, msg.body.answers, msg.body.question, liveMeeting)
pvo <- Polls.handleStartCustomPollReqMsg(state, msg.header.userId, msg.body.pollId, msg.body.pollType, msg.body.secretPoll, msg.body.isMultipleResponse, msg.body.answers, msg.body.question, liveMeeting)
} yield {
broadcastEvent(msg, pvo)
}

View File

@ -18,7 +18,7 @@ trait StartPollReqMsgHdlr extends RightsManagementTrait {
val envelope = BbbCoreEnvelope(PollStartedEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(PollStartedEvtMsg.NAME, liveMeeting.props.meetingProp.intId, msg.header.userId)
val body = PollStartedEvtMsgBody(msg.header.userId, poll.id, msg.body.pollType, msg.body.question, poll)
val body = PollStartedEvtMsgBody(msg.header.userId, poll.id, msg.body.pollType, msg.body.secretPoll, msg.body.question, poll)
val event = PollStartedEvtMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
bus.outGW.send(msgEvent)
@ -30,7 +30,7 @@ trait StartPollReqMsgHdlr extends RightsManagementTrait {
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
} else {
for {
pvo <- Polls.handleStartPollReqMsg(state, msg.header.userId, msg.body.pollId, msg.body.pollType, msg.body.question, msg.body.isMultipleResponse, liveMeeting)
pvo <- Polls.handleStartPollReqMsg(state, msg.header.userId, msg.body.pollId, msg.body.pollType, msg.body.secretPoll, msg.body.isMultipleResponse, msg.body.question, liveMeeting)
} yield {
broadcastEvent(msg, pvo)
}

View File

@ -17,6 +17,7 @@ trait SetPresenterInPodReqMsgHdlr {
): MeetingState2x = {
if (msg.body.podId == PresentationPod.DEFAULT_PRESENTATION_POD) {
// Swith presenter as default presenter pod has changed.
log.info("Presenter pod change will trigger a presenter change")
AssignPresenterActionHandler.handleAction(liveMeeting, bus.outGW, msg.header.userId, msg.body.nextPresenterId)
}
SetPresenterInPodActionHandler.handleAction(state, liveMeeting, bus.outGW, msg.header.userId, msg.body.podId, msg.body.nextPresenterId)
@ -74,4 +75,4 @@ object SetPresenterInPodActionHandler extends RightsManagementTrait {
}
}
}
}
}

View File

@ -1,7 +1,7 @@
package org.bigbluebutton.core.apps.screenshare
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.ScreenshareModel
import org.bigbluebutton.core.apps.{ ScreenshareModel, ExternalVideoModel }
import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.running.LiveMeeting
@ -34,6 +34,9 @@ trait ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr {
// only valid if not broadcasting yet
if (!ScreenshareModel.isBroadcastingRTMP(liveMeeting.screenshareModel)) {
// Stop external video if it's running
ExternalVideoModel.stop(bus.outGW, liveMeeting)
ScreenshareModel.setRTMPBroadcastingUrl(liveMeeting.screenshareModel, msg.body.stream)
ScreenshareModel.broadcastingRTMPStarted(liveMeeting.screenshareModel)
ScreenshareModel.setScreenshareVideoWidth(liveMeeting.screenshareModel, msg.body.vidWidth)

View File

@ -2,6 +2,7 @@ package org.bigbluebutton.core.apps.users
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.presentationpod.SetPresenterInPodActionHandler
import org.bigbluebutton.core.apps.{ ExternalVideoModel }
import org.bigbluebutton.core.models.{ PresentationPod, UserState, Users2x }
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
@ -14,6 +15,7 @@ trait AssignPresenterReqMsgHdlr extends RightsManagementTrait {
val outGW: OutMsgRouter
def handleAssignPresenterReqMsg(msg: AssignPresenterReqMsg, state: MeetingState2x): MeetingState2x = {
log.info("handleAssignPresenterReqMsg: assignedBy={} newPresenterId={}", msg.body.assignedBy, msg.body.newPresenterId)
AssignPresenterActionHandler.handleAction(liveMeeting, outGW, msg.body.assignedBy, msg.body.newPresenterId)
// Change presenter of default presentation pod
@ -67,8 +69,13 @@ object AssignPresenterActionHandler extends RightsManagementTrait {
for {
oldPres <- Users2x.findPresenter(liveMeeting.users2x)
} yield {
Users2x.makeNotPresenter(liveMeeting.users2x, oldPres.intId)
broadcastOldPresenterChange(oldPres)
if (oldPres.intId != newPresenterId) {
// Stop external video if it's running
ExternalVideoModel.stop(outGW, liveMeeting)
Users2x.makeNotPresenter(liveMeeting.users2x, oldPres.intId)
broadcastOldPresenterChange(oldPres)
}
}
for {
@ -79,4 +86,4 @@ object AssignPresenterActionHandler extends RightsManagementTrait {
}
}
}
}
}

View File

@ -3,22 +3,29 @@ package org.bigbluebutton.core.apps.users
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
import org.bigbluebutton.core2.MeetingStatus2x
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
trait UpdateWebcamsOnlyForModeratorCmdMsgHdlr {
trait UpdateWebcamsOnlyForModeratorCmdMsgHdlr extends RightsManagementTrait {
this: UsersApp =>
val liveMeeting: LiveMeeting
val outGW: OutMsgRouter
def handleUpdateWebcamsOnlyForModeratorCmdMsg(msg: UpdateWebcamsOnlyForModeratorCmdMsg) {
log.info("Change webcams only for moderator status. meetingId=" + liveMeeting.props.meetingProp.intId + " webcamsOnlyForModeratorrecording=" + msg.body.webcamsOnlyForModerator)
if (MeetingStatus2x.webcamsOnlyForModeratorEnabled(liveMeeting.status) != msg.body.webcamsOnlyForModerator) {
MeetingStatus2x.setWebcamsOnlyForModerator(liveMeeting.status, msg.body.webcamsOnlyForModerator)
val event = buildWebcamsOnlyForModeratorChangedEvtMsg(liveMeeting.props.meetingProp.intId, msg.body.setBy, msg.body.webcamsOnlyForModerator)
outGW.send(event)
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to change lock settings"
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)
} else {
log.info("Change webcams only for moderator status. meetingId=" + liveMeeting.props.meetingProp.intId + " webcamsOnlyForModeratorrecording=" + msg.body.webcamsOnlyForModerator)
if (MeetingStatus2x.webcamsOnlyForModeratorEnabled(liveMeeting.status) != msg.body.webcamsOnlyForModerator) {
MeetingStatus2x.setWebcamsOnlyForModerator(liveMeeting.status, msg.body.webcamsOnlyForModerator)
val event = buildWebcamsOnlyForModeratorChangedEvtMsg(liveMeeting.props.meetingProp.intId, msg.body.setBy, msg.body.webcamsOnlyForModerator)
outGW.send(event)
}
}
def buildWebcamsOnlyForModeratorChangedEvtMsg(meetingId: String, userId: String, webcamsOnlyForModerator: Boolean): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId)
val envelope = BbbCoreEnvelope(WebcamsOnlyForModeratorChangedEvtMsg.NAME, routing)
@ -29,4 +36,4 @@ trait UpdateWebcamsOnlyForModeratorCmdMsgHdlr {
BbbCommonEnvCoreMsg(envelope, event)
}
}
}
}

View File

@ -3,6 +3,7 @@ package org.bigbluebutton.core.apps.users
import akka.actor.ActorContext
import akka.event.Logging
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.{ ExternalVideoModel }
import org.bigbluebutton.core.bus.InternalEventBus
import org.bigbluebutton.core.models._
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
@ -53,11 +54,15 @@ object UsersApp {
}
def automaticallyAssignPresenter(outGW: OutMsgRouter, liveMeeting: LiveMeeting): Unit = {
// Stop external video if it's running
ExternalVideoModel.stop(outGW, liveMeeting)
val meetingId = liveMeeting.props.meetingProp.intId
for {
moderator <- Users2x.findModerator(liveMeeting.users2x)
newPresenter <- Users2x.makePresenter(liveMeeting.users2x, moderator.intId)
} yield {
// println(s"automaticallyAssignPresenter: moderator=${moderator} newPresenter=${newPresenter.intId}");
sendPresenterAssigned(outGW, meetingId, newPresenter.intId, newPresenter.name, newPresenter.intId)
}
}
@ -111,6 +116,7 @@ object UsersApp {
sendUserEjectedMessageToClient(outGW, meetingId, userId, ejectedBy, reason, reasonCode)
sendUserLeftMeetingToAllClients(outGW, meetingId, userId)
if (user.presenter) {
// println(s"ejectUserFromMeeting will cause a automaticallyAssignPresenter for user=${user}")
automaticallyAssignPresenter(outGW, liveMeeting)
}
}

View File

@ -6,6 +6,8 @@ case class BreakoutRoom2x(
name: String,
parentId: String,
sequence: Int,
shortName: String,
isDefaultName: Boolean,
freeJoin: Boolean,
voiceConf: String,
assignedUsers: Vector[String],

View File

@ -32,4 +32,5 @@ object MeetingEndReason {
val BREAKOUT_ENDED_EXCEEDING_DURATION = "BREAKOUT_ENDED_EXCEEDING_DURATION"
val BREAKOUT_ENDED_BY_MOD = "BREAKOUT_ENDED_BY_MOD"
val ENDED_DUE_TO_NO_AUTHED_USER = "ENDED_DUE_TO_NO_AUTHED_USER"
val ENDED_DUE_TO_NO_MODERATOR = "ENDED_DUE_TO_NO_MODERATOR"
}

View File

@ -3,14 +3,18 @@ package org.bigbluebutton.core.domain
case class MeetingExpiryTracker(
startedOnInMs: Long,
userHasJoined: Boolean,
moderatorHasJoined: Boolean,
isBreakout: Boolean,
lastUserLeftOnInMs: Option[Long],
lastModeratorLeftOnInMs: Long,
durationInMs: Long,
meetingExpireIfNoUserJoinedInMs: Long,
meetingExpireWhenLastUserLeftInMs: Long,
userInactivityInspectTimerInMs: Long,
userInactivityThresholdInMs: Long,
userActivitySignResponseDelayInMs: Long
userActivitySignResponseDelayInMs: Long,
endWhenNoModerator: Boolean,
endWhenNoModeratorDelayInMs: Long
) {
def setUserHasJoined(): MeetingExpiryTracker = {
if (!userHasJoined) {
@ -24,6 +28,18 @@ case class MeetingExpiryTracker(
copy(lastUserLeftOnInMs = Some(timestampInMs))
}
def setModeratorHasJoined(): MeetingExpiryTracker = {
if (!moderatorHasJoined) {
copy(moderatorHasJoined = true, lastModeratorLeftOnInMs = 0)
} else {
copy(lastModeratorLeftOnInMs = 0)
}
}
def setLastModeratorLeftOn(timestampInMs: Long): MeetingExpiryTracker = {
copy(lastModeratorLeftOnInMs = timestampInMs)
}
def hasMeetingExpiredAfterLastUserLeft(timestampInMs: Long): Boolean = {
val expire = for {
lastUserLeftOn <- lastUserLeftOnInMs

View File

@ -1,5 +1,7 @@
package org.bigbluebutton.core.models
import scala.collection.mutable.Map
object Layouts {
def setCurrentLayout(instance: Layouts, layout: String, setBy: String) {
instance.currentLayout = layout
@ -29,3 +31,12 @@ class Layouts {
// this is not being set by the client, and we need to apply the layouts to all users, not just viewers, so will keep the default value of this as false
private var affectViewersOnly = true
}
object LayoutsType {
val layoutsType = Map(
"custom" -> "CUSTOM_LAYOUT",
"smart" -> "SMART_LAYOUT",
"presentationFocus" -> "PRESENTATION_FOCUS",
"videoFocus" -> "VIDEO_FOCUS"
)
}

View File

@ -11,14 +11,14 @@ import org.bigbluebutton.core.running.LiveMeeting
object Polls {
def handleStartPollReqMsg(state: MeetingState2x, userId: String, pollId: String, pollType: String, question: String,
multiResponse: Boolean, lm: LiveMeeting): Option[SimplePollOutVO] = {
def handleStartPollReqMsg(state: MeetingState2x, userId: String, pollId: String, pollType: String, secretPoll: Boolean, multiResponse: Boolean, questionText: String,
lm: LiveMeeting): Option[SimplePollOutVO] = {
def createPoll(stampedPollId: String): Option[Poll] = {
val numRespondents: Int = Users2x.numUsers(lm.users2x) - 1 // subtract the presenter
for {
poll <- PollFactory.createPoll(stampedPollId, pollType, multiResponse, numRespondents, None)
poll <- PollFactory.createPoll(stampedPollId, pollType, multiResponse, numRespondents, None, Some(questionText), secretPoll)
} yield {
lm.polls.save(poll)
poll
@ -68,7 +68,9 @@ object Polls {
stoppedPoll match {
case None => {
for {
curPoll <- getRunningPollThatStartsWith("public", lm.polls)
// Assuming there's only one running poll at a time, fallback to the
// current running poll without indexing by a presentation page.
curPoll <- getRunningPoll(lm.polls)
} yield {
stopPoll(curPoll.id, lm.polls)
curPoll.id
@ -166,13 +168,13 @@ object Polls {
}
}
def handleStartCustomPollReqMsg(state: MeetingState2x, requesterId: String, pollId: String, pollType: String,
multiResponse: Boolean, answers: Seq[String], question: String, lm: LiveMeeting): Option[SimplePollOutVO] = {
def handleStartCustomPollReqMsg(state: MeetingState2x, requesterId: String, pollId: String, pollType: String, secretPoll: Boolean, multiResponse: Boolean,
answers: Seq[String], questionText: String, lm: LiveMeeting): Option[SimplePollOutVO] = {
def createPoll(stampedPollId: String): Option[Poll] = {
val numRespondents: Int = Users2x.numUsers(lm.users2x) - 1 // subtract the presenter
for {
poll <- PollFactory.createPoll(stampedPollId, pollType, multiResponse, numRespondents, Some(answers))
poll <- PollFactory.createPoll(stampedPollId, pollType, numRespondents, Some(answers), Some(questionText), secretPoll, multiResponse)
} yield {
lm.polls.save(poll)
poll
@ -255,6 +257,7 @@ object Polls {
shape += "numRespondents" -> new Integer(result.numRespondents)
shape += "numResponders" -> new Integer(result.numResponders)
shape += "type" -> WhiteboardKeyUtil.POLL_RESULT_TYPE
shape += "pollType" -> result.questionType
shape += "id" -> result.id
shape += "status" -> WhiteboardKeyUtil.DRAW_END_STATUS
@ -268,7 +271,7 @@ object Polls {
// Limit the number of answers displayed to minimize
// squishing the display.
if (sorted_answers.length < 7) {
if (sorted_answers.length <= 7) {
sorted_answers.foreach(ans => {
answers += SimpleVoteOutVO(ans.id, ans.key, ans.numVotes)
})
@ -306,6 +309,12 @@ object Polls {
shape.toMap
}
def getRunningPoll(polls: Polls): Option[PollVO] = {
for {
poll <- polls.polls.values find { poll => poll.isRunning() }
} yield poll.toPollVO()
}
def getRunningPollThatStartsWith(pollId: String, polls: Polls): Option[PollVO] = {
for {
poll <- polls.polls.values find { poll => poll.id.startsWith(pollId) && poll.isRunning() }
@ -435,7 +444,7 @@ object PollType {
val CustomPollType = "CUSTOM"
val LetterPollType = "A-"
val NumberPollType = "1-"
val ResponsePollType = "RP"
val ResponsePollType = "R-"
}
object PollFactory {
@ -443,35 +452,35 @@ object PollFactory {
val LetterArray = Array("A", "B", "C", "D", "E", "F")
val NumberArray = Array("1", "2", "3", "4", "5", "6")
private def processYesNoPollType(qType: String, multiResponse: Boolean): Question = {
private def processYesNoPollType(qType: String, text: Option[String], multiResponse: Boolean): Question = {
val answers = new ArrayBuffer[Answer];
answers += new Answer(0, "Yes", Some("Yes"))
answers += new Answer(1, "No", Some("No"))
new Question(0, PollType.YesNoPollType, multiResponse, None, answers)
new Question(0, PollType.YesNoPollType, false, text, answers, multiResponse)
}
private def processYesNoAbstentionPollType(qType: String, multiResponse: Boolean): Question = {
private def processYesNoAbstentionPollType(qType: String, text: Option[String], multiResponse: Boolean): Question = {
val answers = new ArrayBuffer[Answer]
answers += new Answer(0, "Yes", Some("Yes"))
answers += new Answer(1, "No", Some("No"))
answers += new Answer(2, "Abstention", Some("Abstention"))
new Question(0, PollType.YesNoAbstentionPollType, multiResponse, None, answers)
new Question(0, PollType.YesNoAbstentionPollType, false, text, answers, multiResponse)
}
private def processTrueFalsePollType(qType: String, multiResponse: Boolean): Question = {
private def processTrueFalsePollType(qType: String, text: Option[String], multiResponse: Boolean): Question = {
val answers = new ArrayBuffer[Answer];
answers += new Answer(0, "True", Some("True"))
answers += new Answer(1, "False", Some("False"))
new Question(0, PollType.TrueFalsePollType, multiResponse, None, answers)
new Question(0, PollType.TrueFalsePollType, false, text, answers, multiResponse)
}
private def processLetterPollType(qType: String, multiResponse: Boolean): Option[Question] = {
private def processLetterPollType(qType: String, multiResponse: Boolean, text: Option[String]): Option[Question] = {
val q = qType.split('-')
val numQs = q(1).toInt
@ -481,7 +490,7 @@ object PollFactory {
val answers = new ArrayBuffer[Answer];
for (i <- 0 until numQs) {
answers += new Answer(i, LetterArray(i), Some(LetterArray(i)))
val question = new Question(0, PollType.LetterPollType, multiResponse, None, answers)
val question = new Question(0, PollType.LetterPollType, multiResponse, text, answers)
questionOption = Some(question)
}
}
@ -489,7 +498,7 @@ object PollFactory {
questionOption
}
private def processNumberPollType(qType: String, multiResponse: Boolean): Option[Question] = {
private def processNumberPollType(qType: String, multiResponse: Boolean, text: Option[String]): Option[Question] = {
val q = qType.split('-')
val numQs = q(1).toInt
@ -499,7 +508,7 @@ object PollFactory {
val answers = new ArrayBuffer[Answer];
for (i <- 0 until numQs) {
answers += new Answer(i, NumberArray(i), Some(NumberArray(i)))
val question = new Question(0, PollType.NumberPollType, multiResponse, None, answers)
val question = new Question(0, PollType.NumberPollType, multiResponse, text, answers)
questionOption = Some(question)
}
}
@ -515,58 +524,58 @@ object PollFactory {
ans
}
private def processCustomPollType(qType: String, multiResponse: Boolean, answers: Option[Seq[String]]): Option[Question] = {
private def processCustomPollType(qType: String, multiResponse: Boolean, text: Option[String], answers: Option[Seq[String]]): Option[Question] = {
var questionOption: Option[Question] = None
answers.foreach { ans =>
val someAnswers = buildAnswers(ans)
val question = new Question(0, PollType.CustomPollType, multiResponse, None, someAnswers)
val question = new Question(0, PollType.CustomPollType, multiResponse, text, someAnswers)
questionOption = Some(question)
}
questionOption
}
private def processResponsePollType(qType: String): Option[Question] = {
private def processResponsePollType(qType: String, text: Option[String]): Option[Question] = {
var questionOption: Option[Question] = None
val answers = new ArrayBuffer[Answer]
val question = new Question(0, PollType.ResponsePollType, false, None, answers)
val question = new Question(0, PollType.ResponsePollType, false, text, answers)
questionOption = Some(question)
questionOption
}
private def createQuestion(qType: String, multiResponse: Boolean, answers: Option[Seq[String]]): Option[Question] = {
private def createQuestion(qType: String, multiResponse: Boolean, answers: Option[Seq[String]], text: Option[String]): Option[Question] = {
val qt = qType.toUpperCase()
var questionOption: Option[Question] = None
if (qt.matches(PollType.YesNoPollType)) {
questionOption = Some(processYesNoPollType(qt, multiResponse))
questionOption = Some(processYesNoPollType(qt, text, multiResponse))
} else if (qt.matches(PollType.YesNoAbstentionPollType)) {
questionOption = Some(processYesNoAbstentionPollType(qt, multiResponse))
questionOption = Some(processYesNoAbstentionPollType(qt, text, multiResponse))
} else if (qt.matches(PollType.TrueFalsePollType)) {
questionOption = Some(processTrueFalsePollType(qt, multiResponse))
questionOption = Some(processTrueFalsePollType(qt, text, multiResponse))
} else if (qt.matches(PollType.CustomPollType)) {
questionOption = processCustomPollType(qt, multiResponse, answers)
questionOption = processCustomPollType(qt, false, text, answers, multiResponse)
} else if (qt.startsWith(PollType.LetterPollType)) {
questionOption = processLetterPollType(qt, multiResponse)
questionOption = processLetterPollType(qt, false, text, multiResponse)
} else if (qt.startsWith(PollType.NumberPollType)) {
questionOption = processNumberPollType(qt, multiResponse)
questionOption = processNumberPollType(qt, false, text, multiResponse)
} else if (qt.startsWith(PollType.ResponsePollType)) {
questionOption = processResponsePollType(qt)
questionOption = processResponsePollType(qt, text, multiResponse)
}
questionOption
}
def createPoll(id: String, pollType: String, multiResponse: Boolean, numRespondents: Int, answers: Option[Seq[String]]): Option[Poll] = {
def createPoll(id: String, pollType: String, multiResponse: Boolean, numRespondents: Int, answers: Option[Seq[String]], questionText: Option[String], isSecret: Boolean): Option[Poll] = {
var poll: Option[Poll] = None
createQuestion(pollType, multiResponse, answers) match {
createQuestion(pollType, multiResponse, answers, questionText) match {
case Some(question) => {
poll = Some(new Poll(id, Array(question), numRespondents, None))
poll = Some(new Poll(id, Array(question), numRespondents, None, isSecret))
}
case None => poll = None
}
@ -582,7 +591,7 @@ case class ResponderVO(responseID: String, user: Responder)
case class ResponseOutVO(id: String, text: String, responders: Array[Responder] = Array[Responder]())
case class QuestionOutVO(id: String, multiResponse: Boolean, question: String, responses: Array[ResponseOutVO])
class Poll(val id: String, val questions: Array[Question], val numRespondents: Int, val title: Option[String]) {
class Poll(val id: String, val questions: Array[Question], val numRespondents: Int, val title: Option[String], val isSecret: Boolean) {
private var _started: Boolean = false
private var _stopped: Boolean = false
private var _showResult: Boolean = false
@ -636,7 +645,7 @@ class Poll(val id: String, val questions: Array[Question], val numRespondents: I
qvos += q.toQuestionVO
})
new PollVO(id, qvos.toArray, title, _started, _stopped, _showResult)
new PollVO(id, qvos.toArray, title, _started, _stopped, _showResult, isSecret)
}
def toSimplePollOutVO(): SimplePollOutVO = {
@ -644,7 +653,7 @@ class Poll(val id: String, val questions: Array[Question], val numRespondents: I
}
def toSimplePollResultOutVO(): SimplePollResultOutVO = {
new SimplePollResultOutVO(id, questions(0).toSimpleVotesOutVO(), numRespondents, _numResponders)
new SimplePollResultOutVO(id, questions(0).questionType, questions(0).text, questions(0).toSimpleVotesOutVO(), numRespondents, _numResponders)
}
}

View File

@ -8,6 +8,14 @@ object Users2x {
users.toVector find (u => u.intId == intId)
}
def findWithBreakoutRoomId(users: Users2x, breakoutRoomId: String): Option[UserState] = {
//userId + "-" + roomSequence
val userIdParts = breakoutRoomId.split("-")
val userExtId = userIdParts(0)
users.toVector find (u => u.extId == userExtId)
}
def findAll(users: Users2x): Vector[UserState] = users.toVector
def add(users: Users2x, user: UserState): Option[UserState] = {
@ -55,6 +63,10 @@ object Users2x {
users.toVector.length
}
def numActiveModerators(users: Users2x): Int = {
users.toVector.filter(u => u.role == Roles.MODERATOR_ROLE && !u.userLeftFlag.left).length
}
def findNotPresenters(users: Users2x): Vector[UserState] = {
users.toVector.filter(u => !u.presenter)
}
@ -68,7 +80,13 @@ object Users2x {
}
def updateLastUserActivity(users: Users2x, u: UserState): UserState = {
val newUserState = modify(u)(_.lastActivityTime).setTo(TimeUtil.timeNowInMs())
val newUserState = modify(u)(_.lastActivityTime).setTo(System.currentTimeMillis())
users.save(newUserState)
newUserState
}
def updateLastInactivityInspect(users: Users2x, u: UserState): UserState = {
val newUserState = modify(u)(_.lastInactivityInspect).setTo(System.currentTimeMillis())
users.save(newUserState)
newUserState
}
@ -257,21 +275,22 @@ case class OldPresenter(userId: String, changedPresenterOn: Long)
case class UserLeftFlag(left: Boolean, leftOn: Long)
case class UserState(
intId: String,
extId: String,
name: String,
role: String,
guest: Boolean,
authed: Boolean,
guestStatus: String,
emoji: String,
locked: Boolean,
presenter: Boolean,
avatar: String,
roleChangedOn: Long = System.currentTimeMillis(),
lastActivityTime: Long = TimeUtil.timeNowInMs(),
clientType: String,
userLeftFlag: UserLeftFlag
intId: String,
extId: String,
name: String,
role: String,
guest: Boolean,
authed: Boolean,
guestStatus: String,
emoji: String,
locked: Boolean,
presenter: Boolean,
avatar: String,
roleChangedOn: Long = System.currentTimeMillis(),
lastActivityTime: Long = System.currentTimeMillis(),
lastInactivityInspect: Long = 0,
clientType: String,
userLeftFlag: UserLeftFlag
)
case class UserIdAndName(id: String, name: String)

View File

@ -14,6 +14,7 @@ object VoiceUsers {
def findAll(users: VoiceUsers): Vector[VoiceUserState] = users.toVector
def findAllNonListenOnlyVoiceUsers(users: VoiceUsers): Vector[VoiceUserState] = users.toVector.filter(u => u.listenOnly == false)
def findAllListenOnlyVoiceUsers(users: VoiceUsers): Vector[VoiceUserState] = users.toVector.filter(u => u.listenOnly == true)
def findAllFreeswitchCallers(users: VoiceUsers): Vector[VoiceUserState] = users.toVector.filter(u => u.calledInto == "freeswitch")
def findAllKurentoCallers(users: VoiceUsers): Vector[VoiceUserState] = users.toVector.filter(u => u.calledInto == "kms")

View File

@ -30,6 +30,21 @@ object Webcams {
removedStream <- webcams.remove(streamId)
} yield removedStream
}
def updateWebcamStream(webcams: Webcams, streamId: String, userId: String): Option[WebcamStream] = {
findWithStreamId(webcams, streamId) match {
case Some(value) => {
val mediaStream: MediaStream = MediaStream(value.stream.id, value.stream.url, userId, value.stream.attributes,
value.stream.viewers)
val webcamStream: WebcamStream = WebcamStream(streamId, mediaStream)
webcams.update(streamId, webcamStream)
Some(webcamStream)
}
case None => {
None
}
}
}
}
class Webcams {
@ -47,6 +62,12 @@ class Webcams {
webcam foreach (u => webcams -= streamId)
webcam
}
private def update(streamId: String, webcamStream: WebcamStream): WebcamStream = {
val webcam = remove(streamId)
save(webcamStream)
}
}
case class WebcamStream(streamId: String, stream: MediaStream)

View File

@ -189,6 +189,8 @@ class ReceivedJsonMsgHandlerActor(
routeGenericMsg[EndAllBreakoutRoomsMsg](envelope, jsonNode)
case TransferUserToMeetingRequestMsg.NAME =>
routeGenericMsg[TransferUserToMeetingRequestMsg](envelope, jsonNode)
case ExtendBreakoutRoomsTimeReqMsg.NAME =>
routeGenericMsg[ExtendBreakoutRoomsTimeReqMsg](envelope, jsonNode)
// Layout
case GetCurrentLayoutReqMsg.NAME =>

View File

@ -0,0 +1,53 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2017 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.core.record.events
import org.bigbluebutton.common2.domain.SimpleVoteOutVO
import org.bigbluebutton.common2.util.JsonUtil
class PollPublishedRecordEvent extends AbstractPollRecordEvent {
import PollPublishedRecordEvent._
setEvent("PollPublishedRecordEvent")
def setQuestion(question: String) {
eventMap.put(QUESTION, question)
}
def setAnswers(answers: Array[SimpleVoteOutVO]) {
eventMap.put(ANSWERS, JsonUtil.toJson(answers))
}
def setNumRespondents(numRespondents: Int) {
eventMap.put(NUM_RESPONDENTS, Integer.toString(numRespondents))
}
def setNumResponders(numResponders: Int) {
eventMap.put(NUM_RESPONDERS, Integer.toString(numResponders))
}
}
object PollPublishedRecordEvent {
protected final val USER_ID = "userId"
protected final val QUESTION = "question"
protected final val ANSWERS = "answers"
protected final val NUM_RESPONDENTS = "numRespondents"
protected final val NUM_RESPONDERS = "numResponders"
}

View File

@ -31,12 +31,27 @@ class PollStartedRecordEvent extends AbstractPollRecordEvent {
eventMap.put(USER_ID, userId)
}
def setQuestion(question: String) {
eventMap.put(QUESTION, question)
}
def setAnswers(answers: Array[SimpleAnswerOutVO]) {
eventMap.put(ANSWERS, JsonUtil.toJson(answers))
}
def setType(pollType: String) {
eventMap.put(TYPE, pollType)
}
def setSecretPoll(secretPoll: Boolean) {
eventMap.put(SECRET_POLL, secretPoll.toString)
}
}
object PollStartedRecordEvent {
protected final val USER_ID = "userId"
protected final val QUESTION = "question"
protected final val ANSWERS = "answers"
protected final val TYPE = "type"
protected final val SECRET_POLL = "secretPoll"
}

View File

@ -36,7 +36,7 @@ class UpdateExternalVideoRecordEvent extends AbstractExternalVideoRecordEvent {
eventMap.put(TIME, time.toString)
}
def setState(state: Boolean) {
def setState(state: Int) {
eventMap.put(STATE, state.toString)
}
}

View File

@ -33,9 +33,14 @@ class UserRespondedToPollRecordEvent extends AbstractPollRecordEvent {
def setAnswerId(answerIds: Array[Int]) {
eventMap.put(ANSWER_IDS, JsonUtil.toJson(answerIds))
}
def setAnswer(answer: String) {
eventMap.put(ANSWER, answer)
}
}
object UserRespondedToPollRecordEvent {
protected final val USER_ID = "userId"
protected final val ANSWER_IDS = "answerIds"
protected final val ANSWER = "answer"
}

View File

@ -35,5 +35,5 @@ class WebcamsOnlyForModeratorRecordEvent extends AbstractParticipantRecordEvent
object WebcamsOnlyForModeratorRecordEvent {
protected final val USER_ID = "userId"
protected final val WEBCAMS_ONLY_FOR_MODERATOR = "webacmsOnlyForModerator"
protected final val WEBCAMS_ONLY_FOR_MODERATOR = "webcamsOnlyForModerator"
}

View File

@ -84,6 +84,7 @@ trait HandlerHelpers extends SystemConfiguration {
outGW.send(event)
val newState = startRecordingIfAutoStart2x(outGW, liveMeeting, state)
if (!Users2x.hasPresenter(liveMeeting.users2x)) {
// println(s"userJoinMeeting will trigger an automaticallyAssignPresenter for user=${newUser}")
UsersApp.automaticallyAssignPresenter(outGW, liveMeeting)
}
newState.update(newState.expiryTracker.setUserHasJoined())

View File

@ -6,18 +6,19 @@ import org.bigbluebutton.core.models._
import org.bigbluebutton.core2.MeetingStatus2x
class LiveMeeting(
val props: DefaultProps,
val status: MeetingStatus2x,
val screenshareModel: ScreenshareModel,
val chatModel: ChatModel,
val layouts: Layouts,
val registeredUsers: RegisteredUsers,
val polls: Polls, // 2x
val wbModel: WhiteboardModel,
val presModel: PresentationModel,
val captionModel: CaptionModel,
val webcams: Webcams,
val voiceUsers: VoiceUsers,
val users2x: Users2x,
val guestsWaiting: GuestsWaiting
val props: DefaultProps,
val status: MeetingStatus2x,
val screenshareModel: ScreenshareModel,
val chatModel: ChatModel,
val externalVideoModel: ExternalVideoModel,
val layouts: Layouts,
val registeredUsers: RegisteredUsers,
val polls: Polls, // 2x
val wbModel: WhiteboardModel,
val presModel: PresentationModel,
val captionModel: CaptionModel,
val webcams: Webcams,
val voiceUsers: VoiceUsers,
val users2x: Users2x,
val guestsWaiting: GuestsWaiting
)

View File

@ -1,7 +1,6 @@
package org.bigbluebutton.core.running
import java.io.{ PrintWriter, StringWriter }
import akka.actor._
import akka.actor.SupervisorStrategy.Resume
import org.bigbluebutton.SystemConfiguration
@ -22,7 +21,7 @@ import org.bigbluebutton.core.apps.presentation.PresentationApp2x
import org.bigbluebutton.core.apps.users.UsersApp2x
import org.bigbluebutton.core.apps.whiteboard.WhiteboardApp2x
import org.bigbluebutton.core.bus._
import org.bigbluebutton.core.models._
import org.bigbluebutton.core.models.{ Users2x, VoiceUsers, _ }
import org.bigbluebutton.core2.{ MeetingStatus2x, Permissions }
import org.bigbluebutton.core2.message.handlers._
import org.bigbluebutton.core2.message.handlers.meeting._
@ -31,15 +30,21 @@ import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.breakout._
import org.bigbluebutton.core.apps.polls._
import org.bigbluebutton.core.apps.voice._
import akka.actor._
import akka.actor.Props
import akka.actor.OneForOneStrategy
import akka.actor.SupervisorStrategy.Resume
import org.bigbluebutton.common2.msgs
import scala.concurrent.duration._
import org.bigbluebutton.core.apps.layout.LayoutApp2x
import org.bigbluebutton.core.apps.meeting.{ SyncGetMeetingInfoRespMsgHdlr, ValidateConnAuthTokenSysMsgHdlr }
import org.bigbluebutton.core.apps.users.ChangeLockSettingsInMeetingCmdMsgHdlr
import org.bigbluebutton.core.models.VoiceUsers.{ findAllFreeswitchCallers, findAllListenOnlyVoiceUsers }
import org.bigbluebutton.core.models.Webcams.{ findAll, updateWebcamStream }
import org.bigbluebutton.core2.MeetingStatus2x.{ hasAuthedUserJoined, isVoiceRecording }
import org.bigbluebutton.core2.message.senders.{ MsgBuilder, Sender }
import java.util.concurrent.TimeUnit
import scala.concurrent.ExecutionContext.Implicits.global
object MeetingActor {
@ -97,6 +102,8 @@ class MeetingActor(
object CheckVoiceRecordingInternalMsg
object SyncVoiceUserStatusInternalMsg
object MeetingInfoAnalyticsMsg
object MeetingInfoAnalyticsLogMsg
override val supervisorStrategy = OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) {
case e: Exception => {
@ -135,14 +142,18 @@ class MeetingActor(
val expiryTracker = new MeetingExpiryTracker(
startedOnInMs = TimeUtil.timeNowInMs(),
userHasJoined = false,
moderatorHasJoined = false,
isBreakout = props.meetingProp.isBreakout,
lastUserLeftOnInMs = None,
lastModeratorLeftOnInMs = 0,
durationInMs = TimeUtil.minutesToMillis(props.durationProps.duration),
meetingExpireIfNoUserJoinedInMs = TimeUtil.minutesToMillis(props.durationProps.meetingExpireIfNoUserJoinedInMinutes),
meetingExpireWhenLastUserLeftInMs = TimeUtil.minutesToMillis(props.durationProps.meetingExpireWhenLastUserLeftInMinutes),
userInactivityInspectTimerInMs = TimeUtil.minutesToMillis(props.durationProps.userInactivityInspectTimerInMinutes),
userInactivityThresholdInMs = TimeUtil.minutesToMillis(props.durationProps.userInactivityThresholdInMinutes),
userActivitySignResponseDelayInMs = TimeUtil.minutesToMillis(props.durationProps.userActivitySignResponseDelayInMinutes)
userActivitySignResponseDelayInMs = TimeUtil.minutesToMillis(props.durationProps.userActivitySignResponseDelayInMinutes),
endWhenNoModerator = props.durationProps.endWhenNoModerator,
endWhenNoModeratorDelayInMs = TimeUtil.minutesToMillis(props.durationProps.endWhenNoModeratorDelayInMinutes)
)
val recordingTracker = new MeetingRecordingTracker(startedOnInMs = 0L, previousDurationInMs = 0L, currentDurationInMs = 0L)
@ -208,12 +219,32 @@ class MeetingActor(
CheckVoiceRecordingInternalMsg
)
context.system.scheduler.scheduleOnce(
10 seconds,
self,
MeetingInfoAnalyticsLogMsg
)
context.system.scheduler.schedule(
10 seconds,
30 seconds,
self,
MeetingInfoAnalyticsMsg
)
def receive = {
case SyncVoiceUserStatusInternalMsg =>
checkVoiceConfUsersStatus()
case CheckVoiceRecordingInternalMsg =>
checkVoiceConfIsRunningAndRecording()
case MeetingInfoAnalyticsLogMsg =>
handleMeetingInfoAnalyticsLogging()
case MeetingInfoAnalyticsMsg =>
handleMeetingInfoAnalyticsService()
case msg: CamStreamSubscribeSysMsg =>
handleCamStreamSubscribeSysMsg(msg)
case msg: ScreenStreamSubscribeSysMsg =>
handleScreenStreamSubscribeSysMsg(msg)
//=============================
// 2x messages
@ -250,6 +281,7 @@ class MeetingActor(
case msg: SendBreakoutUsersAuditInternalMsg => handleSendBreakoutUsersUpdateInternalMsg(msg)
case msg: BreakoutRoomUsersUpdateInternalMsg => state = handleBreakoutRoomUsersUpdateInternalMsg(msg, state)
case msg: EndBreakoutRoomInternalMsg => handleEndBreakoutRoomInternalMsg(msg)
case msg: ExtendBreakoutRoomTimeInternalMsg => state = handleExtendBreakoutRoomTimeInternalMsgHdlr(msg, state)
case msg: BreakoutRoomEndedInternalMsg => state = handleBreakoutRoomEndedInternalMsg(msg, state)
case msg: SendBreakoutTimeRemainingInternalMsg =>
handleSendBreakoutTimeRemainingInternalMsg(msg)
@ -298,6 +330,31 @@ class MeetingActor(
}
}
private def updateModeratorsPresence() {
if (Users2x.numActiveModerators(liveMeeting.users2x) > 0) {
if (state.expiryTracker.moderatorHasJoined == false ||
state.expiryTracker.lastModeratorLeftOnInMs != 0) {
log.info("A moderator has joined. Setting setModeratorHasJoined(). meetingId=" + props.meetingProp.intId)
val tracker = state.expiryTracker.setModeratorHasJoined()
state = state.update(tracker)
}
} else {
if (state.expiryTracker.moderatorHasJoined == true) {
log.info("All moderators have left. Setting setLastModeratorLeftOn(). meetingId=" + props.meetingProp.intId)
val tracker = state.expiryTracker.setLastModeratorLeftOn(TimeUtil.timeNowInMs())
state = state.update(tracker)
}
}
}
private def updateUserLastInactivityInspect(userId: String) {
for {
user <- Users2x.findWithIntId(liveMeeting.users2x, userId)
} yield {
Users2x.updateLastInactivityInspect(liveMeeting.users2x, user)
}
}
private def handleBbbCommonEnvCoreMsg(msg: BbbCommonEnvCoreMsg): Unit = {
msg.core match {
case m: ClientToServerLatencyTracerMsg => handleClientToServerLatencyTracerMsg(m)
@ -309,20 +366,26 @@ class MeetingActor(
private def handleMessageThatAffectsInactivity(msg: BbbCommonEnvCoreMsg): Unit = {
msg.core match {
case m: EndMeetingSysCmdMsg => handleEndMeeting(m, state)
case m: EndMeetingSysCmdMsg => handleEndMeeting(m, state)
// Users
case m: ValidateAuthTokenReqMsg => state = usersApp.handleValidateAuthTokenReqMsg(m, state)
case m: UserJoinMeetingReqMsg => state = handleUserJoinMeetingReqMsg(m, state)
case m: UserJoinMeetingAfterReconnectReqMsg => state = handleUserJoinMeetingAfterReconnectReqMsg(m, state)
case m: UserLeaveReqMsg => state = handleUserLeaveReqMsg(m, state)
case m: UserBroadcastCamStartMsg => handleUserBroadcastCamStartMsg(m)
case m: UserBroadcastCamStopMsg => handleUserBroadcastCamStopMsg(m)
case m: GetCamBroadcastPermissionReqMsg => handleGetCamBroadcastPermissionReqMsg(m)
case m: GetCamSubscribePermissionReqMsg => handleGetCamSubscribePermissionReqMsg(m)
case m: ValidateAuthTokenReqMsg => state = usersApp.handleValidateAuthTokenReqMsg(m, state)
case m: UserJoinMeetingReqMsg =>
state = handleUserJoinMeetingReqMsg(m, state)
updateModeratorsPresence()
case m: UserJoinMeetingAfterReconnectReqMsg =>
state = handleUserJoinMeetingAfterReconnectReqMsg(m, state)
updateModeratorsPresence()
case m: UserLeaveReqMsg =>
state = handleUserLeaveReqMsg(m, state)
updateModeratorsPresence()
case m: UserBroadcastCamStartMsg => handleUserBroadcastCamStartMsg(m)
case m: UserBroadcastCamStopMsg => handleUserBroadcastCamStopMsg(m)
case m: GetCamBroadcastPermissionReqMsg => handleGetCamBroadcastPermissionReqMsg(m)
case m: GetCamSubscribePermissionReqMsg => handleGetCamSubscribePermissionReqMsg(m)
case m: UserJoinedVoiceConfEvtMsg => handleUserJoinedVoiceConfEvtMsg(m)
case m: LogoutAndEndMeetingCmdMsg => usersApp.handleLogoutAndEndMeetingCmdMsg(m, state)
case m: UserJoinedVoiceConfEvtMsg => handleUserJoinedVoiceConfEvtMsg(m)
case m: LogoutAndEndMeetingCmdMsg => usersApp.handleLogoutAndEndMeetingCmdMsg(m, state)
case m: SetRecordingStatusCmdMsg =>
state = usersApp.handleSetRecordingStatusCmdMsg(m, state)
updateUserLastActivity(m.body.setBy)
@ -346,6 +409,7 @@ class MeetingActor(
case m: ChangeUserRoleCmdMsg =>
usersApp.handleChangeUserRoleCmdMsg(m)
updateUserLastActivity(m.body.changedBy)
updateModeratorsPresence()
// Whiteboard
case m: SendCursorPositionPubMsg => wbApp.handle(m, liveMeeting, msgBus)
@ -382,6 +446,7 @@ class MeetingActor(
case m: EndAllBreakoutRoomsMsg => state = handleEndAllBreakoutRoomsMsg(m, state)
case m: RequestBreakoutJoinURLReqMsg => state = handleRequestBreakoutJoinURLReqMsg(m, state)
case m: TransferUserToMeetingRequestMsg => state = handleTransferUserToMeetingRequestMsg(m, state)
case m: ExtendBreakoutRoomsTimeReqMsg => state = handleExtendBreakoutRoomsTimeMsg(m, state)
// Voice
case m: UserLeftVoiceConfEvtMsg => handleUserLeftVoiceConfEvtMsg(m)
@ -508,6 +573,98 @@ class MeetingActor(
}
}
private def handleCamStreamSubscribeSysMsg(msg: CamStreamSubscribeSysMsg): Unit = {
updateWebcamStream(liveMeeting.webcams, msg.body.streamId, msg.body.userId)
}
private def handleScreenStreamSubscribeSysMsg(msg: ScreenStreamSubscribeSysMsg): Unit = ???
private def handleMeetingInfoAnalyticsLogging(): Unit = {
val meetingInfoAnalyticsLogMsg: MeetingInfoAnalytics = prepareMeetingInfo()
val event = MsgBuilder.buildMeetingInfoAnalyticsMsg(meetingInfoAnalyticsLogMsg)
outGW.send(event)
}
private def handleMeetingInfoAnalyticsService(): Unit = {
val meetingInfoAnalyticsLogMsg: MeetingInfoAnalytics = prepareMeetingInfo()
val event2 = MsgBuilder.buildMeetingInfoAnalyticsServiceMsg(meetingInfoAnalyticsLogMsg)
outGW.send(event2)
}
private def prepareMeetingInfo(): MeetingInfoAnalytics = {
val meetingName: String = liveMeeting.props.meetingProp.name
val externalId: String = liveMeeting.props.meetingProp.extId
val internalId: String = liveMeeting.props.meetingProp.intId
val hasUserJoined: Boolean = hasAuthedUserJoined(liveMeeting.status)
val isMeetingRecorded = MeetingStatus2x.isRecording(liveMeeting.status)
// TODO: Placeholder values as required values not available
val screenshareStream: ScreenshareStream = ScreenshareStream(new User("", ""), List())
val screenshare: Screenshare = Screenshare(screenshareStream)
val listOfUsers: List[UserState] = Users2x.findAll(liveMeeting.users2x).toList
val breakoutRoomNames: List[String] = {
if (state.breakout.isDefined)
state.breakout.get.getRooms.map(_.name).toList
else
List()
}
val breakoutRoom: BreakoutRoom = BreakoutRoom(liveMeeting.props.breakoutProps.parentId, breakoutRoomNames)
MeetingInfoAnalytics(
meetingName, externalId, internalId, hasUserJoined, isMeetingRecorded, getMeetingInfoWebcamDetails, getMeetingInfoAudioDetails,
screenshare, listOfUsers.map(u => Participant(u.intId, u.name, u.role)), getMeetingInfoPresentationDetails, breakoutRoom
)
}
private def resolveUserName(userId: String): String = {
val userName: String = Users2x.findWithIntId(liveMeeting.users2x, userId).map(_.name).getOrElse("")
if (userName.isEmpty) log.error(s"Failed to map username for id $userId")
userName
}
private def getMeetingInfoWebcamDetails(): Webcam = {
val liveWebcams: Vector[org.bigbluebutton.core.models.WebcamStream] = findAll(liveMeeting.webcams)
val numOfLiveWebcams: Int = liveWebcams.length
val broadcasts: List[Broadcast] = liveWebcams.map(webcam => Broadcast(
webcam.stream.id,
User(webcam.stream.userId, resolveUserName(webcam.stream.userId)), 0L
)).toList
val viewers: Set[String] = liveWebcams.flatMap(_.stream.viewers).toSet
val webcamStream: msgs.WebcamStream = msgs.WebcamStream(broadcasts, viewers)
Webcam(numOfLiveWebcams, webcamStream)
}
private def getMeetingInfoAudioDetails(): Audio = {
val voiceUsers: Vector[VoiceUserState] = VoiceUsers.findAll(liveMeeting.voiceUsers)
val numOfVoiceUsers: Int = voiceUsers.length
val listenOnlyUsers: Vector[VoiceUserState] = findAllListenOnlyVoiceUsers(liveMeeting.voiceUsers)
val numOfListenOnlyUsers: Int = listenOnlyUsers.length
val listenOnlyAudio = ListenOnlyAudio(
numOfListenOnlyUsers,
listenOnlyUsers.map(voiceUserState => User(voiceUserState.voiceUserId, resolveUserName(voiceUserState.intId))).toList
)
val freeswitchUsers: Vector[VoiceUserState] = findAllFreeswitchCallers(liveMeeting.voiceUsers)
val numOfFreeswitchUsers: Int = freeswitchUsers.length
val twoWayAudio = TwoWayAudio(
numOfFreeswitchUsers,
freeswitchUsers.map(voiceUserState => User(voiceUserState.voiceUserId, resolveUserName(voiceUserState.intId))).toList
)
// TODO: Placeholder values
val phoneAudio = PhoneAudio(0, List())
Audio(numOfVoiceUsers, listenOnlyAudio, twoWayAudio, phoneAudio)
}
private def getMeetingInfoPresentationDetails(): PresentationInfo = {
val presentationPods: Vector[PresentationPod] = state.presentationPodManager.getAllPresentationPodsInMeeting()
val presentationId: String = presentationPods.flatMap(_.getCurrentPresentation.map(_.id)).mkString
val presentationName: String = presentationPods.flatMap(_.getCurrentPresentation.map(_.name)).mkString
PresentationInfo(presentationId, presentationName)
}
def handleGetRunningMeetingStateReqMsg(msg: GetRunningMeetingStateReqMsg): Unit = {
processGetRunningMeetingStateReqMsg()
}
@ -558,7 +715,6 @@ class MeetingActor(
}
def handleDeskShareGetDeskShareInfoRequest(msg: DeskShareGetDeskShareInfoRequest): Unit = {
log.info("handleDeskShareGetDeskShareInfoRequest: " + msg.conferenceName + "isBroadcasting="
+ ScreenshareModel.isBroadcastingRTMP(liveMeeting.screenshareModel) + " URL:" +
ScreenshareModel.getRTMPBroadcastingUrl(liveMeeting.screenshareModel))
@ -588,7 +744,8 @@ class MeetingActor(
processUserInactivityAudit()
flagRegisteredUsersWhoHasNotJoined()
checkIfNeetToEndMeetingWhenNoAuthedUsers(liveMeeting)
checkIfNeedToEndMeetingWhenNoAuthedUsers(liveMeeting)
checkIfNeedToEndMeetingWhenNoModerators(liveMeeting)
}
def checkVoiceConfUsersStatus(): Unit = {
@ -610,7 +767,7 @@ class MeetingActor(
var lastRecBreakSentOn = expiryTracker.startedOnInMs
def setRecordingChapterBreak(): Unit = {
val now = TimeUtil.timeNowInMs()
val now = System.currentTimeMillis()
val elapsedInMs = now - lastRecBreakSentOn
val elapsedInMin = TimeUtil.millisToMinutes(elapsedInMs)
@ -655,7 +812,7 @@ class MeetingActor(
}
private def checkIfNeetToEndMeetingWhenNoAuthedUsers(liveMeeting: LiveMeeting): Unit = {
private def checkIfNeedToEndMeetingWhenNoAuthedUsers(liveMeeting: LiveMeeting): Unit = {
val authUserJoined = MeetingStatus2x.hasAuthedUserJoined(liveMeeting.status)
if (endMeetingWhenNoMoreAuthedUsers &&
@ -676,10 +833,30 @@ class MeetingActor(
}
}
def handleExtendMeetingDuration(msg: ExtendMeetingDuration) {
private def checkIfNeedToEndMeetingWhenNoModerators(liveMeeting: LiveMeeting): Unit = {
if (state.expiryTracker.endWhenNoModerator &&
!liveMeeting.props.meetingProp.isBreakout &&
state.expiryTracker.moderatorHasJoined &&
state.expiryTracker.lastModeratorLeftOnInMs != 0 &&
//Check if has moderator with leftFlag
Users2x.findModerator(liveMeeting.users2x).toVector.length == 0) {
val hasModeratorLeftRecently = (TimeUtil.timeNowInMs() - state.expiryTracker.endWhenNoModeratorDelayInMs) < state.expiryTracker.lastModeratorLeftOnInMs
if (!hasModeratorLeftRecently) {
log.info("Meeting will end due option endWhenNoModerator is enabled and all moderators have left the meeting. meetingId=" + props.meetingProp.intId)
sendEndMeetingDueToExpiry(
MeetingEndReason.ENDED_DUE_TO_NO_MODERATOR,
eventBus, outGW, liveMeeting,
"system"
)
} else {
val msToEndMeeting = state.expiryTracker.lastModeratorLeftOnInMs - (TimeUtil.timeNowInMs() - state.expiryTracker.endWhenNoModeratorDelayInMs)
log.info("All moderators have left. Meeting will end in " + TimeUtil.millisToSeconds(msToEndMeeting) + " seconds. meetingId=" + props.meetingProp.intId)
}
}
}
def handleExtendMeetingDuration(msg: ExtendMeetingDuration) = ???
def removeUsersWithExpiredUserLeftFlag(liveMeeting: LiveMeeting, state: MeetingState2x): MeetingState2x = {
val leftUsers = Users2x.findAllExpiredUserLeftFlags(liveMeeting.users2x, expiryTracker.meetingExpireWhenLastUserLeftInMs)
leftUsers foreach { leftUser =>
@ -695,6 +872,7 @@ class MeetingActor(
outGW.send(userLeftMeetingEvent)
if (u.presenter) {
log.info("removeUsersWithExpiredUserLeftFlag will cause an automaticallyAssignPresenter because user={} left", u)
UsersApp.automaticallyAssignPresenter(outGW, liveMeeting)
// request screenshare to end
@ -723,34 +901,37 @@ class MeetingActor(
}
}
var lastUserInactivityInspectSentOn = TimeUtil.timeNowInMs()
var checkInactiveUsers = false
var lastUsersInactivityInspection = System.currentTimeMillis()
def processUserInactivityAudit(): Unit = {
val now = TimeUtil.timeNowInMs()
val now = System.currentTimeMillis()
// Check if user is inactive. We only do the check is user inactivity
// is not disabled (0).
if ((expiryTracker.userInactivityInspectTimerInMs > 0) &&
(now > lastUserInactivityInspectSentOn + expiryTracker.userInactivityInspectTimerInMs)) {
lastUserInactivityInspectSentOn = now
checkInactiveUsers = true
warnPotentiallyInactiveUsers()
}
(now > lastUsersInactivityInspection + expiryTracker.userInactivityInspectTimerInMs)) {
lastUsersInactivityInspection = now
if (checkInactiveUsers && now > lastUserInactivityInspectSentOn + expiryTracker.userActivitySignResponseDelayInMs) {
checkInactiveUsers = false
warnPotentiallyInactiveUsers()
disconnectInactiveUsers()
}
}
def warnPotentiallyInactiveUsers(): Unit = {
log.info("Checking for inactive users.")
val users = Users2x.findAll(liveMeeting.users2x)
users foreach { u =>
val active = (lastUserInactivityInspectSentOn - expiryTracker.userInactivityThresholdInMs) < u.lastActivityTime
if (!active) {
Sender.sendUserInactivityInspectMsg(liveMeeting.props.meetingProp.intId, u.intId, TimeUtil.minutesToSeconds(props.durationProps.userActivitySignResponseDelayInMinutes), outGW)
val hasActivityAfterWarning = u.lastInactivityInspect < u.lastActivityTime
val hasActivityRecently = (lastUsersInactivityInspection - expiryTracker.userInactivityThresholdInMs) < u.lastActivityTime
if (hasActivityAfterWarning && !hasActivityRecently) {
log.info("User has been inactive for " + TimeUnit.MILLISECONDS.toMinutes(expiryTracker.userInactivityThresholdInMs) + " minutes. Sending inactivity warning. meetingId=" + props.meetingProp.intId + " userId=" + u.intId + " user=" + u)
val secsToDisconnect = TimeUnit.MILLISECONDS.toSeconds(expiryTracker.userActivitySignResponseDelayInMs);
Sender.sendUserInactivityInspectMsg(liveMeeting.props.meetingProp.intId, u.intId, secsToDisconnect, outGW)
updateUserLastInactivityInspect(u.intId)
}
}
}
@ -759,8 +940,13 @@ class MeetingActor(
log.info("Check for users who haven't responded to user inactivity warning.")
val users = Users2x.findAll(liveMeeting.users2x)
users foreach { u =>
val respondedOnTime = (lastUserInactivityInspectSentOn - expiryTracker.userInactivityThresholdInMs) < u.lastActivityTime && (lastUserInactivityInspectSentOn + expiryTracker.userActivitySignResponseDelayInMs) > u.lastActivityTime
if (!respondedOnTime) {
val hasInactivityWarningSent = u.lastInactivityInspect != 0
val hasActivityAfterWarning = u.lastInactivityInspect < u.lastActivityTime
val respondedOnTime = (lastUsersInactivityInspection - expiryTracker.userActivitySignResponseDelayInMs) < u.lastInactivityInspect
if (hasInactivityWarningSent && !hasActivityAfterWarning && !respondedOnTime) {
log.info("User didn't response the inactivity warning within " + TimeUnit.MILLISECONDS.toSeconds(expiryTracker.userActivitySignResponseDelayInMs) + " seconds. Ejecting from meeting. meetingId=" + props.meetingProp.intId + " userId=" + u.intId + " user=" + u)
UsersApp.ejectUserFromMeeting(
outGW,
liveMeeting,

View File

@ -17,6 +17,7 @@ object RunningMeeting {
class RunningMeeting(val props: DefaultProps, outGW: OutMessageGateway,
eventBus: InternalEventBus)(implicit val context: ActorContext) {
private val externalVideoModel = new ExternalVideoModel()
private val chatModel = new ChatModel()
private val layouts = new Layouts()
private val wbModel = new WhiteboardModel()
@ -35,8 +36,8 @@ class RunningMeeting(val props: DefaultProps, outGW: OutMessageGateway,
// We extract the meeting handlers into this class so it is
// easy to test.
private val liveMeeting = new LiveMeeting(props, meetingStatux2x, deskshareModel, chatModel, layouts,
registeredUsers, polls2x, wbModel, presModel, captionModel,
private val liveMeeting = new LiveMeeting(props, meetingStatux2x, deskshareModel, chatModel, externalVideoModel,
layouts, registeredUsers, polls2x, wbModel, presModel, captionModel,
webcams, voiceUsers, users2x, guestsWaiting)
GuestsWaiting.setGuestPolicy(
@ -44,6 +45,12 @@ class RunningMeeting(val props: DefaultProps, outGW: OutMessageGateway,
GuestPolicy(props.usersProp.guestPolicy, SystemUser.ID)
)
Layouts.setCurrentLayout(
liveMeeting.layouts,
props.usersProp.meetingLayout,
SystemUser.ID
)
private val recordEvents = props.recordProp.record || props.recordProp.keepEvents
val outMsgRouter = new OutMsgRouter(recordEvents, outGW)

View File

@ -3,7 +3,6 @@ package org.bigbluebutton.core2
import akka.actor.{ Actor, ActorLogging, Props }
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.common2.util.JsonUtil
object AnalyticsActor {
def props(includeChat: Boolean): Props = Props(classOf[AnalyticsActor], includeChat)
}
@ -61,6 +60,7 @@ class AnalyticsActor(val includeChat: Boolean) extends Actor with ActorLogging {
case m: RequestBreakoutJoinURLReqMsg => logMessage(msg)
case m: EndAllBreakoutRoomsMsg => logMessage(msg)
case m: TransferUserToMeetingRequestMsg => logMessage(msg)
case m: ExtendBreakoutRoomsTimeReqMsg => logMessage(msg)
case m: UserLeftVoiceConfToClientEvtMsg => logMessage(msg)
case m: UserLeftVoiceConfEvtMsg => logMessage(msg)
case m: RecordingStartedVoiceConfEvtMsg => logMessage(msg)
@ -167,6 +167,11 @@ class AnalyticsActor(val includeChat: Boolean) extends Actor with ActorLogging {
case m: ChangeLockSettingsInMeetingCmdMsg => logMessage(msg)
case m: GetLockSettingsReqMsg => logMessage(msg)
case m: LockSettingsNotInitializedRespMsg => logMessage(msg)
case m: MeetingInfoAnalyticsMsg => logMessage(msg)
// Layout
case m: BroadcastLayoutMsg => logMessage(msg)
case m: BroadcastLayoutEvtMsg => logMessage(msg)
case _ => // ignore message
}

View File

@ -160,6 +160,17 @@ object MsgBuilder {
BbbCommonEnvCoreMsg(envelope, event)
}
def buildStopExternalVideoEvtMsg(meetingId: String): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, "nodeJSapp")
val envelope = BbbCoreEnvelope(StopExternalVideoEvtMsg.NAME, routing)
val body = StopExternalVideoEvtMsgBody()
val header = BbbClientMsgHeader(StopExternalVideoEvtMsg.NAME, meetingId, "not-used")
val event = StopExternalVideoEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
def buildMeetingCreatedEvtMsg(meetingId: String, props: DefaultProps): BbbCommonEnvCoreMsg = {
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
val envelope = BbbCoreEnvelope(MeetingCreatedEvtMsg.NAME, routing)
@ -169,6 +180,35 @@ object MsgBuilder {
BbbCommonEnvCoreMsg(envelope, event)
}
def buildMeetingInfoAnalyticsMsg(analytics: MeetingInfoAnalytics): BbbCommonEnvCoreMsg = {
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
val envelope = BbbCoreEnvelope(MeetingInfoAnalyticsMsg.NAME, routing)
val header = BbbCoreBaseHeader(MeetingInfoAnalyticsMsg.NAME)
val body = MeetingInfoAnalyticsMsgBody(analytics)
val event = MeetingInfoAnalyticsMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
def buildMeetingInfoAnalyticsServiceMsg(analytics: MeetingInfoAnalytics): BbbCommonEnvCoreMsg = {
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
val envelope = BbbCoreEnvelope(MeetingInfoAnalyticsServiceMsg.NAME, routing)
val header = BbbCoreBaseHeader(MeetingInfoAnalyticsServiceMsg.NAME)
val body = MeetingInfoAnalyticsMsgBody(analytics)
val event = MeetingInfoAnalyticsServiceMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
def buildCamStreamSubscribeSysMsg(meetingId: String, userId: String, streamId: String, sfuSessionId: String): BbbCommonEnvCoreMsg = {
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
val envelope = BbbCoreEnvelope(CamStreamSubscribeSysMsg.NAME, routing)
val header = BbbCoreBaseHeader(CamStreamSubscribeSysMsg.NAME)
val body = CamStreamSubscribeSysMsgBody(meetingId, userId, streamId, sfuSessionId)
val event = CamStreamSubscribeSysMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
def buildMeetingDestroyedEvtMsg(meetingId: String): BbbCommonEnvCoreMsg = {
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
val envelope = BbbCoreEnvelope(MeetingDestroyedEvtMsg.NAME, routing)
@ -504,4 +544,15 @@ object MsgBuilder {
BbbCommonEnvCoreMsg(envelope, event)
}
def buildLearningDashboardEvtMsg(meetingId: String, activityJson: String): BbbCommonEnvCoreMsg = {
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
val envelope = BbbCoreEnvelope(LearningDashboardEvtMsg.NAME, routing)
val body = LearningDashboardEvtMsgBody(activityJson)
val header = BbbCoreHeaderWithMeetingId(LearningDashboardEvtMsg.NAME, meetingId)
val event = LearningDashboardEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
}

View File

@ -0,0 +1,433 @@
package org.bigbluebutton.endpoint.redis
import akka.actor.{Actor, ActorLogging, ActorSystem, Props}
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.common2.util.JsonUtil
import org.bigbluebutton.core.OutMessageGateway
import org.bigbluebutton.core.apps.groupchats.GroupChatApp
import org.bigbluebutton.core.models.Roles
import org.bigbluebutton.core2.message.senders.MsgBuilder
import java.security.MessageDigest
import scala.concurrent.duration._
import scala.concurrent._
import ExecutionContext.Implicits.global
case object SendPeriodicReport
case class Meeting(
intId: String,
extId: String,
name: String,
learningDashboardAccessToken: String,
users: Map[String, User] = Map(),
polls: Map[String, Poll] = Map(),
screenshares: Vector[Screenshare] = Vector(),
createdOn: Long = System.currentTimeMillis(),
endedOn: Long = 0,
)
case class User(
intId: String,
extId: String,
name: String,
isModerator: Boolean,
isDialIn: Boolean = false,
answers: Map[String,String] = Map(),
talk: Talk = Talk(),
emojis: Vector[Emoji] = Vector(),
webcams: Vector[Webcam] = Vector(),
totalOfMessages: Long = 0,
registeredOn: Long = System.currentTimeMillis(),
leftOn: Long = 0,
)
case class Poll(
pollId: String,
pollType: String,
anonymous: Boolean,
question: String,
options: Vector[String] = Vector(),
anonymousAnswers: Vector[String] = Vector(),
createdOn: Long = System.currentTimeMillis(),
)
case class Talk(
totalTime: Long = 0,
lastTalkStartedOn: Long = 0,
)
case class Emoji(
name: String,
sentOn: Long = System.currentTimeMillis(),
)
case class Webcam(
startedOn: Long = System.currentTimeMillis(),
stoppedOn: Long = 0,
)
case class Screenshare(
startedOn: Long = System.currentTimeMillis(),
stoppedOn: Long = 0,
)
object LearningDashboardActor {
def props(
system: ActorSystem,
outGW: OutMessageGateway,
): Props =
Props(
classOf[LearningDashboardActor],
system,
outGW
)
}
class LearningDashboardActor(
system: ActorSystem,
val outGW: OutMessageGateway,
) extends Actor with ActorLogging {
private var meetings: Map[String, Meeting] = Map()
private var meetingsLastJsonHash : Map[String,String] = Map()
system.scheduler.schedule(10.seconds, 10.seconds, self, SendPeriodicReport)
def receive = {
//=============================
// 2x messages
case msg: BbbCommonEnvCoreMsg => handleBbbCommonEnvCoreMsg(msg)
case SendPeriodicReport => sendPeriodicReport()
case _ => // do nothing
}
private def handleBbbCommonEnvCoreMsg(msg: BbbCommonEnvCoreMsg): Unit = {
msg.core match {
// Chat
case m: GroupChatMessageBroadcastEvtMsg => handleGroupChatMessageBroadcastEvtMsg(m)
// User
case m: UserJoinedMeetingEvtMsg => handleUserJoinedMeetingEvtMsg(m)
case m: UserLeftMeetingEvtMsg => handleUserLeftMeetingEvtMsg(m)
case m: UserEmojiChangedEvtMsg => handleUserEmojiChangedEvtMsg(m)
case m: UserRoleChangedEvtMsg => handleUserRoleChangedEvtMsg(m)
case m: UserBroadcastCamStartedEvtMsg => handleUserBroadcastCamStartedEvtMsg(m)
case m: UserBroadcastCamStoppedEvtMsg => handleUserBroadcastCamStoppedEvtMsg(m)
// Voice
case m: UserJoinedVoiceConfToClientEvtMsg => handleUserJoinedVoiceConfToClientEvtMsg(m)
case m: UserLeftVoiceConfToClientEvtMsg => handleUserLeftVoiceConfToClientEvtMsg(m)
case m: UserMutedVoiceEvtMsg => handleUserMutedVoiceEvtMsg(m)
case m: UserTalkingVoiceEvtMsg => handleUserTalkingVoiceEvtMsg(m)
// Screenshare
case m: ScreenshareRtmpBroadcastStartedEvtMsg => handleScreenshareRtmpBroadcastStartedEvtMsg(m)
case m: ScreenshareRtmpBroadcastStoppedEvtMsg => handleScreenshareRtmpBroadcastStoppedEvtMsg(m)
// Meeting
case m: CreateMeetingReqMsg => handleCreateMeetingReqMsg(m)
case m: MeetingEndingEvtMsg => handleMeetingEndingEvtMsg(m)
// Poll
case m: PollStartedEvtMsg => handlePollStartedEvtMsg(m)
case m: UserRespondedToPollRecordMsg => handleUserRespondedToPollRecordMsg(m)
case _ => // message not to be handled.
}
}
private def handleGroupChatMessageBroadcastEvtMsg(msg: GroupChatMessageBroadcastEvtMsg) {
if (msg.body.chatId == GroupChatApp.MAIN_PUBLIC_CHAT) {
for {
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
user <- meeting.users.values.find(u => u.intId == msg.header.userId)
} yield {
val updatedUser = user.copy(totalOfMessages = user.totalOfMessages + 1)
val updatedMeeting = meeting.copy(users = meeting.users + (updatedUser.intId -> updatedUser))
meetings += (updatedMeeting.intId -> updatedMeeting)
}
}
}
private def handleUserJoinedMeetingEvtMsg(msg: UserJoinedMeetingEvtMsg): Unit = {
for {
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
} yield {
val user: User = meeting.users.values.find(u => u.intId == msg.body.intId).getOrElse({
User(
msg.body.intId, msg.body.extId, msg.body.name, (msg.body.role == Roles.MODERATOR_ROLE)
)
})
meetings += (meeting.intId -> meeting.copy(users = meeting.users + (user.intId -> user.copy(leftOn = 0))))
}
}
private def handleUserLeftMeetingEvtMsg(msg: UserLeftMeetingEvtMsg): Unit = {
for {
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
user <- meeting.users.values.find(u => u.intId == msg.body.intId)
} yield {
val updatedUser = user.copy(leftOn = System.currentTimeMillis())
val updatedMeeting = meeting.copy(users = meeting.users + (updatedUser.intId -> updatedUser))
meetings += (updatedMeeting.intId -> updatedMeeting)
}
}
private def handleUserEmojiChangedEvtMsg(msg: UserEmojiChangedEvtMsg) {
for {
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
user <- meeting.users.values.find(u => u.intId == msg.body.userId)
} yield {
if (msg.body.emoji != "none") {
val updatedUser = user.copy(emojis = user.emojis :+ Emoji(msg.body.emoji))
val updatedMeeting = meeting.copy(users = meeting.users + (updatedUser.intId -> updatedUser))
meetings += (updatedMeeting.intId -> updatedMeeting)
}
}
}
private def handleUserRoleChangedEvtMsg(msg: UserRoleChangedEvtMsg) {
if(msg.body.role == Roles.MODERATOR_ROLE) {
for {
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
user <- meeting.users.values.find(u => u.intId == msg.body.userId)
} yield {
val updatedUser = user.copy(isModerator = true)
val updatedMeeting = meeting.copy(users = meeting.users + (updatedUser.intId -> updatedUser))
meetings += (updatedMeeting.intId -> updatedMeeting)
}
}
}
private def handleUserBroadcastCamStartedEvtMsg(msg: UserBroadcastCamStartedEvtMsg) {
for {
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
user <- meeting.users.values.find(u => u.intId == msg.body.userId)
} yield {
val updatedUser = user.copy(webcams = user.webcams :+ Webcam())
val updatedMeeting = meeting.copy(users = meeting.users + (updatedUser.intId -> updatedUser))
meetings += (updatedMeeting.intId -> updatedMeeting)
}
}
private def handleUserBroadcastCamStoppedEvtMsg(msg: UserBroadcastCamStoppedEvtMsg) {
for {
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
user <- meeting.users.values.find(u => u.intId == msg.body.userId)
} yield {
val lastWebcam: Webcam = user.webcams.last.copy(stoppedOn = System.currentTimeMillis())
val updatedUser = user.copy(webcams = user.webcams.dropRight(1) :+ lastWebcam)
val updatedMeeting = meeting.copy(users = meeting.users + (updatedUser.intId -> updatedUser))
meetings += (updatedMeeting.intId -> updatedMeeting)
}
}
private def handleUserJoinedVoiceConfToClientEvtMsg(msg: UserJoinedVoiceConfToClientEvtMsg): Unit = {
//Create users for Dial-in connections
if(msg.body.intId.startsWith("v_")) {
for {
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
} yield {
val user: User = meeting.users.values.find(u => u.intId == msg.body.intId).getOrElse({
User(
msg.body.intId, msg.body.callerNum, msg.body.callerName, false, true
)
})
meetings += (meeting.intId -> meeting.copy(users = meeting.users + (user.intId -> user.copy(leftOn = 0))))
}
}
}
private def handleUserLeftVoiceConfToClientEvtMsg(msg: UserLeftVoiceConfToClientEvtMsg) {
for {
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
user <- meeting.users.values.find(u => u.intId == msg.body.intId)
} yield {
endUserTalk(meeting, user)
if(user.isDialIn) {
val updatedUser = user.copy(leftOn = System.currentTimeMillis())
val updatedMeeting = meeting.copy(users = meeting.users + (updatedUser.intId -> updatedUser))
meetings += (updatedMeeting.intId -> updatedMeeting)
}
}
}
private def handleUserMutedVoiceEvtMsg(msg: UserMutedVoiceEvtMsg) {
for {
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
user <- meeting.users.values.find(u => u.intId == msg.body.intId)
} yield {
endUserTalk(meeting, user)
}
}
private def handleUserTalkingVoiceEvtMsg(msg: UserTalkingVoiceEvtMsg) {
for {
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
user <- meeting.users.values.find(u => u.intId == msg.body.intId)
} yield {
if(msg.body.talking) {
val updatedUser = user.copy(talk = user.talk.copy(lastTalkStartedOn = System.currentTimeMillis()))
val updatedMeeting = meeting.copy(users = meeting.users + (updatedUser.intId -> updatedUser))
meetings += (updatedMeeting.intId -> updatedMeeting)
} else {
endUserTalk(meeting, user)
}
}
}
private def endUserTalk(meeting: Meeting, user: User): Unit = {
if(user.talk.lastTalkStartedOn > 0) {
val updatedUser = user.copy(
talk = user.talk.copy(
lastTalkStartedOn = 0,
totalTime = user.talk.totalTime + (System.currentTimeMillis() - user.talk.lastTalkStartedOn)
)
)
val updatedMeeting = meeting.copy(users = meeting.users + (updatedUser.intId -> updatedUser))
meetings += (updatedMeeting.intId -> updatedMeeting)
}
}
private def handlePollStartedEvtMsg(msg: PollStartedEvtMsg): Unit = {
for {
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
} yield {
val options = msg.body.poll.answers.map(answer => answer.key)
val newPoll = Poll(msg.body.pollId, msg.body.pollType, msg.body.secretPoll, msg.body.question, options.toVector)
val updatedMeeting = meeting.copy(polls = meeting.polls + (newPoll.pollId -> newPoll))
meetings += (updatedMeeting.intId -> updatedMeeting)
}
}
private def handleUserRespondedToPollRecordMsg(msg: UserRespondedToPollRecordMsg): Unit = {
for {
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
user <- meeting.users.values.find(u => u.intId == msg.header.userId)
} yield {
if(msg.body.isSecret) {
//Store Anonymous Poll in `poll.anonymousAnswers`
for {
poll <- meeting.polls.find(p => p._1 == msg.body.pollId)
} yield {
val updatedPoll = poll._2.copy(anonymousAnswers = poll._2.anonymousAnswers :+ msg.body.answer)
val updatedMeeting = meeting.copy(polls = meeting.polls + (poll._1 -> updatedPoll))
meetings += (updatedMeeting.intId -> updatedMeeting)
}
} else {
//Store Public Poll in `user.answers`
val updatedUser = user.copy(answers = user.answers + (msg.body.pollId -> msg.body.answer))
val updatedMeeting = meeting.copy(users = meeting.users + (updatedUser.intId -> updatedUser))
meetings += (updatedMeeting.intId -> updatedMeeting)
}
}
}
private def handleScreenshareRtmpBroadcastStartedEvtMsg(msg: ScreenshareRtmpBroadcastStartedEvtMsg) {
for {
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
} yield {
val updatedMeeting = meeting.copy(screenshares = meeting.screenshares :+ Screenshare())
meetings += (updatedMeeting.intId -> updatedMeeting)
}
}
private def handleScreenshareRtmpBroadcastStoppedEvtMsg(msg: ScreenshareRtmpBroadcastStoppedEvtMsg) {
for {
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
} yield {
val lastScreenshare: Screenshare = meeting.screenshares.last.copy(stoppedOn = System.currentTimeMillis())
val updatedMeeting = meeting.copy(screenshares = meeting.screenshares.dropRight(1) :+ lastScreenshare)
meetings += (updatedMeeting.intId -> updatedMeeting)
}
}
private def handleCreateMeetingReqMsg(msg: CreateMeetingReqMsg): Unit = {
if(msg.body.props.meetingProp.learningDashboardEnabled) {
val newMeeting = Meeting(
msg.body.props.meetingProp.intId,
msg.body.props.meetingProp.extId,
msg.body.props.meetingProp.name,
msg.body.props.password.learningDashboardAccessToken,
)
meetings += (newMeeting.intId -> newMeeting)
log.info(" created for meeting {}.",msg.body.props.meetingProp.intId)
} else {
log.info(" disabled for meeting {}.",msg.body.props.meetingProp.intId)
}
}
private def handleMeetingEndingEvtMsg(msg: MeetingEndingEvtMsg): Unit = {
for {
meeting <- meetings.values.find(m => m.intId == msg.body.meetingId)
} yield {
//Update endedOn and screenshares.stoppedOn, user.totalTime talks, webcams.stoppedOn
val endedOn : Long = System.currentTimeMillis()
val updatedMeeting = meeting.copy(
endedOn = endedOn,
screenshares = meeting.screenshares.map(screenshare => {
if(screenshare.stoppedOn > 0) screenshare;
else screenshare.copy(stoppedOn = endedOn)
}),
users = meeting.users.map(user => {
(user._1 ->
user._2.copy(
leftOn = if(user._2.leftOn > 0) user._2.leftOn else endedOn,
talk = user._2.talk.copy(
totalTime = user._2.talk.totalTime + (if (user._2.talk.lastTalkStartedOn > 0) (endedOn - user._2.talk.lastTalkStartedOn) else 0),
lastTalkStartedOn = 0
),
webcams = user._2.webcams.map(webcam => {
if(webcam.stoppedOn > 0) webcam
else webcam.copy(stoppedOn = endedOn)
})
))
})
)
meetings += (updatedMeeting.intId -> updatedMeeting)
//Send report one last time
sendReport(updatedMeeting)
meetings = meetings.-(updatedMeeting.intId)
log.info(" removed for meeting {}.",updatedMeeting.intId)
}
}
private def sendPeriodicReport(): Unit = {
meetings.map(meeting => {
sendReport(meeting._2)
})
}
private def sendReport(meeting : Meeting): Unit = {
val activityJson: String = JsonUtil.toJson(meeting)
//Avoid send repeated activity jsons
val activityJsonHash : String = MessageDigest.getInstance("MD5").digest(activityJson.getBytes).mkString
if(!meetingsLastJsonHash.contains(meeting.intId) || meetingsLastJsonHash.get(meeting.intId).getOrElse("") != activityJsonHash) {
val event = MsgBuilder.buildLearningDashboardEvtMsg(meeting.intId, activityJson)
outGW.send(event)
meetingsLastJsonHash += (meeting.intId -> activityJsonHash)
log.info("Activity Report sent for meeting {}",meeting.intId)
}
}
}

View File

@ -85,6 +85,7 @@ class RedisRecorderActor(
case m: UserLeftMeetingEvtMsg => handleUserLeftMeetingEvtMsg(m)
case m: PresenterAssignedEvtMsg => handlePresenterAssignedEvtMsg(m)
case m: UserEmojiChangedEvtMsg => handleUserEmojiChangedEvtMsg(m)
case m: UserRoleChangedEvtMsg => handleUserRoleChangedEvtMsg(m)
case m: UserBroadcastCamStartedEvtMsg => handleUserBroadcastCamStartedEvtMsg(m)
case m: UserBroadcastCamStoppedEvtMsg => handleUserBroadcastCamStoppedEvtMsg(m)
@ -357,6 +358,10 @@ class RedisRecorderActor(
handleUserStatusChange(msg.header.meetingId, msg.body.userId, "emojiStatus", msg.body.emoji)
}
private def handleUserRoleChangedEvtMsg(msg: UserRoleChangedEvtMsg) {
handleUserStatusChange(msg.header.meetingId, msg.body.userId, "role", msg.body.role)
}
private def handleUserBroadcastCamStartedEvtMsg(msg: UserBroadcastCamStartedEvtMsg) {
handleUserStatusChange(msg.header.meetingId, msg.body.userId, "hasStream", "true,stream=" + msg.body.stream)
}
@ -555,7 +560,10 @@ class RedisRecorderActor(
private def handlePollStartedEvtMsg(msg: PollStartedEvtMsg): Unit = {
val ev = new PollStartedRecordEvent()
ev.setPollId(msg.body.pollId)
ev.setQuestion(msg.body.question)
ev.setAnswers(msg.body.poll.answers)
ev.setType(msg.body.pollType)
ev.setSecretPoll(msg.body.secretPoll)
record(msg.header.meetingId, ev.toMap.asJava)
}
@ -563,25 +571,33 @@ class RedisRecorderActor(
private def handleUserRespondedToPollRecordMsg(msg: UserRespondedToPollRecordMsg): Unit = {
val ev = new UserRespondedToPollRecordEvent()
ev.setPollId(msg.body.pollId)
ev.setUserId(msg.header.userId)
if (msg.body.isSecret) {
ev.setUserId("")
} else {
ev.setUserId(msg.header.userId)
}
ev.setAnswerId(msg.body.answerIds.toArray)
ev.setAnswer(msg.body.answer)
record(msg.header.meetingId, ev.toMap.asJava)
}
private def handlePollStoppedEvtMsg(msg: PollStoppedEvtMsg): Unit = {
pollStoppedRecordHelper(msg.header.meetingId, msg.body.pollId)
val ev = new PollStoppedRecordEvent()
ev.setPollId(msg.body.pollId)
record(msg.header.meetingId, ev.toMap.asJava)
}
private def handlePollShowResultEvtMsg(msg: PollShowResultEvtMsg): Unit = {
pollStoppedRecordHelper(msg.header.meetingId, msg.body.pollId)
}
val ev = new PollPublishedRecordEvent()
ev.setPollId(msg.body.pollId)
ev.setQuestion(msg.body.poll.questionText.getOrElse(""))
ev.setAnswers(msg.body.poll.answers)
ev.setNumRespondents(msg.body.poll.numRespondents)
ev.setNumResponders(msg.body.poll.numResponders)
private def pollStoppedRecordHelper(meetingId: String, pollId: String): Unit = {
val ev = new PollStoppedRecordEvent()
ev.setPollId(pollId)
record(meetingId, ev.toMap.asJava)
record(msg.header.meetingId, ev.toMap.asJava)
}
private def checkRecordingDBStatus(): Unit = {

View File

@ -0,0 +1,95 @@
package org.bigbluebutton.service
import akka.actor.{ Actor, ActorLogging, ActorRef, ActorSystem, Props }
import akka.pattern.ask
import akka.pattern.AskTimeoutException
import akka.util.Timeout
import org.bigbluebutton.MeetingInfoAnalytics
import org.bigbluebutton.common2.msgs.{ BbbCommonEnvCoreMsg, MeetingEndingEvtMsg, MeetingInfoAnalyticsServiceMsg }
import scala.collection.mutable
import scala.concurrent.duration.DurationInt
import scala.concurrent.{ ExecutionContextExecutor, Future }
sealed trait MeetingInfoMessage
case class GetMeetingInfoMessage(meetingId: String) extends MeetingInfoMessage
case object GetMeetingsInfoMessage extends MeetingInfoMessage
case class MeetingInfoResponseMsg(optionMeetingInfoAnalytics: Option[MeetingInfoAnalytics]) extends MeetingInfoMessage
case class MeetingInfoListResponseMsg(optionMeetingsInfoAnalytics: Option[List[MeetingInfoAnalytics]]) extends MeetingInfoMessage
object MeetingInfoService {
def apply(system: ActorSystem, meetingInfoActor: ActorRef) = new MeetingInfoService(system, meetingInfoActor)
}
class MeetingInfoService(system: ActorSystem, meetingInfoActor: ActorRef) {
implicit def executionContext: ExecutionContextExecutor = system.dispatcher
implicit val timeout: Timeout = 2 seconds
def getAnalytics(): Future[MeetingInfoListResponseMsg] = {
val future = meetingInfoActor.ask(GetMeetingsInfoMessage).mapTo[MeetingInfoListResponseMsg]
future.recover {
case e: AskTimeoutException => {
MeetingInfoListResponseMsg(None)
}
}
}
def getAnalytics(meetingId: String): Future[MeetingInfoResponseMsg] = {
val future = meetingInfoActor.ask(GetMeetingInfoMessage(meetingId)).mapTo[MeetingInfoResponseMsg]
future.recover {
case e: AskTimeoutException => {
MeetingInfoResponseMsg(None)
}
}
}
}
object MeetingInfoActor {
def props(): Props = Props(classOf[MeetingInfoActor])
}
class MeetingInfoActor extends Actor with ActorLogging {
var optionMeetingInfo: Option[MeetingInfoAnalytics] = None
var meetingInfoMap: mutable.HashMap[String, MeetingInfoAnalytics] = mutable.HashMap.empty[String, MeetingInfoAnalytics]
override def receive: Receive = {
case msg: BbbCommonEnvCoreMsg => handle(msg)
case GetMeetingsInfoMessage =>
if (meetingInfoMap.size > 0) {
sender ! MeetingInfoListResponseMsg(Option(meetingInfoMap.values.toList))
} else {
sender ! MeetingInfoListResponseMsg(None)
}
case GetMeetingInfoMessage(meetingId) =>
meetingInfoMap.get(meetingId) match {
case Some(meetingInfoAnalytics) =>
sender ! MeetingInfoResponseMsg(Option(meetingInfoAnalytics))
case None => sender ! MeetingInfoResponseMsg(None)
}
case _ => // ignore other messages
}
def handle(msg: BbbCommonEnvCoreMsg): Unit = {
msg.core match {
case m: MeetingInfoAnalyticsServiceMsg =>
val meetingInternalId = m.body.meetingInfo.internalId
optionMeetingInfo = Option.apply(MeetingInfoAnalytics(m.body.meetingInfo.name, m.body.meetingInfo.externalId,
meetingInternalId, m.body.meetingInfo.hasUserJoined, m.body.meetingInfo.isMeetingRecorded, m.body.meetingInfo.webcams,
m.body.meetingInfo.audio, m.body.meetingInfo.screenshare, m.body.meetingInfo.users, m.body.meetingInfo.presentation,
m.body.meetingInfo.breakoutRoom))
meetingInfoMap.get(meetingInternalId) match {
case Some(_) => {
meetingInfoMap(meetingInternalId) = optionMeetingInfo.get
}
case None => meetingInfoMap += (meetingInternalId -> optionMeetingInfo.get)
}
case m: MeetingEndingEvtMsg => meetingInfoMap -= m.body.meetingId
case _ => // ignore
}
}
}

View File

@ -29,6 +29,7 @@ trait AppsTestFixtures {
val webcamsOnlyForModerator = false;
val moderatorPassword = "modpass"
val viewerPassword = "viewpass"
val learningDashboardAccessToken = "ldToken"
val createTime = System.currentTimeMillis
val createDate = "Oct 26, 2015"
val isBreakout = false
@ -52,7 +53,7 @@ trait AppsTestFixtures {
val durationProps = DurationProps(duration = durationInMinutes, createdTime = createTime, createdDate = createDate,
meetingExpireIfNoUserJoinedInMinutes = meetingExpireIfNoUserJoinedInMinutes, meetingExpireWhenLastUserLeftInMinutes = meetingExpireWhenLastUserLeftInMinutes,
userInactivityInspectTimerInMinutes = userInactivityInspectTimerInMinutes, userInactivityThresholdInMinutes = userInactivityInspectTimerInMinutes, userActivitySignResponseDelayInMinutes = userActivitySignResponseDelayInMinutes)
val password = PasswordProp(moderatorPass = moderatorPassword, viewerPass = viewerPassword)
val password = PasswordProp(moderatorPass = moderatorPassword, viewerPass = viewerPassword, learningDashboardAccessToken = learningDashboardAccessToken)
val recordProp = RecordProp(record = record, autoStartRecording = autoStartRecording,
allowStartStopRecording = allowStartStopRecording, keepEvents = keepEvents )
val welcomeProp = WelcomeProp(welcomeMsgTemplate = welcomeMsgTemplate, welcomeMsg = welcomeMsg,

View File

@ -4,9 +4,11 @@ case class ConfigProps(defaultConfigToken: String, config: String)
case class DurationProps(duration: Int, createdTime: Long, createdDate: String,
meetingExpireIfNoUserJoinedInMinutes: Int, meetingExpireWhenLastUserLeftInMinutes: Int,
userInactivityInspectTimerInMinutes: Int, userInactivityThresholdInMinutes: Int, userActivitySignResponseDelayInMinutes: Int)
userInactivityInspectTimerInMinutes: Int, userInactivityThresholdInMinutes: Int,
userActivitySignResponseDelayInMinutes: Int,
endWhenNoModerator: Boolean, endWhenNoModeratorDelayInMinutes: Int)
case class MeetingProp(name: String, extId: String, intId: String, isBreakout: Boolean)
case class MeetingProp(name: String, extId: String, intId: String, isBreakout: Boolean, learningDashboardEnabled: Boolean)
case class BreakoutProps(
parentId: String,
@ -18,7 +20,7 @@ case class BreakoutProps(
privateChatEnabled: Boolean
)
case class PasswordProp(moderatorPass: String, viewerPass: String)
case class PasswordProp(moderatorPass: String, viewerPass: String, learningDashboardAccessToken: String)
case class RecordProp(record: Boolean, autoStartRecording: Boolean, allowStartStopRecording: Boolean, keepEvents: Boolean)
@ -26,7 +28,7 @@ case class WelcomeProp(welcomeMsgTemplate: String, welcomeMsg: String, modOnlyMe
case class VoiceProp(telVoice: String, voiceConf: String, dialNumber: String, muteOnStart: Boolean)
case class UsersProp(maxUsers: Int, webcamsOnlyForModerator: Boolean, guestPolicy: String, allowModsToUnmuteUsers: Boolean, authenticatedGuest: Boolean)
case class UsersProp(maxUsers: Int, webcamsOnlyForModerator: Boolean, guestPolicy: String, meetingLayout: String, allowModsToUnmuteUsers: Boolean, authenticatedGuest: Boolean)
case class MetadataProp(metadata: collection.immutable.Map[String, String])
@ -73,13 +75,13 @@ case class MeetingStatus(startEndTimeStatus: StartEndTimeStatus, recordingStatus
case class Meeting2x(defaultProps: DefaultProps, meetingStatus: MeetingStatus)
case class SimpleAnswerOutVO(id: Int, key: String)
case class SimplePollOutVO(id: String, isMultipleResponse: Boolean, answers: Array[SimpleAnswerOutVO])
case class SimplePollOutVO(id: String, answers: Array[SimpleAnswerOutVO])
case class SimpleVoteOutVO(id: Int, key: String, numVotes: Int)
case class SimplePollResultOutVO(id: String, answers: Array[SimpleVoteOutVO], numRespondents: Int, numResponders: Int)
case class SimplePollResultOutVO(id: String, questionType: String, questionText: Option[String], answers: Array[SimpleVoteOutVO], numRespondents: Int, numResponders: Int)
case class Responder(userId: String, name: String)
case class AnswerVO(id: Int, key: String, text: Option[String], responders: Option[Array[Responder]])
case class QuestionVO(id: Int, questionType: String, isMultipleResponse: Boolean, questionText: Option[String], answers: Option[Array[AnswerVO]])
case class PollVO(id: String, questions: Array[QuestionVO], title: Option[String], started: Boolean, stopped: Boolean, showResult: Boolean)
case class QuestionVO(id: Int, questionType: String, multiResponse: Boolean, questionText: Option[String], answers: Option[Array[AnswerVO]])
case class PollVO(id: String, questions: Array[QuestionVO], title: Option[String], started: Boolean, stopped: Boolean, showResult: Boolean, isSecret: Boolean)
case class UserVO(id: String, externalId: String, name: String, role: String,
guest: Boolean, authed: Boolean, guestStatus: String, emojiStatus: String,

View File

@ -13,7 +13,7 @@ case class BreakoutRoomJoinURLEvtMsgBody(parentId: String, breakoutId: String, e
object BreakoutRoomsListEvtMsg { val NAME = "BreakoutRoomsListEvtMsg" }
case class BreakoutRoomsListEvtMsg(header: BbbClientMsgHeader, body: BreakoutRoomsListEvtMsgBody) extends BbbCoreMsg
case class BreakoutRoomsListEvtMsgBody(meetingId: String, rooms: Vector[BreakoutRoomInfo], roomsReady: Boolean)
case class BreakoutRoomInfo(name: String, externalId: String, breakoutId: String, sequence: Int, freeJoin: Boolean)
case class BreakoutRoomInfo(name: String, externalId: String, breakoutId: String, sequence: Int, shortName: String, isDefaultName: Boolean, freeJoin: Boolean)
object BreakoutRoomsListMsg { val NAME = "BreakoutRoomsListMsg" }
case class BreakoutRoomsListMsg(header: BbbClientMsgHeader, body: BreakoutRoomsListMsgBody) extends StandardMsg
@ -47,6 +47,8 @@ case class BreakoutRoomDetail(
name: String,
parentId: String,
sequence: Integer,
shortName: String,
isDefaultName: Boolean,
freeJoin: Boolean,
dialNumber: String,
voiceConfId: String,
@ -65,7 +67,7 @@ case class BreakoutRoomDetail(
object CreateBreakoutRoomsCmdMsg { val NAME = "CreateBreakoutRoomsCmdMsg" }
case class CreateBreakoutRoomsCmdMsg(header: BbbClientMsgHeader, body: CreateBreakoutRoomsCmdMsgBody) extends StandardMsg
case class CreateBreakoutRoomsCmdMsgBody(meetingId: String, durationInMinutes: Int, record: Boolean, rooms: Vector[BreakoutRoomMsgBody])
case class BreakoutRoomMsgBody(name: String, sequence: Int, freeJoin: Boolean, users: Vector[String])
case class BreakoutRoomMsgBody(name: String, sequence: Int, shortName: String, isDefaultName: Boolean, freeJoin: Boolean, users: Vector[String])
// Sent by user to request ending all the breakout rooms
object EndAllBreakoutRoomsMsg { val NAME = "EndAllBreakoutRoomsMsg" }
@ -100,6 +102,14 @@ object UpdateBreakoutUsersEvtMsg { val NAME = "UpdateBreakoutUsersEvtMsg" }
case class UpdateBreakoutUsersEvtMsg(header: BbbClientMsgHeader, body: UpdateBreakoutUsersEvtMsgBody) extends BbbCoreMsg
case class UpdateBreakoutUsersEvtMsgBody(parentId: String, breakoutId: String, users: Vector[BreakoutUserVO])
object ExtendBreakoutRoomsTimeReqMsg { val NAME = "ExtendBreakoutRoomsTimeReqMsg" }
case class ExtendBreakoutRoomsTimeReqMsg(header: BbbClientMsgHeader, body: ExtendBreakoutRoomsTimeReqMsgBody) extends StandardMsg
case class ExtendBreakoutRoomsTimeReqMsgBody(meetingId: String, extendTimeInMinutes: Int)
object ExtendBreakoutRoomsTimeEvtMsg { val NAME = "ExtendBreakoutRoomsTimeEvtMsg" }
case class ExtendBreakoutRoomsTimeEvtMsg(header: BbbClientMsgHeader, body: ExtendBreakoutRoomsTimeEvtMsgBody) extends BbbCoreMsg
case class ExtendBreakoutRoomsTimeEvtMsgBody(meetingId: String, extendTimeInMinutes: Int)
// Common Value objects
case class BreakoutUserVO(id: String, name: String)

View File

@ -0,0 +1,14 @@
package org.bigbluebutton.common2.msgs
object CamStreamSubscribeSysMsg { val NAME = "CamStreamSubscribeSysMsg" }
case class CamStreamSubscribeSysMsg(
header: BbbCoreBaseHeader,
body: CamStreamSubscribeSysMsgBody
) extends BbbCoreMsg
case class CamStreamSubscribeSysMsgBody(
meetingId: String,
userId: String,
streamId: String,
sfuSessionId: String
)

View File

@ -7,7 +7,7 @@ case class StartExternalVideoPubMsgBody(externalVideoUrl: String)
object UpdateExternalVideoPubMsg { val NAME = "UpdateExternalVideoPubMsg" }
case class UpdateExternalVideoPubMsg(header: BbbClientMsgHeader, body: UpdateExternalVideoPubMsgBody) extends StandardMsg
case class UpdateExternalVideoPubMsgBody(status: String, rate: Double, time: Double, state: Boolean)
case class UpdateExternalVideoPubMsgBody(status: String, rate: Double, time: Double, state: Int)
object StopExternalVideoPubMsg { val NAME = "StopExternalVideoPubMsg" }
case class StopExternalVideoPubMsg(header: BbbClientMsgHeader, body: StopExternalVideoPubMsgBody) extends StandardMsg
@ -20,7 +20,7 @@ case class StartExternalVideoEvtMsgBody(externalVideoUrl: String)
object UpdateExternalVideoEvtMsg { val NAME = "UpdateExternalVideoEvtMsg" }
case class UpdateExternalVideoEvtMsg(header: BbbClientMsgHeader, body: UpdateExternalVideoEvtMsgBody) extends BbbCoreMsg
case class UpdateExternalVideoEvtMsgBody(status: String, rate: Double, time: Double, state: Boolean)
case class UpdateExternalVideoEvtMsgBody(status: String, rate: Double, time: Double, state: Int)
object StopExternalVideoEvtMsg { val NAME = "StopExternalVideoEvtMsg" }
case class StopExternalVideoEvtMsg(header: BbbClientMsgHeader, body: StopExternalVideoEvtMsgBody) extends BbbCoreMsg

View File

@ -0,0 +1,47 @@
package org.bigbluebutton.common2.msgs
object MeetingInfoAnalyticsMsg { val NAME = "MeetingInfoAnalyticsMsg" }
case class MeetingInfoAnalyticsMsg(
header: BbbCoreBaseHeader,
body: MeetingInfoAnalyticsMsgBody
) extends BbbCoreMsg
case class MeetingInfoAnalyticsMsgBody(meetingInfo: MeetingInfoAnalytics)
object MeetingInfoAnalytics {
def apply(name: String, externalId: String, internalId: String, hasUserJoined: Boolean, isMeetingRecorded: Boolean,
webcam: Webcam, audio: Audio, screenshare: Screenshare, users: List[Participant], presentation: PresentationInfo,
breakoutRooms: BreakoutRoom): MeetingInfoAnalytics =
new MeetingInfoAnalytics(name, externalId, internalId, hasUserJoined, isMeetingRecorded, webcam, audio, screenshare, users,
presentation, breakoutRooms)
}
case class MeetingInfoAnalytics(
name: String,
externalId: String,
internalId: String,
hasUserJoined: Boolean,
isMeetingRecorded: Boolean,
webcams: Webcam,
audio: Audio,
screenshare: Screenshare,
users: List[Participant],
presentation: PresentationInfo,
breakoutRoom: BreakoutRoom
)
case class Webcam(total: Int, streams: WebcamStream)
case class WebcamStream(broadcasts: List[Broadcast], viewers: Set[String])
case class User(id: String, name: String)
case class Broadcast(id: String, user: User, startedOn: Long)
case class Audio(total: Int, listenOnly: ListenOnlyAudio, twoWay: TwoWayAudio, phone: PhoneAudio)
case class ListenOnlyAudio(total: Int, users: List[User])
case class TwoWayAudio(total: Int, users: List[User])
case class PhoneAudio(total: Int, users: List[User])
case class Screenshare(stream: ScreenshareStream)
case class ScreenshareStream(user: User, viewers: List[User])
case class Participant(id: String, name: String, role: String)
case class PresentationInfo(id: String, name: String)
case class BreakoutRoom(id: String, names: List[String])

View File

@ -0,0 +1,7 @@
package org.bigbluebutton.common2.msgs
object MeetingInfoAnalyticsServiceMsg { val NAME = "MeetingInfoAnalyticsServiceMsg" }
case class MeetingInfoAnalyticsServiceMsg(
header: BbbCoreBaseHeader,
body: MeetingInfoAnalyticsMsgBody
) extends BbbCoreMsg

View File

@ -16,7 +16,7 @@ case class PollShowResultEvtMsgBody(userId: String, pollId: String, poll: Simple
object PollStartedEvtMsg { val NAME = "PollStartedEvtMsg" }
case class PollStartedEvtMsg(header: BbbClientMsgHeader, body: PollStartedEvtMsgBody) extends BbbCoreMsg
case class PollStartedEvtMsgBody(userId: String, pollId: String, pollType: String, question: String, poll: SimplePollOutVO)
case class PollStartedEvtMsgBody(userId: String, pollId: String, pollType: String, secretPoll: Boolean, question: String, poll: SimplePollOutVO)
object PollStoppedEvtMsg { val NAME = "PollStoppedEvtMsg" }
case class PollStoppedEvtMsg(header: BbbClientMsgHeader, body: PollStoppedEvtMsgBody) extends BbbCoreMsg
@ -28,11 +28,11 @@ case class PollUpdatedEvtMsgBody(pollId: String, poll: SimplePollResultOutVO)
object UserRespondedToPollRecordMsg { val NAME = "UserRespondedToPollRecordMsg" }
case class UserRespondedToPollRecordMsg(header: BbbClientMsgHeader, body: UserRespondedToPollRecordMsgBody) extends BbbCoreMsg
case class UserRespondedToPollRecordMsgBody(pollId: String, answerIds: Seq[Int])
case class UserRespondedToPollRecordMsgBody(pollId: String, answerId: Int, answer: String, isSecret: Boolean)
object RespondToPollReqMsg { val NAME = "RespondToPollReqMsg" }
case class RespondToPollReqMsg(header: BbbClientMsgHeader, body: RespondToPollReqMsgBody) extends StandardMsg
case class RespondToPollReqMsgBody(requesterId: String, pollId: String, questionId: Int, answerIds: Seq[Int])
case class RespondToPollReqMsgBody(requesterId: String, pollId: String, questionId: Int, answerId: Int)
object RespondToTypedPollReqMsg { val NAME = "RespondToTypedPollReqMsg" }
case class RespondToTypedPollReqMsg(header: BbbClientMsgHeader, body: RespondToTypedPollReqMsgBody) extends StandardMsg
@ -40,7 +40,7 @@ case class RespondToTypedPollReqMsgBody(requesterId: String, pollId: String, que
object UserRespondedToPollRespMsg { val NAME = "UserRespondedToPollRespMsg" }
case class UserRespondedToPollRespMsg(header: BbbClientMsgHeader, body: UserRespondedToPollRespMsgBody) extends BbbCoreMsg
case class UserRespondedToPollRespMsgBody(pollId: String, userId: String, answerIds: Seq[Int])
case class UserRespondedToPollRespMsgBody(pollId: String, userId: String, answerId: Int)
object UserRespondedToTypedPollRespMsg { val NAME = "UserRespondedToTypedPollRespMsg" }
case class UserRespondedToTypedPollRespMsg(header: BbbClientMsgHeader, body: UserRespondedToTypedPollRespMsgBody) extends BbbCoreMsg
@ -52,12 +52,12 @@ case class ShowPollResultReqMsgBody(requesterId: String, pollId: String)
object StartCustomPollReqMsg { val NAME = "StartCustomPollReqMsg" }
case class StartCustomPollReqMsg(header: BbbClientMsgHeader, body: StartCustomPollReqMsgBody) extends StandardMsg
case class StartCustomPollReqMsgBody(requesterId: String, pollId: String, pollType: String, isMultipleResponse: Boolean, answers: Seq[String], question: String)
case class StartCustomPollReqMsgBody(requesterId: String, pollId: String, pollType: String, secretPoll: Boolean, answers: Seq[String], question: String)
object StartPollReqMsg { val NAME = "StartPollReqMsg" }
case class StartPollReqMsg(header: BbbClientMsgHeader, body: StartPollReqMsgBody) extends StandardMsg
case class StartPollReqMsgBody(requesterId: String, pollId: String, pollType: String, question: String, isMultipleResponse: Boolean)
case class StartPollReqMsgBody(requesterId: String, pollId: String, pollType: String, secretPoll: Boolean, question: String)
object StopPollReqMsg { val NAME = "StopPollReqMsg" }
case class StopPollReqMsg(header: BbbClientMsgHeader, body: StopPollReqMsgBody) extends StandardMsg
case class StopPollReqMsgBody(requesterId: String)
case class StopPollReqMsgBody(requesterId: String)

View File

@ -0,0 +1,14 @@
package org.bigbluebutton.common2.msgs
object ScreenStreamSubscribeSysMsg { val NAME = "ScreenStreamSubscribeSysMsg" }
case class ScreenStreamSubscribeSysMsg(
header: BbbCoreBaseHeader,
body: ScreenStreamSubscribeSysMsg
) extends BbbCoreMsg
case class ScreenStreamSubscribeSysMsgBody(
meetingId: String,
userId: String,
streamId: String,
sfuSessionId: String
)

View File

@ -223,3 +223,13 @@ case class UnpublishedRecordingSysMsgBody(recordId: String)
object DeletedRecordingSysMsg { val NAME = "DeletedRecordingSysMsg" }
case class DeletedRecordingSysMsg(header: BbbCoreBaseHeader, body: DeletedRecordingSysMsgBody) extends BbbCoreMsg
case class DeletedRecordingSysMsgBody(recordId: String)
/**
* Sent from akka-apps to bbb-web to inform a summary of the meeting activities
*/
object LearningDashboardEvtMsg { val NAME = "LearningDashboardEvtMsg" }
case class LearningDashboardEvtMsg(
header: BbbCoreHeaderWithMeetingId,
body: LearningDashboardEvtMsgBody
) extends BbbCoreMsg
case class LearningDashboardEvtMsgBody(activityJson: String)

View File

@ -17,12 +17,15 @@ trait TestFixtures {
val userInactivityInspectTimerInMinutes = 60
val userInactivityThresholdInMinutes = 10
val userActivitySignResponseDelayInMinutes = 5
val endWhenNoModerator = false
val endWhenNoModeratorDelayInMinutes = 1
val autoStartRecording = false
val allowStartStopRecording = false
val webcamsOnlyForModerator = false
val moderatorPassword = "modpass"
val viewerPassword = "viewpass"
val learningDashboardAccessToken = "ldToken"
val createTime = System.currentTimeMillis
val createDate = "Oct 26, 2015"
val isBreakout = false
@ -45,7 +48,7 @@ trait TestFixtures {
val durationProps = DurationProps(duration = durationInMinutes, createdTime = createTime, createdDate = createDate,
meetingExpireIfNoUserJoinedInMinutes = meetingExpireIfNoUserJoinedInMinutes, meetingExpireWhenLastUserLeftInMinutes = meetingExpireWhenLastUserLeftInMinutes,
userInactivityInspectTimerInMinutes = userInactivityInspectTimerInMinutes, userInactivityThresholdInMinutes = userInactivityInspectTimerInMinutes, userActivitySignResponseDelayInMinutes = userActivitySignResponseDelayInMinutes)
val password = PasswordProp(moderatorPass = moderatorPassword, viewerPass = viewerPassword)
val password = PasswordProp(moderatorPass = moderatorPassword, viewerPass = viewerPassword, learningDashboardAccessToken = learningDashboardAccessToken)
val recordProp = RecordProp(record = record, autoStartRecording = autoStartRecording,
allowStartStopRecording = allowStartStopRecording, keepEvents = keepEvents)
val welcomeProp = WelcomeProp(welcomeMsgTemplate = welcomeMsgTemplate, welcomeMsg = welcomeMsg,

View File

@ -95,3 +95,8 @@ pomExtra := (
licenses := Seq("LGPL-3.0" -> url("http://opensource.org/licenses/LGPL-3.0"))
homepage := Some(url("http://www.bigbluebutton.org"))
libraryDependencies += "javax.validation" % "validation-api" % "2.0.1.Final"
libraryDependencies += "org.springframework.boot" % "spring-boot-starter-validation" % "2.5.1"
libraryDependencies += "org.glassfish" % "javax.el" % "3.0.1-b12"
libraryDependencies += "org.apache.httpcomponents" % "httpclient" % "4.5.13"

View File

@ -26,11 +26,12 @@ object Dependencies {
// Server
val servlet = "3.1.0"
// Apache Commons
val lang = "3.9"
val io = "2.6"
val pool = "2.8.0"
val text = "1.9"
// BigBlueButton
val bbbCommons = "0.0.20-SNAPSHOT"
@ -57,10 +58,11 @@ object Dependencies {
val nuProcess = "com.zaxxer" % "nuprocess" % Versions.nuProcess
val servletApi = "javax.servlet" % "javax.servlet-api" % Versions.servlet
val apacheLang = "org.apache.commons" % "commons-lang3" % Versions.lang
val apacheIo = "commons-io" % "commons-io" % Versions.io
val apachePool2 = "org.apache.commons" % "commons-pool2" % Versions.pool
val apacheText = "org.apache.commons" % "commons-text" % Versions.text
val bbbCommons = "org.bigbluebutton" % "bbb-common-message_2.12" % Versions.bbbCommons excludeAll (
ExclusionRule(organization = "org.red5"))
@ -96,5 +98,6 @@ object Dependencies {
Compile.apacheLang,
Compile.apacheIo,
Compile.apachePool2,
Compile.apacheText,
Compile.bbbCommons) ++ testing
}

View File

@ -1,13 +1,13 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
*
* Copyright (c) 2017 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.
@ -33,6 +33,7 @@ public class ApiParams {
public static final String FREE_JOIN = "freeJoin";
public static final String FULL_NAME = "fullName";
public static final String GUEST_POLICY = "guestPolicy";
public static final String MEETING_LAYOUT = "meetingLayout";
public static final String IS_BREAKOUT = "isBreakout";
public static final String LOGO = "logo";
public static final String LOGOUT_TIMER = "logoutTimer";
@ -54,6 +55,8 @@ public class ApiParams {
public static final String SEQUENCE = "sequence";
public static final String VOICE_BRIDGE = "voiceBridge";
public static final String WEB_VOICE = "webVoice";
public static final String LEARNING_DASHBOARD_ENABLED = "learningDashboardEnabled";
public static final String LEARNING_DASHBOARD_CLEANUP_DELAY_IN_MINUTES = "learningDashboardCleanupDelayInMinutes";
public static final String WEBCAMS_ONLY_FOR_MODERATOR = "webcamsOnlyForModerator";
public static final String WELCOME = "welcome";
public static final String HTML5_INSTANCE_ID = "html5InstanceId";
@ -81,6 +84,7 @@ public class ApiParams {
// Needed for classes where teacher gets disconnected and can't get back in. Prevents
// students from running amok.
public static final String END_WHEN_NO_MODERATOR = "endWhenNoModerator";
public static final String END_WHEN_NO_MODERATOR_DELAY_IN_MINUTES = "endWhenNoModeratorDelayInMinutes";
private ApiParams() {
throw new IllegalStateException("ApiParams is a utility class. Instanciation is forbidden.");

View File

@ -0,0 +1,94 @@
/**
* 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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileOutputStream;
public class LearningDashboardService {
private static Logger log = LoggerFactory.getLogger(LearningDashboardService.class);
private static String learningDashboardFilesDir = "/var/bigbluebutton/learning-dashboard";
public void writeJsonDataFile(String meetingId, String learningDashboardAccessToken, String activityJson) {
try {
if(learningDashboardAccessToken.length() == 0) {
log.error("LearningDashboard AccessToken not found. JSON file will not be saved for meeting {}.",meetingId);
return;
}
File baseDir = new File(this.getDestinationBaseDirectoryName(meetingId,learningDashboardAccessToken));
if (!baseDir.exists()) baseDir.mkdirs();
File jsonFile = new File(baseDir.getAbsolutePath() + File.separatorChar + "learning_dashboard_data.json");
FileOutputStream fileOutput = new FileOutputStream(jsonFile);
fileOutput.write(activityJson.getBytes());
fileOutput.close();
log.info("Learning Dashboard ({}) updated for meeting {}.",jsonFile.getAbsolutePath(),meetingId);
} catch(Exception e) {
System.out.println(e);
}
}
public void removeJsonDataFile(String meetingId, int cleanUpDelayMinutes) {
//Delay `cleanUpDelayMinutes` then moderators can open the Dashboard before files has been removed
new java.util.Timer().schedule(
new java.util.TimerTask() {
@Override
public void run() {
File ldMeetingFilesDir = new File(learningDashboardFilesDir + File.separatorChar + meetingId);
LearningDashboardService.deleteDirectory(ldMeetingFilesDir);
log.info("Learning Dashboard files removed for meeting {}.",meetingId);
}
},
(cleanUpDelayMinutes * 60) * 1000
);
}
private String getDestinationBaseDirectoryName(String meetingId, String learningDashboardAccessToken) {
return learningDashboardFilesDir + File.separatorChar + meetingId + File.separatorChar + learningDashboardAccessToken;
}
private static void deleteDirectory(File directory) {
/**
* Go through each directory and check if it's not empty. We need to
* delete files inside a directory before a directory can be deleted.
**/
File[] files = directory.listFiles();
for (File file : files) {
if (file.isDirectory()) {
deleteDirectory(file);
} else {
file.delete();
}
}
// Now that the directory is empty. Delete it.
directory.delete();
}
public void setLearningDashboardFilesDir(String dir) {
learningDashboardFilesDir = dir;
}
}

View File

@ -40,6 +40,7 @@ import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import com.google.gson.JsonObject;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.utils.URIBuilder;
import org.bigbluebutton.api.HTML5LoadBalancingService;
@ -49,39 +50,14 @@ import org.bigbluebutton.api.domain.Recording;
import org.bigbluebutton.api.domain.RegisteredUser;
import org.bigbluebutton.api.domain.User;
import org.bigbluebutton.api.domain.UserSession;
import org.bigbluebutton.api.domain.MeetingLayout;
import org.bigbluebutton.api.messaging.MessageListener;
import org.bigbluebutton.api.messaging.converters.messages.DestroyMeetingMessage;
import org.bigbluebutton.api.messaging.converters.messages.EndMeetingMessage;
import org.bigbluebutton.api.messaging.converters.messages.PublishedRecordingMessage;
import org.bigbluebutton.api.messaging.converters.messages.UnpublishedRecordingMessage;
import org.bigbluebutton.api.messaging.converters.messages.DeletedRecordingMessage;
import org.bigbluebutton.api.messaging.messages.AddPad;
import org.bigbluebutton.api.messaging.messages.AddCaptionsPads;
import org.bigbluebutton.api.messaging.messages.CreateBreakoutRoom;
import org.bigbluebutton.api.messaging.messages.CreateMeeting;
import org.bigbluebutton.api.messaging.messages.EndMeeting;
import org.bigbluebutton.api.messaging.messages.GuestPolicyChanged;
import org.bigbluebutton.api.messaging.messages.GuestLobbyMessageChanged;
import org.bigbluebutton.api.messaging.messages.GuestStatusChangedEventMsg;
import org.bigbluebutton.api.messaging.messages.GuestsStatus;
import org.bigbluebutton.api.messaging.messages.IMessage;
import org.bigbluebutton.api.messaging.messages.MakePresentationDownloadableMsg;
import org.bigbluebutton.api.messaging.messages.MeetingDestroyed;
import org.bigbluebutton.api.messaging.messages.MeetingEnded;
import org.bigbluebutton.api.messaging.messages.MeetingStarted;
import org.bigbluebutton.api.messaging.messages.PresentationUploadToken;
import org.bigbluebutton.api.messaging.messages.RecordChapterBreak;
import org.bigbluebutton.api.messaging.messages.RegisterUser;
import org.bigbluebutton.api.messaging.messages.UpdateRecordingStatus;
import org.bigbluebutton.api.messaging.messages.UserJoined;
import org.bigbluebutton.api.messaging.messages.UserJoinedVoice;
import org.bigbluebutton.api.messaging.messages.UserLeft;
import org.bigbluebutton.api.messaging.messages.UserLeftVoice;
import org.bigbluebutton.api.messaging.messages.UserListeningOnly;
import org.bigbluebutton.api.messaging.messages.UserRoleChanged;
import org.bigbluebutton.api.messaging.messages.UserSharedWebcam;
import org.bigbluebutton.api.messaging.messages.UserStatusChanged;
import org.bigbluebutton.api.messaging.messages.UserUnsharedWebcam;
import org.bigbluebutton.api.messaging.messages.*;
import org.bigbluebutton.api2.IBbbWebApiGWApp;
import org.bigbluebutton.api2.domain.UploadedTrack;
import org.bigbluebutton.common2.redis.RedisStorageService;
@ -114,6 +90,7 @@ public class MeetingService implements MessageListener {
private final ConcurrentMap<String, UserSession> sessions;
private RecordingService recordingService;
private LearningDashboardService learningDashboardService;
private WaitingGuestCleanupTimerTask waitingGuestCleaner;
private UserCleanupTimerTask userCleaner;
private EnteredUserCleanupTimerTask enteredUserCleaner;
@ -142,7 +119,7 @@ public class MeetingService implements MessageListener {
public void addUserSession(String token, UserSession user) {
sessions.put(token, user);
}
public String getTokenByUserId(String internalUserId) {
String result = null;
for (Entry<String, UserSession> e : sessions.entrySet()) {
@ -418,6 +395,7 @@ public class MeetingService implements MessageListener {
logData.put("description", "Create meeting.");
logData.put("meetingKeepEvents", m.getMeetingKeepEvents());
logData.put("meetingLayout", m.getMeetingLayout());
Gson gson = new Gson();
String logStr = gson.toJson(logData);
@ -426,13 +404,15 @@ public class MeetingService implements MessageListener {
gw.createMeeting(m.getInternalId(), m.getExternalId(), m.getParentMeetingId(), m.getName(), m.isRecord(),
m.getTelVoice(), m.getDuration(), m.getAutoStartRecording(), m.getAllowStartStopRecording(),
m.getWebcamsOnlyForModerator(), m.getModeratorPassword(), m.getViewerPassword(), m.getCreateTime(),
m.getWebcamsOnlyForModerator(), m.getModeratorPassword(), m.getViewerPassword(),
m.getLearningDashboardEnabled(), m.getLearningDashboardAccessToken(), m.getCreateTime(),
formatPrettyDate(m.getCreateTime()), m.isBreakout(), m.getSequence(), m.isFreeJoin(), m.getMetadata(),
m.getGuestPolicy(), m.getAuthenticatedGuest(), m.getWelcomeMessageTemplate(), m.getWelcomeMessage(), m.getModeratorOnlyMessage(),
m.getGuestPolicy(), m.getAuthenticatedGuest(), m.getMeetingLayout(), m.getWelcomeMessageTemplate(), m.getWelcomeMessage(), m.getModeratorOnlyMessage(),
m.getDialNumber(), m.getMaxUsers(),
m.getMeetingExpireIfNoUserJoinedInMinutes(), m.getmeetingExpireWhenLastUserLeftInMinutes(),
m.getUserInactivityInspectTimerInMinutes(), m.getUserInactivityThresholdInMinutes(),
m.getUserActivitySignResponseDelayInMinutes(), m.getMuteOnStart(), m.getAllowModsToUnmuteUsers(), m.getMeetingKeepEvents(),
m.getUserActivitySignResponseDelayInMinutes(), m.getEndWhenNoModerator(), m.getEndWhenNoModeratorDelayInMinutes(),
m.getMuteOnStart(), m.getAllowModsToUnmuteUsers(), m.getMeetingKeepEvents(),
m.breakoutRoomsParams,
m.lockSettingsParams, m.getHtml5InstanceId());
}
@ -580,7 +560,7 @@ public class MeetingService implements MessageListener {
public String getCaptionTrackInboxDir() {
return recordingService.getCaptionTrackInboxDir();
}
public String getCaptionsDir() {
return recordingService.getCaptionsDir();
}
@ -694,7 +674,7 @@ public class MeetingService implements MessageListener {
log.error(" --analytics-- data={}", logStr);
}
}
private void processUpdateRecordingStatus(UpdateRecordingStatus message) {
Meeting m = getMeeting(message.meetingId);
// Set only once
@ -879,6 +859,11 @@ public class MeetingService implements MessageListener {
}
}
//Remove Learning Dashboard files
if(m.getLearningDashboardCleanupDelayInMinutes() > 0) {
learningDashboardService.removeJsonDataFile(message.meetingId, m.getLearningDashboardCleanupDelayInMinutes());
}
processRemoveEndedMeeting(message);
}
}
@ -973,6 +958,26 @@ public class MeetingService implements MessageListener {
}
}
public void processLearningDashboard(LearningDashboard message) {
//Get all data from Json instead of getMeeting(message.meetingId), to process messages received even after meeting ended
JsonObject activityJsonObject = new Gson().fromJson(message.activityJson, JsonObject.class).getAsJsonObject();
String learningDashboardAccessToken = activityJsonObject.get("learningDashboardAccessToken").getAsString();
Map<String, Object> logData = new HashMap<String, Object>();
logData.put("meetingId", activityJsonObject.get("intId").getAsString());
logData.put("externalMeetingId", activityJsonObject.get("extId").getAsString());
logData.put("name", activityJsonObject.get("name").getAsString());
logData.put("logCode", "update_activity_json");
logData.put("description", "Updating activities json.");
Gson gson = new Gson();
String logStr = gson.toJson(logData);
log.info(" --analytics-- data={}", logStr);
learningDashboardService.writeJsonDataFile(message.meetingId, learningDashboardAccessToken, message.activityJson);
}
@Override
public void handle(IMessage message) {
receivedMessages.add(message);
@ -1129,6 +1134,8 @@ public class MeetingService implements MessageListener {
processMakePresentationDownloadableMsg((MakePresentationDownloadableMsg) message);
} else if (message instanceof UpdateRecordingStatus) {
processUpdateRecordingStatus((UpdateRecordingStatus) message);
} else if (message instanceof LearningDashboard) {
processLearningDashboard((LearningDashboard) message);
}
}
};
@ -1214,6 +1221,10 @@ public class MeetingService implements MessageListener {
recordingService = s;
}
public void setLearningDashboardService(LearningDashboardService s) {
learningDashboardService = s;
}
public void setRedisStorageService(RedisStorageService mess) {
storeService = mess;
}

View File

@ -1,13 +1,13 @@
/**
* 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.
@ -38,8 +38,6 @@ import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.bigbluebutton.api.domain.BreakoutRoomsParams;
import org.bigbluebutton.api.domain.LockSettingsParams;
@ -54,7 +52,7 @@ public class ParamsProcessorUtil {
private static final String URLDECODER_SEPARATOR=",";
private static final String FILTERDECODER_SEPARATOR_ELEMENTS=":";
private static final String FILTERDECODER_SEPARATOR_OPERATORS="\\|";
private static final String SERVER_URL = "%%SERVERURL%%";
private static final String DIAL_NUM = "%%DIALNUM%%";
private static final String CONF_NUM = "%%CONFNUM%%";
@ -77,17 +75,21 @@ public class ParamsProcessorUtil {
private Boolean allowRequestsWithoutSession;
private Boolean useDefaultAvatar = false;
private String defaultAvatarURL;
private String defaultConfigURL;
private String defaultGuestPolicy;
private Boolean authenticatedGuest;
private String defaultMeetingLayout;
private int defaultMeetingDuration;
private boolean disableRecordingDefault;
private boolean autoStartRecording;
private boolean allowStartStopRecording;
private boolean learningDashboardEnabled;
private int learningDashboardCleanupDelayInMinutes;
private boolean webcamsOnlyForModerator;
private boolean defaultMuteOnStart = false;
private boolean defaultAllowModsToUnmuteUsers = false;
private boolean defaultKeepEvents = false;
private Boolean useDefaultLogo;
private String defaultLogoURL;
private boolean defaultBreakoutRoomsEnabled;
private boolean defaultBreakoutRoomsRecord;
@ -103,8 +105,6 @@ public class ParamsProcessorUtil {
private boolean defaultLockSettingsLockOnJoin;
private boolean defaultLockSettingsLockOnJoinConfigurable;
private String defaultConfigXML = null;
private Long maxPresentationFileUpload = 30000000L; // 30MB
private Integer clientLogoutTimerInMinutes = 0;
@ -115,6 +115,7 @@ public class ParamsProcessorUtil {
private Integer userActivitySignResponseDelayInMinutes = 5;
private Boolean defaultAllowDuplicateExtUserid = true;
private Boolean defaultEndWhenNoModerator = false;
private Integer defaultEndWhenNoModeratorDelayInMinutes = 1;
private Integer defaultHtml5InstanceId = 1;
private String formatConfNum(String s) {
@ -154,7 +155,7 @@ public class ParamsProcessorUtil {
} else if (keyword.equals(CONF_NAME)) {
welcomeMessage = welcomeMessage.replaceAll(
Pattern.quote(CONF_NAME),
Matcher.quoteReplacement(meetingName));
Matcher.quoteReplacement(ParamsUtil.escapeHTMLTags(meetingName)));
} else if (keyword.equals(SERVER_URL)) {
welcomeMessage = welcomeMessage.replaceAll(
Pattern.quote(SERVER_URL),
@ -184,10 +185,10 @@ public class ParamsProcessorUtil {
errors.missingParamError(ApiParams.MEETING_ID);
}
}
public Map<String, Object> processUpdateCreateParams(Map<String, String> params) {
Map<String, Object> newParams = new HashMap<>();
String[] createParams = { ApiParams.NAME, ApiParams.ATTENDEE_PW, ApiParams.MODERATOR_PW, ApiParams.VOICE_BRIDGE,
ApiParams.WEB_VOICE, ApiParams.DIAL_NUMBER, ApiParams.LOGOUT_URL, ApiParams.RECORD,
ApiParams.MAX_PARTICIPANTS, ApiParams.DURATION, ApiParams.WELCOME };
@ -198,7 +199,7 @@ public class ParamsProcessorUtil {
newParams.put(paramName, parameter);
}
}
// Collect metadata for this meeting that the third-party application wants to store if meeting is recorded.
Map<String, String> meetingInfo = new HashMap<>();
for (Map.Entry<String, String> entry : params.entrySet()) {
@ -207,17 +208,17 @@ public class ParamsProcessorUtil {
if(meta.length == 2){
meetingInfo.put(meta[1], entry.getValue());
}
}
}
}
if (!meetingInfo.isEmpty()) {
newParams.put("metadata", meetingInfo);
}
return newParams;
}
private static final Pattern META_VAR_PATTERN = Pattern.compile("meta_[a-zA-Z][a-zA-Z0-9-]*$");
private static final Pattern META_VAR_PATTERN = Pattern.compile("meta_[a-zA-Z][a-zA-Z0-9-]*$");
public static Boolean isMetaValid(String param) {
Matcher metaMatcher = META_VAR_PATTERN.matcher(param);
if (metaMatcher.matches()) {
@ -225,11 +226,11 @@ public class ParamsProcessorUtil {
}
return false;
}
public static String removeMetaString(String param) {
return StringUtils.removeStart(param, "meta_");
}
public static Map<String, String> processMetaParam(Map<String, String> params) {
Map<String, String> metas = new HashMap<>();
for (Map.Entry<String, String> entry : params.entrySet()) {
@ -341,7 +342,7 @@ public class ParamsProcessorUtil {
meetingName = "";
}
meetingName = ParamsUtil.stripHTMLTags(ParamsUtil.stripControlChars(meetingName));
meetingName = ParamsUtil.stripControlChars(meetingName);
String externalMeetingId = params.get(ApiParams.MEETING_ID);
@ -371,11 +372,11 @@ public class ParamsProcessorUtil {
int maxUsers = processMaxUser(params.get(ApiParams.MAX_PARTICIPANTS));
int meetingDuration = processMeetingDuration(params.get(ApiParams.DURATION));
int logoutTimer = processLogoutTimer(params.get(ApiParams.LOGOUT_TIMER));
// Banner parameters
String bannerText = params.get(ApiParams.BANNER_TEXT);
String bannerColor = params.get(ApiParams.BANNER_COLOR);
// set is breakout room property
boolean isBreakout = false;
if (!StringUtils.isEmpty(params.get(ApiParams.IS_BREAKOUT))) {
@ -418,6 +419,36 @@ public class ParamsProcessorUtil {
}
}
boolean learningDashboardEn = learningDashboardEnabled;
if (!StringUtils.isEmpty(params.get(ApiParams.LEARNING_DASHBOARD_ENABLED))) {
try {
learningDashboardEn = Boolean.parseBoolean(params
.get(ApiParams.LEARNING_DASHBOARD_ENABLED));
} catch (Exception ex) {
log.warn(
"Invalid param [learningDashboardEnabled] for meeting=[{}]",
internalMeetingId);
}
}
int learningDashboardCleanupMins = learningDashboardCleanupDelayInMinutes;
if (!StringUtils.isEmpty(params.get(ApiParams.LEARNING_DASHBOARD_CLEANUP_DELAY_IN_MINUTES))) {
try {
learningDashboardCleanupMins = Integer.parseInt(params
.get(ApiParams.LEARNING_DASHBOARD_CLEANUP_DELAY_IN_MINUTES));
} catch (Exception ex) {
log.warn(
"Invalid param [learningDashboardCleanupDelayInMinutes] for meeting=[{}]",
internalMeetingId);
}
}
//Generate token to access Activity Report
String learningDashboardAccessToken = "";
if(learningDashboardEn == true) {
learningDashboardAccessToken = RandomStringUtils.randomAlphanumeric(12).toLowerCase();
}
boolean webcamsOnlyForMod = webcamsOnlyForModerator;
if (!StringUtils.isEmpty(params.get(ApiParams.WEBCAMS_ONLY_FOR_MODERATOR))) {
try {
@ -439,15 +470,29 @@ public class ParamsProcessorUtil {
}
}
int endWhenNoModeratorDelayInMinutes = defaultEndWhenNoModeratorDelayInMinutes;
if (!StringUtils.isEmpty(params.get(ApiParams.END_WHEN_NO_MODERATOR_DELAY_IN_MINUTES))) {
try {
endWhenNoModeratorDelayInMinutes = Integer.parseInt(params.get(ApiParams.END_WHEN_NO_MODERATOR_DELAY_IN_MINUTES));
} catch (Exception ex) {
log.warn("Invalid param [endWhenNoModeratorDelayInMinutes] for meeting=[{}]", internalMeetingId);
}
}
String guestPolicy = defaultGuestPolicy;
if (!StringUtils.isEmpty(params.get(ApiParams.GUEST_POLICY))) {
guestPolicy = params.get(ApiParams.GUEST_POLICY);
}
}
String meetingLayout = defaultMeetingLayout;
if (!StringUtils.isEmpty(params.get(ApiParams.MEETING_LAYOUT))) {
meetingLayout = params.get(ApiParams.MEETING_LAYOUT);
}
BreakoutRoomsParams breakoutParams = processBreakoutRoomsParams(params);
LockSettingsParams lockSettingsParams = processLockSettingsParams(params);
// Collect metadata for this meeting that the third-party app wants to
// store if meeting is recorded.
Map<String, String> meetingInfo = processMetaParam(params);
@ -496,15 +541,16 @@ public class ParamsProcessorUtil {
.withWelcomeMessage(welcomeMessage).isBreakout(isBreakout)
.withGuestPolicy(guestPolicy)
.withAuthenticatedGuest(authenticatedGuest)
.withMeetingLayout(meetingLayout)
.withBreakoutRoomsParams(breakoutParams)
.withLockSettingsParams(lockSettingsParams)
.withAllowDuplicateExtUserid(defaultAllowDuplicateExtUserid)
.withHTML5InstanceId(html5InstanceId)
.withLearningDashboardEnabled(learningDashboardEn)
.withLearningDashboardCleanupDelayInMinutes(learningDashboardCleanupMins)
.withLearningDashboardAccessToken(learningDashboardAccessToken)
.build();
String configXML = getDefaultConfigXML();
meeting.storeConfig(true, configXML);
if (!StringUtils.isEmpty(params.get(ApiParams.MODERATOR_ONLY_MESSAGE))) {
String moderatorOnlyMessageTemplate = params.get(ApiParams.MODERATOR_ONLY_MESSAGE);
String moderatorOnlyMessage = substituteKeywords(moderatorOnlyMessageTemplate,
@ -523,6 +569,8 @@ public class ParamsProcessorUtil {
meeting.setUserActivitySignResponseDelayInMinutes(userActivitySignResponseDelayInMinutes);
meeting.setUserInactivityThresholdInMinutes(userInactivityThresholdInMinutes);
// meeting.setHtml5InstanceId(html5InstanceId);
meeting.setEndWhenNoModerator(endWhenNoModerator);
meeting.setEndWhenNoModeratorDelayInMinutes(endWhenNoModeratorDelayInMinutes);
// Add extra parameters for breakout room
if (isBreakout) {
@ -533,6 +581,8 @@ public class ParamsProcessorUtil {
if (!StringUtils.isEmpty(params.get(ApiParams.LOGO))) {
meeting.setCustomLogoURL(params.get(ApiParams.LOGO));
} else if (this.getUseDefaultLogo()) {
meeting.setCustomLogoURL(this.getDefaultLogoURL());
}
if (!StringUtils.isEmpty(params.get(ApiParams.COPYRIGHT))) {
@ -543,6 +593,12 @@ public class ParamsProcessorUtil {
muteOnStart = Boolean.parseBoolean(params.get(ApiParams.MUTE_ON_START));
}
// when a moderator joins in a breakout room only with the audio, and the muteOnStart is set to true,
// the moderator is unable to unmute himself, because they don't have an icon to do so
if (isBreakout) {
muteOnStart = false;
}
meeting.setMuteOnStart(muteOnStart);
Boolean meetingKeepEvents = defaultKeepEvents;
@ -559,15 +615,15 @@ public class ParamsProcessorUtil {
return meeting;
}
public String getApiVersion() {
return apiVersion;
}
public boolean isServiceEnabled() {
return serviceEnabled;
}
public String getDefaultHTML5ClientUrl() {
return defaultHTML5ClientUrl;
}
@ -576,58 +632,18 @@ public class ParamsProcessorUtil {
return defaultGuestWaitURL;
}
public Boolean getUseDefaultLogo() {
return useDefaultLogo;
}
public String getDefaultLogoURL() {
return defaultLogoURL;
}
public Boolean getAllowRequestsWithoutSession() {
return allowRequestsWithoutSession;
}
public String getDefaultConfigXML() {
defaultConfigXML = getConfig(defaultConfigURL);
return defaultConfigXML;
}
private String getConfig(String url) {
String configXML = "";
CloseableHttpClient httpclient = HttpClients.createDefault();
try {
HttpGet httpget = new HttpGet(url);
// Create a custom response handler
ResponseHandler<String> responseHandler = new ResponseHandler<String>() {
@Override
public String handleResponse(
final HttpResponse response) throws IOException {
int status = response.getStatusLine().getStatusCode();
if (status >= 200 && status < 300) {
HttpEntity entity = response.getEntity();
return entity != null ? EntityUtils.toString(entity, StandardCharsets.UTF_8) : null;
} else {
throw new ClientProtocolException("Unexpected response status: " + status);
}
}
};
String responseBody = httpclient.execute(httpget, responseHandler);
configXML = responseBody;
} catch(IOException ex) {
// IOException
} finally {
try {
httpclient.close();
} catch(IOException ex) {
// do nothing
}
}
return configXML;
}
public String getDefaultConfigURL() {
return defaultConfigURL;
}
public String getDefaultLogoutUrl() {
if ((StringUtils.isEmpty(defaultLogoutUrl)) || "default".equalsIgnoreCase(defaultLogoutUrl)) {
return defaultServerUrl;
@ -635,7 +651,7 @@ public class ParamsProcessorUtil {
return defaultLogoutUrl;
}
}
public String processWelcomeMessage(String message, Boolean isBreakout) {
String welcomeMessage = message;
if (StringUtils.isEmpty(message)) {
@ -649,7 +665,7 @@ public class ParamsProcessorUtil {
public String convertToInternalMeetingId(String extMeetingId) {
return DigestUtils.sha1Hex(extMeetingId);
}
public String processPassword(String pass) {
return StringUtils.isEmpty(pass) ? RandomStringUtils.randomAlphanumeric(8) : pass;
}
@ -661,39 +677,39 @@ public class ParamsProcessorUtil {
public String processTelVoice(String telNum) {
return StringUtils.isEmpty(telNum) ? RandomStringUtils.randomNumeric(defaultNumDigitsForTelVoice) : telNum;
}
public String processDialNumber(String dial) {
return StringUtils.isEmpty(dial) ? defaultDialAccessNumber : dial;
return StringUtils.isEmpty(dial) ? defaultDialAccessNumber : dial;
}
public String processLogoutUrl(String logoutUrl) {
if (StringUtils.isEmpty(logoutUrl)) {
if ((StringUtils.isEmpty(defaultLogoutUrl)) || "default".equalsIgnoreCase(defaultLogoutUrl)) {
return defaultServerUrl;
} else {
return defaultLogoutUrl;
}
}
}
return logoutUrl;
}
public boolean processRecordMeeting(String record) {
// The administrator has turned off recording for all meetings.
if (disableRecordingDefault) {
log.info("Recording is turned OFF by default.");
return false;
}
boolean rec = false;
boolean rec = false;
if(! StringUtils.isEmpty(record)){
try {
rec = Boolean.parseBoolean(record);
} catch(Exception ex){
} catch(Exception ex){
rec = false;
}
}
return rec;
}
@ -707,28 +723,28 @@ public class ParamsProcessorUtil {
return html5InstanceId;
}
public int processMaxUser(String maxUsers) {
int mUsers = -1;
try {
mUsers = Integer.parseInt(maxUsers);
} catch(Exception ex) {
} catch(Exception ex) {
mUsers = defaultMaxUsers;
}
}
return mUsers;
}
}
public int processMeetingDuration(String duration) {
int mDuration = -1;
try {
mDuration = Integer.parseInt(duration);
} catch(Exception ex) {
} catch(Exception ex) {
mDuration = defaultMeetingDuration;
}
}
return mDuration;
}
@ -748,7 +764,7 @@ public class ParamsProcessorUtil {
return ((!StringUtils.isEmpty(telVoice)) && (!StringUtils.isEmpty(testVoiceBridge))
&& (telVoice.equals(testVoiceBridge)));
}
public String getIntMeetingIdForTestMeeting(String telVoice) {
if ((testVoiceBridge != null) && (testVoiceBridge.equals(telVoice))
&& StringUtils.isEmpty(testConferenceMock)) {
@ -757,29 +773,8 @@ public class ParamsProcessorUtil {
return "";
}
public boolean isConfigXMLChecksumSame(String meetingID, String configXML, String checksum) {
if (StringUtils.isEmpty(securitySalt)) {
log.warn("Security is disabled in this service. Make sure this is intentional.");
return true;
}
log.info("CONFIGXML CHECKSUM={} length={}", checksum, checksum.length());
String data = meetingID + configXML + securitySalt;
String cs = DigestUtils.sha1Hex(data);
if (checksum.length() == 64) {
cs = DigestUtils.sha256Hex(data);
log.info("CONFIGXML SHA256 {}", cs);
}
if (cs == null || !cs.equals(checksum)) {
log.info("checksumError: configXML checksum. our: [{}], client: [{}]", cs, checksum);
return false;
}
return true;
}
// Can be removed. Checksum validation is performed by the ChecksumValidator
public boolean isChecksumSame(String apiCall, String checksum, String queryString) {
if (StringUtils.isEmpty(securitySalt)) {
log.warn("Security is disabled in this service. Make sure this is intentional.");
@ -810,9 +805,9 @@ public class ParamsProcessorUtil {
return false;
}
return true;
return true;
}
public boolean isPostChecksumSame(String apiCall, Map<String, String[]> params) {
if (StringUtils.isEmpty(securitySalt)) {
log.warn("Security is disabled in this service. Make sure this is intentional.");
@ -821,9 +816,9 @@ public class ParamsProcessorUtil {
StringBuilder csbuf = new StringBuilder();
csbuf.append(apiCall);
SortedSet<String> keys = new TreeSet<>(params.keySet());
boolean first = true;
String checksum = null;
for (String key: keys) {
@ -832,7 +827,7 @@ public class ParamsProcessorUtil {
checksum = params.get(key)[0];
continue;
}
for (String value: params.get(key)) {
if (first) {
first = false;
@ -844,26 +839,26 @@ public class ParamsProcessorUtil {
String encResult;
encResult = value;
/*****
* Seems like Grails 2.3.6 decodes the string. So we need to re-encode it.
* We'll remove this later. richard (aug 5, 2014)
*/ try {
* We'll remove this later. richard (aug 5, 2014)
*/ try {
// we need to re-encode the values because Grails unencoded it
// when it received the 'POST'ed data. Might not need to do in a GET request.
encResult = URLEncoder.encode(value, StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
encResult = value;
}
encResult = URLEncoder.encode(value, StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
encResult = value;
}
csbuf.append(encResult);
}
}
csbuf.append(securitySalt);
String baseString = csbuf.toString();
String baseString = csbuf.toString();
String cs = DigestUtils.sha1Hex(baseString);
if (cs == null || !cs.equals(checksum)) {
log.info("POST basestring = {}", baseString);
log.info("checksumError: failed checksum. our checksum: [{}], client: [{}]", cs, checksum);
@ -876,7 +871,7 @@ public class ParamsProcessorUtil {
/*************************************************
* Setters
************************************************/
public void setApiVersion(String apiVersion) {
this.apiVersion = apiVersion;
}
@ -884,7 +879,7 @@ public class ParamsProcessorUtil {
public void setServiceEnabled(boolean e) {
serviceEnabled = e;
}
public void setSecuritySalt(String securitySalt) {
this.securitySalt = securitySalt;
}
@ -896,7 +891,7 @@ public class ParamsProcessorUtil {
public void setDefaultWelcomeMessage(String defaultWelcomeMessage) {
this.defaultWelcomeMessage = defaultWelcomeMessage;
}
public void setDefaultWelcomeMessageFooter(String defaultWelcomeMessageFooter) {
this.defaultWelcomeMessageFooter = defaultWelcomeMessageFooter;
}
@ -917,10 +912,6 @@ public class ParamsProcessorUtil {
this.defaultLogoutUrl = defaultLogoutUrl;
}
public void setDefaultConfigURL(String defaultConfigUrl) {
this.defaultConfigURL = defaultConfigUrl;
}
public void setDefaultServerUrl(String defaultServerUrl) {
this.defaultServerUrl = defaultServerUrl;
}
@ -937,6 +928,14 @@ public class ParamsProcessorUtil {
this.defaultGuestWaitURL = url;
}
public void setUseDefaultLogo(Boolean value) {
this.useDefaultLogo = value;
}
public void setDefaultLogoURL(String url) {
this.defaultLogoURL = url;
}
public void setAllowRequestsWithoutSession(Boolean allowRequestsWithoutSession) {
this.allowRequestsWithoutSession = allowRequestsWithoutSession;
}
@ -948,7 +947,7 @@ public class ParamsProcessorUtil {
public void setDisableRecordingDefault(boolean disabled) {
this.disableRecordingDefault = disabled;
}
public void setAutoStartRecording(boolean start) {
this.autoStartRecording = start;
}
@ -956,11 +955,19 @@ public class ParamsProcessorUtil {
public void setAllowStartStopRecording(boolean allowStartStopRecording) {
this.allowStartStopRecording = allowStartStopRecording;
}
public void setLearningDashboardEnabled(boolean learningDashboardEnabled) {
this.learningDashboardEnabled = learningDashboardEnabled;
}
public void setlearningDashboardCleanupDelayInMinutes(int learningDashboardCleanupDelayInMinutes) {
this.learningDashboardCleanupDelayInMinutes = learningDashboardCleanupDelayInMinutes;
}
public void setWebcamsOnlyForModerator(boolean webcamsOnlyForModerator) {
this.webcamsOnlyForModerator = webcamsOnlyForModerator;
}
public void setUseDefaultAvatar(Boolean value) {
this.useDefaultAvatar = value;
}
@ -977,6 +984,10 @@ public class ParamsProcessorUtil {
this.authenticatedGuest = value;
}
public void setDefaultMeetingLayout(String meetingLayout) {
this.defaultMeetingLayout = meetingLayout;
}
public void setClientLogoutTimerInMinutes(Integer value) {
clientLogoutTimerInMinutes = value;
}
@ -992,7 +1003,7 @@ public class ParamsProcessorUtil {
public void setMeetingExpireIfNoUserJoinedInMinutes(Integer value) {
meetingExpireIfNoUserJoinedInMinutes = value;
}
public Integer getUserInactivityInspectTimerInMinutes() {
return userInactivityInspectTimerInMinutes;
}
@ -1000,7 +1011,7 @@ public class ParamsProcessorUtil {
public void setUserInactivityInspectTimerInMinutes(Integer userInactivityInspectTimerInMinutes) {
this.userInactivityInspectTimerInMinutes = userInactivityInspectTimerInMinutes;
}
public Integer getUserInactivityThresholdInMinutes() {
return userInactivityThresholdInMinutes;
}
@ -1052,7 +1063,7 @@ public class ParamsProcessorUtil {
} catch (UnsupportedEncodingException e) {
log.error("Couldn't decode the IDs");
}
return ids;
}
@ -1063,7 +1074,7 @@ public class ParamsProcessorUtil {
}
return internalMeetingIds;
}
public Map<String, String> getUserCustomData(Map<String, String> params) {
Map<String, String> resp = new HashMap<>();
@ -1156,5 +1167,8 @@ public class ParamsProcessorUtil {
this.defaultEndWhenNoModerator = val;
}
public void setEndWhenNoModeratorDelayInMinutes(Integer value) {
this.defaultEndWhenNoModeratorDelayInMinutes = value;
}
}

View File

@ -1,13 +1,13 @@
/**
* 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.
@ -42,7 +42,7 @@ public class Meeting {
private String parentMeetingId = "bbb-none"; // Initialize so we don't send null in the json message.
private Integer sequence = 0;
private Boolean freeJoin = false;
private Integer duration = 0;
private Integer duration = 0;
private long createdTime = 0;
private long startTime = 0;
private long endTime = 0;
@ -51,6 +51,9 @@ public class Meeting {
private String webVoice;
private String moderatorPass;
private String viewerPass;
private Boolean learningDashboardEnabled;
private int learningDashboardCleanupDelayInMinutes;
private String learningDashboardAccessToken;
private String welcomeMsgTemplate;
private String welcomeMsg;
private String modOnlyMessage = "";
@ -66,10 +69,10 @@ public class Meeting {
private boolean webcamsOnlyForModerator = false;
private String dialNumber;
private String defaultAvatarURL;
private String defaultConfigToken;
private String guestPolicy = GuestPolicy.ASK_MODERATOR;
private String guestLobbyMessage = "";
private Boolean authenticatedGuest = false;
private String meetingLayout = MeetingLayout.SMART_LAYOUT;
private boolean userHasJoined = false;
private Map<String, String> pads;
private Map<String, String> metadata;
@ -77,7 +80,6 @@ public class Meeting {
private final ConcurrentMap<String, User> users;
private final ConcurrentMap<String, RegisteredUser> registeredUsers;
private final ConcurrentMap<String, Long> enteredUsers;
private final ConcurrentMap<String, Config> configs;
private final Boolean isBreakout;
private final List<String> breakoutRooms = new ArrayList<>();
private String customLogoURL = "";
@ -91,6 +93,8 @@ public class Meeting {
private Integer userInactivityInspectTimerInMinutes = 120;
private Integer userInactivityThresholdInMinutes = 30;
private Integer userActivitySignResponseDelayInMinutes = 5;
private Boolean endWhenNoModerator = false;
private Integer endWhenNoModeratorDelayInMinutes = 1;
public final BreakoutRoomsParams breakoutRoomsParams;
public final LockSettingsParams lockSettingsParams;
@ -99,8 +103,6 @@ public class Meeting {
private String meetingEndedCallbackURL = "";
public final Boolean endWhenNoModerator;
private Integer html5InstanceId;
public Meeting(Meeting.Builder builder) {
@ -109,6 +111,9 @@ public class Meeting {
intMeetingId = builder.internalId;
viewerPass = builder.viewerPass;
moderatorPass = builder.moderatorPass;
learningDashboardEnabled = builder.learningDashboardEnabled;
learningDashboardCleanupDelayInMinutes = builder.learningDashboardCleanupDelayInMinutes;
learningDashboardAccessToken = builder.learningDashboardAccessToken;
maxUsers = builder.maxUsers;
bannerColor = builder.bannerColor;
bannerText = builder.bannerText;
@ -130,10 +135,12 @@ public class Meeting {
isBreakout = builder.isBreakout;
guestPolicy = builder.guestPolicy;
authenticatedGuest = builder.authenticatedGuest;
meetingLayout = builder.meetingLayout;
breakoutRoomsParams = builder.breakoutRoomsParams;
lockSettingsParams = builder.lockSettingsParams;
allowDuplicateExtUserid = builder.allowDuplicateExtUserid;
endWhenNoModerator = builder.endWhenNoModerator;
endWhenNoModeratorDelayInMinutes = builder.endWhenNoModeratorDelayInMinutes;
html5InstanceId = builder.html5InstanceId;
/*
@ -147,9 +154,7 @@ public class Meeting {
users = new ConcurrentHashMap<>();
registeredUsers = new ConcurrentHashMap<>();
enteredUsers = new ConcurrentHashMap<>();;
configs = new ConcurrentHashMap<>();
enteredUsers = new ConcurrentHashMap<>();
}
public void addBreakoutRoom(String meetingId) {
@ -160,37 +165,6 @@ public class Meeting {
return breakoutRooms;
}
public String storeConfig(boolean defaultConfig, String config) {
String token = RandomStringUtils.randomAlphanumeric(8);
while (configs.containsKey(token)) {
token = RandomStringUtils.randomAlphanumeric(8);
}
configs.put(token, new Config(token, System.currentTimeMillis(), config));
if (defaultConfig) {
defaultConfigToken = token;
}
return token;
}
public Config getDefaultConfig() {
if (defaultConfigToken != null) {
return getConfig(defaultConfigToken);
}
return null;
}
public Config getConfig(String token) {
return configs.get(token);
}
public Config removeConfig(String token) {
return configs.remove(token);
}
public Map<String, String> getPads() {
return pads;
}
@ -253,11 +227,11 @@ public class Meeting {
public long getStartTime() {
return startTime;
}
public void setStartTime(long t) {
startTime = t;
}
public long getCreateTime() {
return createdTime;
}
@ -277,35 +251,35 @@ public class Meeting {
public void setFreeJoin(Boolean freeJoin) {
this.freeJoin = freeJoin;
}
public Integer getDuration() {
return duration;
}
public long getEndTime() {
return endTime;
}
public void setModeratorOnlyMessage(String msg) {
modOnlyMessage = msg;
}
public String getModeratorOnlyMessage() {
return modOnlyMessage;
}
public void setEndTime(long t) {
endTime = t;
}
public boolean isRunning() {
return ! users.isEmpty();
}
public Boolean isBreakout() {
return isBreakout;
}
public void setHaveRecordingMarks(boolean marks) {
haveRecordingMarks = marks;
}
@ -313,7 +287,7 @@ public class Meeting {
public boolean haveRecordingMarks() {
return haveRecordingMarks;
}
public String getName() {
return name;
}
@ -329,7 +303,7 @@ public class Meeting {
public String getExternalId() {
return extMeetingId;
}
public String getInternalId() {
return intMeetingId;
}
@ -357,10 +331,22 @@ public class Meeting {
public String getViewerPassword() {
return viewerPass;
}
public String getWelcomeMessageTemplate() {
return welcomeMsgTemplate;
}
public Boolean getLearningDashboardEnabled() {
return learningDashboardEnabled;
}
public int getLearningDashboardCleanupDelayInMinutes() {
return learningDashboardCleanupDelayInMinutes;
}
public String getLearningDashboardAccessToken() {
return learningDashboardAccessToken;
}
public String getWelcomeMessageTemplate() {
return welcomeMsgTemplate;
}
public String getWelcomeMessage() {
return welcomeMsg;
@ -394,6 +380,14 @@ public class Meeting {
return authenticatedGuest;
}
public void setMeetingLayout(String layout) {
meetingLayout = layout;
}
public String getMeetingLayout() {
return meetingLayout;
}
private String getUnauthenticatedGuestStatus(Boolean guest) {
if (guest) {
switch(guestPolicy) {
@ -447,15 +441,15 @@ public class Meeting {
public int getMaxUsers() {
return maxUsers;
}
public int getLogoutTimer() {
return logoutTimer;
}
public String getBannerColor() {
return bannerColor;
}
public String getBannerText() {
return bannerText;
}
@ -463,19 +457,19 @@ public class Meeting {
public boolean isRecord() {
return record;
}
public boolean getAutoStartRecording() {
return autoStartRecording;
}
public boolean getAllowStartStopRecording() {
return allowStartStopRecording;
}
public boolean getWebcamsOnlyForModerator() {
return webcamsOnlyForModerator;
}
public boolean hasUserJoined() {
return userHasJoined;
}
@ -559,11 +553,11 @@ public class Meeting {
return null;
}
public int getNumUsers(){
return this.users.size();
}
public int getNumModerators() {
int sum = 0;
for (Map.Entry<String, User> entry : users.entrySet()) {
@ -573,7 +567,7 @@ public class Meeting {
}
return sum;
}
public String getDialNumber() {
return dialNumber;
}
@ -587,7 +581,7 @@ public class Meeting {
}
return sum;
}
public int getNumVoiceJoined() {
int sum = 0;
for (Map.Entry<String, User> entry : users.entrySet()) {
@ -606,7 +600,7 @@ public class Meeting {
}
return sum;
}
public void addPad(String padId, String readOnlyId) {
pads.put(padId, readOnlyId);
}
@ -635,7 +629,7 @@ public class Meeting {
public Integer getMeetingExpireIfNoUserJoinedInMinutes() {
return meetingExpireIfNoUserJoinedInMinutes;
}
public Integer getUserInactivityInspectTimerInMinutes() {
return userInactivityInspectTimerInMinutes;
}
@ -643,7 +637,7 @@ public class Meeting {
public void setUserInactivityInspectTimerInMinutes(Integer userInactivityInjspectTimerInMinutes) {
this.userInactivityInspectTimerInMinutes = userInactivityInjspectTimerInMinutes;
}
public Integer getUserInactivityThresholdInMinutes() {
return userInactivityThresholdInMinutes;
}
@ -660,6 +654,22 @@ public class Meeting {
this.userActivitySignResponseDelayInMinutes = userActivitySignResponseDelayInMinutes;
}
public Boolean getEndWhenNoModerator() {
return endWhenNoModerator;
}
public void setEndWhenNoModerator(Boolean endWhenNoModerator) {
this.endWhenNoModerator = endWhenNoModerator;
}
public Integer getEndWhenNoModeratorDelayInMinutes() {
return endWhenNoModeratorDelayInMinutes;
}
public void setEndWhenNoModeratorDelayInMinutes(Integer endWhenNoModeratorDelayInMinutes) {
this.endWhenNoModeratorDelayInMinutes = endWhenNoModeratorDelayInMinutes;
}
public String getMeetingEndedCallbackURL() {
return meetingEndedCallbackURL;
}
@ -722,6 +732,9 @@ public class Meeting {
private boolean webcamsOnlyForModerator;
private String moderatorPass;
private String viewerPass;
private Boolean learningDashboardEnabled;
private int learningDashboardCleanupDelayInMinutes;
private String learningDashboardAccessToken;
private int duration;
private String webVoice;
private String telVoice;
@ -738,10 +751,12 @@ public class Meeting {
private boolean isBreakout;
private String guestPolicy;
private Boolean authenticatedGuest;
private String meetingLayout;
private BreakoutRoomsParams breakoutRoomsParams;
private LockSettingsParams lockSettingsParams;
private Boolean allowDuplicateExtUserid;
private Boolean endWhenNoModerator;
private Integer endWhenNoModeratorDelayInMinutes;
private int html5InstanceId;
public Builder(String externalId, String internalId, long createTime) {
@ -749,7 +764,7 @@ public class Meeting {
this.internalId = internalId;
this.createdTime = createTime;
}
public Builder withName(String name) {
this.name = name;
return this;
@ -759,17 +774,17 @@ public class Meeting {
duration = minutes;
return this;
}
public Builder withMaxUsers(int n) {
maxUsers = n;
return this;
}
public Builder withRecording(boolean record) {
this.record = record;
return this;
}
public Builder withAutoStartRecording(boolean start) {
this.autoStartRecording = start;
return this;
@ -779,37 +794,52 @@ public class Meeting {
this.allowStartStopRecording = allow;
return this;
}
public Builder withWebcamsOnlyForModerator(boolean only) {
this.webcamsOnlyForModerator = only;
return this;
}
public Builder withWebVoice(String w) {
this.webVoice = w;
return this;
}
public Builder withTelVoice(String t) {
this.telVoice = t;
return this;
}
public Builder withDialNumber(String d) {
this.dialNumber = d;
return this;
}
public Builder withModeratorPass(String p) {
this.moderatorPass = p;
return this;
}
public Builder withViewerPass(String p) {
public Builder withViewerPass(String p) {
this.viewerPass = p;
return this;
}
public Builder withLearningDashboardEnabled(Boolean e) {
this.learningDashboardEnabled = e;
return this;
}
public Builder withLearningDashboardCleanupDelayInMinutes(int m) {
this.learningDashboardCleanupDelayInMinutes = m;
return this;
}
public Builder withLearningDashboardAccessToken(String t) {
this.learningDashboardAccessToken = t;
return this;
}
public Builder withWelcomeMessage(String w) {
welcomeMsg = w;
return this;
@ -819,12 +849,12 @@ public class Meeting {
welcomeMsgTemplate = w;
return this;
}
public Builder withDefaultAvatarURL(String w) {
defaultAvatarURL = w;
return this;
}
public Builder isBreakout(Boolean b) {
isBreakout = b;
return this;
@ -834,22 +864,22 @@ public class Meeting {
logoutUrl = l;
return this;
}
public Builder withLogoutTimer(int l) {
logoutTimer = l;
return this;
}
public Builder withBannerColor(String c) {
bannerColor = c;
return this;
}
public Builder withBannerText(String t) {
bannerText = t;
return this;
}
public Builder withMetadata(Map<String, String> m) {
metadata = m;
return this;
@ -858,13 +888,18 @@ public class Meeting {
public Builder withGuestPolicy(String policy) {
guestPolicy = policy;
return this;
}
}
public Builder withAuthenticatedGuest(Boolean authGuest) {
authenticatedGuest = authGuest;
return this;
}
public Builder withMeetingLayout(String layout) {
meetingLayout = layout;
return this;
}
public Builder withBreakoutRoomsParams(BreakoutRoomsParams params) {
breakoutRoomsParams = params;
return this;
@ -885,11 +920,16 @@ public class Meeting {
return this;
}
public Builder withEndWhenNoModeratorDelayInMinutes(Integer endWhenNoModeratorDelayInMinutes) {
this.endWhenNoModeratorDelayInMinutes = endWhenNoModeratorDelayInMinutes;
return this;
}
public Builder withHTML5InstanceId(int instanceId) {
html5InstanceId = instanceId;
return this;
}
public Meeting build() {
return new Meeting(this);
}

View File

@ -0,0 +1,7 @@
package org.bigbluebutton.api.domain;
public class MeetingLayout {
public static final String FOCUS_ON_PRESENTATION = "FOCUS_ON_PRESENTATION";
public static final String FOCUS_ON_WEBCAMS = "FOCUS_ON_WEBCAMS";
public static final String SMART_LAYOUT = "SMART_LAYOUT";
}

View File

@ -42,7 +42,6 @@ public class UserSession {
public String logoutUrl = null;
public String defaultLayout = "NOLAYOUT";
public String avatarURL;
public String configXML;
public String guestStatus = GuestPolicy.ALLOW;
public String clientUrl = null;
@ -52,6 +51,96 @@ public class UserSession {
public synchronized int incrementConnectionNum() {
return connections.incrementAndGet();
}
public String getAuthToken() {
return authToken;
}
public String getInternalUserId() {
return internalUserId;
}
public String getConferencename() {
return conferencename;
}
public String getMeetingID() {
return meetingID;
}
public String getExternMeetingID() {
return externMeetingID;
}
public String getExternUserID() {
return externUserID;
}
public String getFullname() {
return fullname;
}
public String getRole() {
return role;
}
public String getConference() {
return conference;
}
public String getRoom() {
return room;
}
public Boolean getGuest() {
return guest;
}
public Boolean getAuthed() {
return authed;
}
public String getVoicebridge() {
return voicebridge;
}
public String getWebvoiceconf() {
return webvoiceconf;
}
public String getMode() {
return mode;
}
public String getRecord() {
return record;
}
public String getWelcome() {
return welcome;
}
public String getLogoutUrl() {
return logoutUrl;
}
public String getDefaultLayout() {
return defaultLayout;
}
public String getAvatarURL() {
return avatarURL;
}
public String getGuestStatus() {
return guestStatus;
}
public String getClientUrl() {
return clientUrl;
}
public String toString() {
return fullname + " " + meetingID + " " + conferencename;
}
}

View File

@ -17,6 +17,8 @@ public class CreateMeetingMessage {
public boolean webcamsOnlyForModerator;
public final String moderatorPass;
public final String viewerPass;
public final String learningDashboardAccessToken;
public final Boolean learningDashboardEnabled;
public final Long createTime;
public final String createDate;
public final Map<String, String> metadata;
@ -25,7 +27,8 @@ public class CreateMeetingMessage {
String voiceBridge, Long duration,
Boolean autoStartRecording, Boolean allowStartStopRecording,
Boolean webcamsOnlyForModerator, String moderatorPass,
String viewerPass, Long createTime, String createDate, Map<String, String> metadata) {
String viewerPass, String learningDashboardAccessToken, Boolean learningDashboardEnabled,
Long createTime, String createDate, Map<String, String> metadata) {
this.id = id;
this.externalId = externalId;
this.name = name;
@ -37,6 +40,8 @@ public class CreateMeetingMessage {
this.webcamsOnlyForModerator = webcamsOnlyForModerator;
this.moderatorPass = moderatorPass;
this.viewerPass = viewerPass;
this.learningDashboardAccessToken = learningDashboardAccessToken;
this.learningDashboardEnabled = learningDashboardEnabled;
this.createTime = createTime;
this.createDate = createDate;
this.metadata = metadata;

View File

@ -6,6 +6,8 @@ public class CreateBreakoutRoom implements IMessage {
public final String parentMeetingId; // The main meeting internal id
public final String name; // The name of the breakout room
public final Integer sequence; // The sequence number of the breakout room
public final String shortName; // Name used in breakout rooms list
public final Boolean isDefaultName; // Inform if using default name or changed by moderator
public final Boolean freeJoin; // Allow users to freely join the conference
// in the client
public final String dialNumber;
@ -22,6 +24,8 @@ public class CreateBreakoutRoom implements IMessage {
String parentMeetingId,
String name,
Integer sequence,
String shortName,
Boolean isDefaultName,
Boolean freeJoin,
String dialNumber,
String voiceConfId,
@ -36,6 +40,8 @@ public class CreateBreakoutRoom implements IMessage {
this.parentMeetingId = parentMeetingId;
this.name = name;
this.sequence = sequence;
this.shortName = shortName;
this.isDefaultName = isDefaultName;
this.freeJoin = freeJoin;
this.dialNumber = dialNumber;
this.voiceConfId = voiceConfId;

View File

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

View File

@ -0,0 +1,21 @@
package org.bigbluebutton.api.model.constraint;
import org.bigbluebutton.api.model.validator.GetChecksumValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Constraint(validatedBy = GetChecksumValidator.class)
@Target(TYPE)
@Retention(RUNTIME)
public @interface GetChecksumConstraint {
String message() default "Invalid checksum: checksums do not match";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -0,0 +1,21 @@
package org.bigbluebutton.api.model.constraint;
import org.bigbluebutton.api.model.validator.GuestPolicyValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Constraint(validatedBy = { GuestPolicyValidator.class })
@Target(FIELD)
@Retention(RUNTIME)
public @interface GuestPolicyConstraint {
String message() default "User denied access for this session";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -0,0 +1,21 @@
package org.bigbluebutton.api.model.constraint;
import org.bigbluebutton.api.model.validator.IsBooleanValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Constraint(validatedBy = IsBooleanValidator.class)
@Target(FIELD)
@Retention(RUNTIME)
public @interface IsBooleanConstraint {
String message() default "Validation error: value must be a boolean";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -0,0 +1,21 @@
package org.bigbluebutton.api.model.constraint;
import org.bigbluebutton.api.model.validator.IsIntegralValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Constraint(validatedBy = IsIntegralValidator.class)
@Target(FIELD)
@Retention(RUNTIME)
public @interface IsIntegralConstraint {
String message() default "Validation error: value must be an integral number";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -0,0 +1,21 @@
package org.bigbluebutton.api.model.constraint;
import org.bigbluebutton.api.model.validator.MaxParticipantsValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Constraint(validatedBy = { MaxParticipantsValidator.class })
@Target(FIELD)
@Retention(RUNTIME)
public @interface MaxParticipantsConstraint {
String message() default "The maximum number of participants for the meeting has been reached";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -0,0 +1,21 @@
package org.bigbluebutton.api.model.constraint;
import org.bigbluebutton.api.model.validator.MeetingEndedValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Constraint(validatedBy = MeetingEndedValidator.class)
@Target(FIELD)
@Retention(RUNTIME)
public @interface MeetingEndedConstraint {
String message() default "You can not re-join a meeting that has already been forcibly ended";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -0,0 +1,22 @@
package org.bigbluebutton.api.model.constraint;
import org.bigbluebutton.api.model.validator.MeetingExistsValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Constraint(validatedBy = MeetingExistsValidator.class)
@Target(FIELD)
@Retention(RUNTIME)
public @interface MeetingExistsConstraint {
String message() default "Invalid meeting ID: A meeting with that ID does not exist";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -0,0 +1,25 @@
package org.bigbluebutton.api.model.constraint;
import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@NotEmpty(message = "You must provide a meeting ID")
@Size(min = 2, max = 256, message = "Meeting ID must be between 2 and 256 characters")
@Pattern(regexp = "^[^,]+$", message = "Meeting ID cannot contain ','")
@Constraint(validatedBy = {})
@Target(FIELD)
@Retention(RUNTIME)
public @interface MeetingIDConstraint {
String message() default "Invalid meeting ID";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -0,0 +1,25 @@
package org.bigbluebutton.api.model.constraint;
import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@NotEmpty(message = "You must provide a meeting name")
@Size(min = 2, max = 256, message = "Meeting name must be between 2 and 256 characters")
//@Pattern(regexp = "^[^,]+$", message = "Meeting name cannot contain ','")
@Constraint(validatedBy = {})
@Target(FIELD)
@Retention(RUNTIME)
public @interface MeetingNameConstraint {
String message() default "Invalid meeting name";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -0,0 +1,21 @@
package org.bigbluebutton.api.model.constraint;
import org.bigbluebutton.api.model.validator.ModeratorPasswordValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Constraint(validatedBy = ModeratorPasswordValidator.class)
@Target(TYPE)
@Retention(RUNTIME)
public @interface ModeratorPasswordConstraint {
String message() default "Invalid password: The supplied moderator password is incorrect";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -0,0 +1,23 @@
package org.bigbluebutton.api.model.constraint;
import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@NotEmpty(message = "You must provide your password")
@Size(min = 2, max = 64, message = "Password must be between 8 and 20 characters")
@Constraint(validatedBy = {})
@Target(FIELD)
@Retention(RUNTIME)
public @interface PasswordConstraint {
String message() default "Invalid password";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -0,0 +1,21 @@
package org.bigbluebutton.api.model.constraint;
import org.bigbluebutton.api.model.validator.PostChecksumValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Constraint(validatedBy = PostChecksumValidator.class)
@Target(TYPE)
@Retention(RUNTIME)
public @interface PostChecksumConstraint {
String message() default "Invalid checksum: checksums do not match";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -0,0 +1,21 @@
package org.bigbluebutton.api.model.constraint;
import org.bigbluebutton.api.model.validator.UserSessionValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Constraint(validatedBy = { UserSessionValidator.class })
@Target(FIELD)
@Retention(RUNTIME)
public @interface UserSessionConstraint {
String message() default "Invalid session token";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -0,0 +1,142 @@
package org.bigbluebutton.api.model.request;
import org.bigbluebutton.api.model.constraint.*;
import org.bigbluebutton.api.model.shared.Checksum;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.Map;
public class CreateMeeting extends RequestWithChecksum<CreateMeeting.Params> {
public enum Params implements RequestParameters {
NAME("name"),
MEETING_ID("meetingID"),
VOICE_BRIDGE("voiceBridge"),
ATTENDEE_PW("attendeePW"),
MODERATOR_PW("moderatorPW"),
IS_BREAKOUT_ROOM("isBreakoutRoom"),
RECORD("record");
private final String value;
Params(String value) { this.value = value; }
public String getValue() { return value; }
}
@MeetingNameConstraint
private String name;
@MeetingIDConstraint
private String meetingID;
//@NotEmpty(message = "You must provide a voice bridge")
@IsIntegralConstraint(message = "Voice bridge must be a 5-digit integral value")
private String voiceBridgeString;
private Integer voiceBridge;
@PasswordConstraint
private String attendeePW;
@PasswordConstraint
private String moderatorPW;
//@NotEmpty(message = "You must provide whether this meeting is breakout room")
@IsBooleanConstraint(message = "You must provide a boolean value (true or false) for the breakout room")
private String isBreakoutRoomString;
private Boolean isBreakoutRoom;
//@NotEmpty(message = "You must provide whether to record this meeting")
@IsBooleanConstraint(message = "Record must be a boolean value (true or false)")
private String recordString;
private Boolean record;
public CreateMeeting(Checksum checksum) {
super(checksum);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getMeetingID() {
return meetingID;
}
public void setMeetingID(String meetingID) {
this.meetingID = meetingID;
}
public String getVoiceBridgeString() {
return voiceBridgeString;
}
public void setVoiceBridgeString(String voiceBridgeString) {
this.voiceBridgeString = voiceBridgeString;
}
public Integer getVoiceBridge() { return voiceBridge; }
public void setVoiceBridge(Integer voiceBridge) { this.voiceBridge = voiceBridge; }
public String getAttendeePW() {
return attendeePW;
}
public void setAttendeePW(String attendeePW) {
this.attendeePW = attendeePW;
}
public String getModeratorPW() {
return moderatorPW;
}
public void setModeratorPW(String moderatorPW) {
this.moderatorPW = moderatorPW;
}
public void setBreakoutRoomString(String breakoutRoomString) { isBreakoutRoomString = breakoutRoomString; }
public Boolean isBreakoutRoom() {
return isBreakoutRoom;
}
public void setBreakoutRoom(boolean breakoutRoom) {
isBreakoutRoom = breakoutRoom;
}
public void setRecordString(String recordString) { this.recordString = recordString; }
public Boolean isRecord() {
return record;
}
public void setRecord(boolean record) {
this.record = record;
}
@Override
public void populateFromParamsMap(Map<String, String[]> params) {
if(params.containsKey(Params.NAME.getValue())) setName(params.get(Params.NAME.getValue())[0]);
if(params.containsKey(Params.MEETING_ID.getValue())) setMeetingID(params.get(Params.MEETING_ID.getValue())[0]);
if(params.containsKey(Params.VOICE_BRIDGE.getValue())) setVoiceBridgeString(params.get(Params.VOICE_BRIDGE.getValue())[0]);
if(params.containsKey(Params.ATTENDEE_PW.getValue())) setAttendeePW(params.get(Params.ATTENDEE_PW.getValue())[0]);
if(params.containsKey(Params.MODERATOR_PW.getValue())) setModeratorPW(params.get(Params.MODERATOR_PW.getValue())[0]);
if(params.containsKey(Params.IS_BREAKOUT_ROOM.getValue())) setBreakoutRoomString(params.get(Params.IS_BREAKOUT_ROOM.value)[0]);
if(params.containsKey(Params.RECORD.getValue())) setRecordString(params.get(Params.RECORD.getValue())[0]);
}
@Override
public void convertParamsFromString() {
if (voiceBridge != null) {
voiceBridge = Integer.parseInt(voiceBridgeString);
}
isBreakoutRoom = Boolean.parseBoolean(isBreakoutRoomString);
record = Boolean.parseBoolean(recordString);
}
}

View File

@ -0,0 +1,68 @@
package org.bigbluebutton.api.model.request;
import org.bigbluebutton.api.model.constraint.MeetingExistsConstraint;
import org.bigbluebutton.api.model.constraint.MeetingIDConstraint;
import org.bigbluebutton.api.model.constraint.PasswordConstraint;
import org.bigbluebutton.api.model.shared.Checksum;
import org.bigbluebutton.api.model.shared.ModeratorPassword;
import javax.validation.Valid;
import java.util.Map;
public class EndMeeting extends RequestWithChecksum<EndMeeting.Params> {
public enum Params implements RequestParameters {
MEETING_ID("meetingID"),
PASSWORD("password");
private final String value;
Params(String value) { this.value = value; }
public String getValue() { return value; }
}
@MeetingIDConstraint
@MeetingExistsConstraint
private String meetingID;
@PasswordConstraint
private String password;
@Valid
private ModeratorPassword moderatorPassword;
public EndMeeting(Checksum checksum) {
super(checksum);
moderatorPassword = new ModeratorPassword();
}
public String getMeetingID() {
return meetingID;
}
public void setMeetingID(String meetingID) {
this.meetingID = meetingID;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public void populateFromParamsMap(Map<String, String[]> params) {
if(params.containsKey(Params.MEETING_ID.getValue())) {
setMeetingID(params.get(Params.MEETING_ID.getValue())[0]);
moderatorPassword.setMeetingID(meetingID);
}
if(params.containsKey(Params.PASSWORD.getValue())) {
setPassword(params.get(Params.PASSWORD.getValue())[0]);
moderatorPassword.setPassword(password);
}
}
}

View File

@ -0,0 +1,57 @@
package org.bigbluebutton.api.model.request;
import org.bigbluebutton.api.model.constraint.*;
import org.bigbluebutton.api.service.SessionService;
import javax.validation.constraints.NotNull;
import java.util.Map;
public class Enter implements Request<Enter.Params> {
public enum Params implements RequestParameters {
SESSION_TOKEN("sessionToken");
private final String value;
Params(String value) { this.value = value; }
public String getValue() { return value; }
}
@NotNull(message = "You must provide a session token")
@UserSessionConstraint
//@MaxParticipantsConstraint
@GuestPolicyConstraint
private String sessionToken;
@MeetingExistsConstraint
@MeetingEndedConstraint
private String meetingID;
private SessionService sessionService;
public Enter() {
sessionService = new SessionService();
}
public String getSessionToken() {
return sessionToken;
}
public void setSessionToken(String sessionToken) {
this.sessionToken = sessionToken;
}
@Override
public void populateFromParamsMap(Map<String, String[]> params) {
if(params.containsKey(Params.SESSION_TOKEN.getValue())) {
setSessionToken(params.get(Params.SESSION_TOKEN.getValue())[0]);
sessionService.setSessionToken(sessionToken);
meetingID = sessionService.getMeetingID();
}
}
@Override
public void convertParamsFromString() {
}
}

View File

@ -0,0 +1,60 @@
package org.bigbluebutton.api.model.request;
import org.bigbluebutton.api.model.constraint.MaxParticipantsConstraint;
import org.bigbluebutton.api.model.constraint.MeetingEndedConstraint;
import org.bigbluebutton.api.model.constraint.MeetingExistsConstraint;
import org.bigbluebutton.api.model.constraint.UserSessionConstraint;
import org.bigbluebutton.api.service.SessionService;
import javax.validation.constraints.NotNull;
import java.util.Map;
public class GuestWait implements Request<GuestWait.Params> {
public enum Params implements RequestParameters {
SESSION_TOKEN("sessionToken");
private final String value;
Params(String value) { this.value = value; }
public String getValue() { return value; }
}
@NotNull(message = "You must provide the session token")
@UserSessionConstraint
// @MaxParticipantsConstraint
private String sessionToken;
@MeetingExistsConstraint
@MeetingEndedConstraint
private String meetingID;
private SessionService sessionService;
public GuestWait() {
sessionService = new SessionService();
}
public String getSessionToken() {
return sessionToken;
}
public void setSessionToken(String sessionToken) {
this.sessionToken = sessionToken;
}
@Override
public void populateFromParamsMap(Map<String, String[]> params) {
if(params.containsKey(Params.SESSION_TOKEN.getValue())) {
setSessionToken(params.get(Params.SESSION_TOKEN.getValue())[0]);
sessionService.setSessionToken(sessionToken);
meetingID = sessionService.getMeetingID();
}
}
@Override
public void convertParamsFromString() {
}
}

View File

@ -0,0 +1,141 @@
package org.bigbluebutton.api.model.request;
import org.bigbluebutton.api.model.constraint.*;
import org.bigbluebutton.api.model.shared.Checksum;
import javax.validation.constraints.NotEmpty;
import java.util.Map;
public class JoinMeeting extends RequestWithChecksum<JoinMeeting.Params> {
public enum Params implements RequestParameters {
MEETING_ID("meetingID"),
USER_ID("userID"),
FULL_NAME("fullName"),
PASSWORD("password"),
GUEST("guest"),
AUTH("auth"),
CREATE_TIME("createTime");
private final String value;
Params(String value) { this.value = value; }
public String getValue() { return value; }
}
@MeetingIDConstraint
@MeetingExistsConstraint
@MeetingEndedConstraint
private String meetingID;
private String userID;
@NotEmpty(message = "You must provide your name")
private String fullName;
@PasswordConstraint
private String password;
@IsBooleanConstraint(message = "Guest must be a boolean value (true or false)")
private String guestString;
private Boolean guest;
@IsBooleanConstraint(message = "Auth must be a boolean value (true or false)")
private String authString;
private Boolean auth;
@IsIntegralConstraint
private String createTimeString;
private Long createTime;
public JoinMeeting(Checksum checksum) {
super(checksum);
}
public String getMeetingID() {
return meetingID;
}
public void setMeetingID(String meetingID) {
this.meetingID = meetingID;
}
public String getUserID() {
return userID;
}
public void setUserID(String userID) {
this.userID = userID;
}
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public void setGuestString(String guestString) { this.guestString = guestString; }
public Boolean getGuest() {
return guest;
}
public void setGuest(Boolean guest) {
this.guest = guest;
}
public void setAuthString(String authString) { this.authString = authString; }
public Boolean getAuth() {
return auth;
}
public void setAuth(Boolean auth) {
this.auth = auth;
}
public void setCreateTimeString(String createTimeString) { this.createTimeString = createTimeString; }
public Long getCreateTime() {
return createTime;
}
public void setCreateTime(Long createTime) {
this.createTime = createTime;
}
@Override
public void populateFromParamsMap(Map<String, String[]> params) {
if(params.containsKey(Params.MEETING_ID.getValue())) {
setMeetingID(params.get(Params.MEETING_ID.getValue())[0]);
}
if(params.containsKey(Params.USER_ID.getValue())) setUserID(params.get(Params.USER_ID.getValue())[0]);
if(params.containsKey(Params.FULL_NAME.getValue())) setFullName(params.get(Params.FULL_NAME.getValue())[0]);
if(params.containsKey(Params.PASSWORD.getValue())) setPassword(params.get(Params.PASSWORD.getValue())[0]);
if(params.containsKey(Params.GUEST.getValue())) setGuestString(params.get(Params.GUEST.getValue())[0]);
if(params.containsKey(Params.AUTH.getValue())) setAuthString(params.get(Params.AUTH.getValue())[0]);
if(params.containsKey(Params.CREATE_TIME.getValue())) setCreateTimeString(params.get(Params.CREATE_TIME.getValue())[0]);
}
@Override
public void convertParamsFromString() {
guest = Boolean.parseBoolean(guestString);
auth = Boolean.parseBoolean(authString);
if(createTimeString != null) {
createTime = Long.parseLong(createTimeString);
}
}
}

View File

@ -0,0 +1,43 @@
package org.bigbluebutton.api.model.request;
import org.bigbluebutton.api.model.constraint.MeetingExistsConstraint;
import org.bigbluebutton.api.model.constraint.MeetingIDConstraint;
import org.bigbluebutton.api.model.shared.Checksum;
import java.util.Map;
public class MeetingInfo extends RequestWithChecksum<MeetingInfo.Params> {
public enum Params implements RequestParameters {
MEETING_ID("meetingID");
private final String value;
Params(String value) { this.value = value; }
public String getValue() { return value; }
}
@MeetingIDConstraint
@MeetingExistsConstraint
private String meetingID;
public MeetingInfo(Checksum checksum) {
super(checksum);
}
public String getMeetingID() {
return meetingID;
}
public void setMeetingID(String meetingID) {
this.meetingID = meetingID;
}
@Override
public void populateFromParamsMap(Map<String, String[]> params) {
if(params.containsKey(Params.MEETING_ID.getValue())) {
setMeetingID(params.get(Params.MEETING_ID.getValue())[0]);
}
}
}

View File

@ -0,0 +1,39 @@
package org.bigbluebutton.api.model.request;
import org.bigbluebutton.api.model.constraint.MeetingIDConstraint;
import org.bigbluebutton.api.model.shared.Checksum;
import java.util.Map;
public class MeetingRunning extends RequestWithChecksum<MeetingRunning.Params> {
public enum Params implements RequestParameters {
MEETING_ID("meetingID");
private final String value;
Params(String value) { this.value = value; }
public String getValue() { return value; }
}
@MeetingIDConstraint
private String meetingID;
public MeetingRunning(Checksum checksum) {
super(checksum);
}
public String getMeetingID() {
return meetingID;
}
public void setMeetingID(String meetingID) {
this.meetingID = meetingID;
}
@Override
public void populateFromParamsMap(Map<String, String[]> params) {
if(params.containsKey(Params.MEETING_ID.getValue())) setMeetingID(params.get(Params.MEETING_ID.getValue())[0]);
}
}

View File

@ -0,0 +1,9 @@
package org.bigbluebutton.api.model.request;
import java.util.Map;
public interface Request<P extends Enum<P> & RequestParameters> {
void populateFromParamsMap(Map<String, String[]> params);
void convertParamsFromString();
}

View File

@ -0,0 +1,6 @@
package org.bigbluebutton.api.model.request;
public interface RequestParameters {
String getValue();
}

View File

@ -0,0 +1,30 @@
package org.bigbluebutton.api.model.request;
import org.bigbluebutton.api.model.shared.Checksum;
import javax.validation.Valid;
import java.util.Map;
public abstract class RequestWithChecksum<P extends Enum<P> & RequestParameters> implements Request<P> {
@Valid
protected Checksum checksum;
protected RequestWithChecksum(Checksum checksum) {
this.checksum = checksum;
}
public Checksum getChecksum() {
return checksum;
}
public void setChecksum(Checksum checksum) {
this.checksum = checksum;
}
public abstract void populateFromParamsMap(Map<String, String[]> params);
public void convertParamsFromString() {
}
}

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