diff --git a/.github/actions/merge-branches/action.yml b/.github/actions/merge-branches/action.yml new file mode 100644 index 000000000000..e171711fbefd --- /dev/null +++ b/.github/actions/merge-branches/action.yml @@ -0,0 +1,20 @@ +name: Merge branches +on: + workflow_call: + +runs: + using: "composite" + steps: + - name: Checkout ${{ github.event.pull_request.base.ref || 'master' }} + uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.base.ref || '' }} + fetch-depth: 0 # Fetch all history + - name: Merge pr-${{ github.event.number }} into ${{ github.event.pull_request.base.ref }} + if: github.event_name == 'pull_request' + shell: bash + run: | + git config user.name "BBB Automated Tests" + git config user.email "tests@bigbluebutton.org" + git config pull.rebase false + git pull origin pull/${{ github.event.number }}/head:${{ github.head_ref }} \ No newline at end of file diff --git a/.github/workflows/automated-tests.yml b/.github/workflows/automated-tests.yml index 15df026f4cf8..e875511f3b6f 100644 --- a/.github/workflows/automated-tests.yml +++ b/.github/workflows/automated-tests.yml @@ -1,89 +1,211 @@ -name: 'Automated tests' +name: "Automated tests" on: push: branches: - - 'develop' - - 'v2.[5-9].x-release' - - 'v[3-9].*.x-release' + - "develop" + - "v2.[5-9].x-release" + - "v[3-9].*.x-release" paths-ignore: - - 'docs/**' - - '**/*.md' + - "docs/**" + - "**/*.md" pull_request: types: [opened, synchronize, reopened] paths-ignore: - - 'docs/**' - - '**/*.md' + - "docs/**" + - "**/*.md" + - "bigbluebutton-html5/public/locales/*.json" permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: - build-install-and-test: - runs-on: ubuntu-20.04 + build-package: + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + package: + [ + bbb-apps-akka, + bbb-config, + bbb-export-annotations, + bbb-learning-dashboard, + bbb-playback-record, + bbb-etherpad, + bbb-web, + bbb-fsesl-akka, + bbb-html5, + bbb-freeswitch, + bbb-webrtc, + others, + ] + include: + - package: bbb-apps-akka + cache-files-list: akka-bbb-apps bbb-common-message + - package: bbb-config + cache-files-list: bigbluebutton-config + - package: bbb-export-annotations + cache-files-list: bbb-export-annotations + - package: bbb-learning-dashboard + cache-files-list: bbb-learning-dashboard + - package: bbb-playback-record + build-list: bbb-playback bbb-playback-notes bbb-playback-podcast bbb-playback-presentation bbb-playback-screenshare bbb-playback-video bbb-record-core + - package: bbb-etherpad + cache-files-list: bbb-etherpad.placeholder.sh build/packages-template/bbb-etherpad + cache-urls-list: https://api.github.com/repos/mconf/ep_pad_ttl/commits https://api.github.com/repos/alangecker/bbb-etherpad-plugin/commits https://api.github.com/repos/mconf/ep_redis_publisher/commits https://api.github.com/repos/alangecker/bbb-etherpad-skin/commits + - package: bbb-web + cache-files-list: bigbluebutton-web bbb-common-message bbb-common-web + - package: bbb-fsesl-akka + cache-files-list: akka-bbb-fsesl bbb-common-message + - package: bbb-html5 + build-list: bbb-html5-nodejs bbb-html5 + cache-files-list: bigbluebutton-html5 + - package: bbb-freeswitch + build-list: bbb-freeswitch-core bbb-freeswitch-sounds + cache-files-list: freeswitch.placeholder.sh build/packages-template/bbb-freeswitch-core build/packages-template/bbb-freeswitch-sounds + cache-urls-list: http://bigbluebutton.org/downloads/sounds.tar.gz + - package: bbb-webrtc + build-list: bbb-webrtc-sfu bbb-webrtc-recorder + cache-files-list: bbb-webrtc-sfu.placeholder.sh bbb-webrtc-recorder.placeholder.sh build/packages-template/bbb-webrtc-sfu build/packages-template/bbb-webrtc-recorder + - package: others + build-list: bbb-mkclean bbb-pads bbb-libreoffice-docker bbb-transcription-controller bigbluebutton steps: - uses: actions/checkout@v3 - - run: ./build/get_external_dependencies.sh - - run: ./build/setup.sh bbb-apps-akka - - run: ./build/setup.sh bbb-config - - run: ./build/setup.sh bbb-etherpad - - run: ./build/setup.sh bbb-export-annotations - - run: ./build/setup.sh bbb-freeswitch-core - - run: ./build/setup.sh bbb-freeswitch-sounds - - run: ./build/setup.sh bbb-fsesl-akka - - run: ./build/setup.sh bbb-html5-nodejs - - run: ./build/setup.sh bbb-html5 - - run: ./build/setup.sh bbb-learning-dashboard - - run: ./build/setup.sh bbb-libreoffice-docker - - run: ./build/setup.sh bbb-mkclean - - run: ./build/setup.sh bbb-pads - - run: ./build/setup.sh bbb-playback - - run: ./build/setup.sh bbb-playback-notes - - run: ./build/setup.sh bbb-playback-podcast - - run: ./build/setup.sh bbb-playback-presentation - - run: ./build/setup.sh bbb-playback-screenshare - - run: ./build/setup.sh bbb-playback-video - - run: ./build/setup.sh bbb-record-core - - run: ./build/setup.sh bbb-web - - run: ./build/setup.sh bbb-webrtc-sfu - - run: ./build/setup.sh bigbluebutton - - run: tar cvf artifacts.tar artifacts/ + - name: Merge branches + uses: ./.github/actions/merge-branches + - name: Set cache-key vars + run: | + echo "CACHE_KEY_FILES=$(echo '${{ matrix.cache-files-list }} .gitlab-ci.yml build/deb-helper.sh' | xargs -n1 git log -1 --format=%h -- | tr '\n' '-' | sed 's/-$//')" >> $GITHUB_ENV + echo "CACHE_KEY_URLS=$(echo '${{ matrix.cache-urls-list }}' | xargs -r -n 1 curl -Is | grep -i 'Last-Modified' | md5sum | cut -c1-10)" >> $GITHUB_ENV + cat bigbluebutton-config/bigbluebutton-release >> $GITHUB_ENV + echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh + echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh + - name: Handle cache + if: matrix.cache-files-list != '' + id: cache-action + uses: actions/cache@v3 + with: + path: artifacts.tar + key: ${{ runner.os }}-${{ matrix.package }}-${{ env.BIGBLUEBUTTON_RELEASE }}-commits-${{ env.CACHE_KEY_FILES }}-urls-${{ env.CACHE_KEY_URLS }} + - if: ${{ steps.cache-action.outputs.cache-hit != 'true' }} + name: Generate artifacts + shell: bash + run: | + ./build/get_external_dependencies.sh + echo "${{ matrix.build-list || matrix.package }}" | xargs -n 1 ./build/setup.sh + tar cvf artifacts.tar artifacts/ - name: Archive packages uses: actions/upload-artifact@v3 with: - name: artifacts.tar - path: | - artifacts.tar - # - name: Fake package build - # run: | - # sudo sh -c ' - # echo "Faking a package build (to speed up installation test)" - # cd / - # wget -q "http://ci.bbb.imdt.dev/artifacts.tar" - # tar xf artifacts.tar - # ' + name: artifacts_${{ matrix.package }}.tar + path: artifacts.tar + install-and-run-tests: + needs: build-package + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + shard: [1/8, 2/8, 3/8, 4/8, 5/8, 6/8, 7/8, 8/8] + steps: + - uses: actions/checkout@v3 + - name: Merge branches + uses: ./.github/actions/merge-branches + - run: ./build/get_external_dependencies.sh + - name: Download artifacts_bbb-apps-akka + uses: actions/download-artifact@v3 + with: + name: artifacts_bbb-apps-akka.tar + - run: tar xf artifacts.tar + - name: Download artifacts_bbb-config + uses: actions/download-artifact@v3 + with: + name: artifacts_bbb-config.tar + - run: tar xf artifacts.tar + - name: Download artifacts_bbb-export-annotations + uses: actions/download-artifact@v3 + with: + name: artifacts_bbb-export-annotations.tar + - run: tar xf artifacts.tar + - name: Download artifacts_bbb-learning-dashboard + uses: actions/download-artifact@v3 + with: + name: artifacts_bbb-learning-dashboard.tar + - run: tar xf artifacts.tar + - name: Download artifacts_bbb-playback-record + uses: actions/download-artifact@v3 + with: + name: artifacts_bbb-playback-record.tar + - run: tar xf artifacts.tar + - name: Download artifacts_bbb-etherpad + uses: actions/download-artifact@v3 + with: + name: artifacts_bbb-etherpad.tar + - run: tar xf artifacts.tar + - name: Download artifacts_bbb-freeswitch + uses: actions/download-artifact@v3 + with: + name: artifacts_bbb-freeswitch.tar + - run: tar xf artifacts.tar + - name: Download artifacts_bbb-webrtc + uses: actions/download-artifact@v3 + with: + name: artifacts_bbb-webrtc.tar + - run: tar xf artifacts.tar + - name: Download artifacts_bbb-web + uses: actions/download-artifact@v3 + with: + name: artifacts_bbb-web.tar + - run: tar xf artifacts.tar + - name: Download artifacts_bbb-fsesl-akka + uses: actions/download-artifact@v3 + with: + name: artifacts_bbb-fsesl-akka.tar + - run: tar xf artifacts.tar + - name: Download artifacts_bbb-html5 + uses: actions/download-artifact@v3 + with: + name: artifacts_bbb-html5.tar + - run: tar xf artifacts.tar + - name: Download artifacts + uses: actions/download-artifact@v3 + with: + name: artifacts_others.tar + - run: tar xf artifacts.tar + - name: Extracting files .tar + run: | + set -e + pwd + echo "----ls artifacts/----" + ls artifacts/ + echo "Done" - name: Generate CA run: | - sudo sh -c ' + sudo -i < /root/bbb-ci-ssl/bbb-dev-ca.pass ; chmod 600 /root/bbb-ci-ssl/bbb-dev-ca.pass ; openssl genrsa -des3 -out bbb-dev-ca.key -passout file:/root/bbb-ci-ssl/bbb-dev-ca.pass 2048 ; openssl req -x509 -new -nodes -key bbb-dev-ca.key -sha256 -days 1460 -passin file:/root/bbb-ci-ssl/bbb-dev-ca.pass -out bbb-dev-ca.crt -subj "/C=CA/ST=BBB/L=BBB/O=BBB/OU=BBB/CN=BBB-DEV" ; - ' + EOF - name: Trust CA run: | - sudo sh -c ' + sudo -i <> /etc/hosts openssl genrsa -out bbb-ci.test.key 2048 - rm bbb-ci.test.csr bbb-ci.test.crt bbb-ci.test.key + rm -f bbb-ci.test.csr bbb-ci.test.crt bbb-ci.test.key cat > bbb-ci.test.ext << EOF authorityKeyIdentifier=keyid,issuer basicConstraints=CA:FALSE @@ -103,35 +225,38 @@ jobs: cat /root/bbb-ci-ssl/bbb-ci.test.key > /local/certs/privkey.pem ' - name: Setup local repository + shell: bash run: | - sudo sh -c ' - apt install -yq dpkg-dev - cd /root && wget -q http://ci.bbb.imdt.dev/cache-3rd-part-packages.tar - cp -r /home/runner/work/bigbluebutton/bigbluebutton/artifacts/ /artifacts/ - cd /artifacts && tar xf /root/cache-3rd-part-packages.tar - cd /artifacts && dpkg-scanpackages . /dev/null | gzip -9c > Packages.gz - echo "deb [trusted=yes] file:/artifacts/ ./" >> /etc/apt/sources.list - ' + sudo -i < Packages.gz + echo "deb [trusted=yes] file:/artifacts/ ./" >> /etc/apt/sources.list + EOF - name: Prepare for install run: | - sudo sh -c ' - apt --purge -y remove apache2-bin - ' + sudo sh -c ' + apt --purge -y remove apache2-bin + ' - name: Install BBB run: | - sudo sh -c ' - cd /root/ && wget -nv https://raw.githubusercontent.com/bigbluebutton/bbb-install/master/bbb-install-2.6.sh -O bbb-install.sh - cat bbb-install.sh | sed "s|> /etc/apt/sources.list.d/bigbluebutton.list||g" | bash -s -- -v focal-26-dev -s bbb-ci.test -j -d /certs/ - bbb-conf --salt bbbci - echo "NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/bbb-dev/bbb-dev-ca.crt" >> /usr/share/meteor/bundle/bbb-html5-with-roles.conf - sed -i "s/\"minify\": true,/\"minify\": false,/" /usr/share/etherpad-lite/settings.json - bbb-conf --restart - ' + sudo -i < /etc/apt/sources.list.d/bigbluebutton.list||g" | bash -s -- -v focal-27-dev -s bbb-ci.test -j -d /certs/ + bbb-conf --salt bbbci + echo "NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/bbb-dev/bbb-dev-ca.crt" >> /usr/share/meteor/bundle/bbb-html5-with-roles.conf + sed -i "s/\"minify\": true,/\"minify\": false,/" /usr/share/etherpad-lite/settings.json + bbb-conf --restart + EOF - name: Install test dependencies working-directory: ./bigbluebutton-tests/playwright run: | sh -c ' - npm install + npm ci npx playwright install-deps npx playwright install ' @@ -142,11 +267,12 @@ jobs: ACTIONS_RUNNER_DEBUG: true BBB_URL: https://bbb-ci.test/bigbluebutton/api BBB_SECRET: bbbci - run: npm run test-chromium-ci + run: npm run test-chromium-ci -- --shard ${{ matrix.shard }} - name: Run Firefox tests working-directory: ./bigbluebutton-tests/playwright - if: ${{ contains(github.event.pull_request.labels.*.name, 'test Firefox') - || contains(github.event.pull_request.labels.*.name, 'Test Firefox') }} + if: | + contains(github.event.pull_request.labels.*.name, 'test Firefox') || + contains(github.event.pull_request.labels.*.name, 'Test Firefox') env: NODE_EXTRA_CA_CERTS: /usr/local/share/ca-certificates/bbb-dev/bbb-dev-ca.crt ACTIONS_RUNNER_DEBUG: true @@ -156,19 +282,18 @@ jobs: run: | sh -c ' find $HOME/.cache/ms-playwright -name libnssckbi.so -exec rm {} \; -exec ln -s /usr/lib/x86_64-linux-gnu/pkcs11/p11-kit-trust.so {} \; - npm run test-firefox-ci + npm run test-firefox-ci -- --shard ${{ matrix.shard }} ' - - if: always() + - if: always() && github.event_name == 'pull_request' + name: Upload blob report to GitHub Actions Artifacts uses: actions/upload-artifact@v3 with: - name: tests-report - path: | - bigbluebutton-tests/playwright/playwright-report - bigbluebutton-tests/playwright/test-results + name: all-blob-reports + path: bigbluebutton-tests/playwright/blob-report - if: failure() name: Prepare artifacts (configs and logs) run: | - sudo sh -c ' + sudo -i < ./pr-comment-data/pr_number + echo ${{ github.run_id }} > ./pr-comment-data/workflow_id + - name: Upload PR data for auto-comment + if: github.event_name == 'pull_request' + uses: actions/upload-artifact@v3 + with: + name: pr-comment-data + path: pr-comment-data diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 512e94761ba7..0bb31badec36 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -7,6 +7,7 @@ on: - 'develop' paths: - 'docs/**' + - '.github/**' # Do not build the docs concurrently concurrency: diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9a4fea321fe9..44c1bab4a94a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,7 +12,7 @@ stages: # define which docker image to use for builds default: - image: bigbluebutton/bbb-build:v2.6.x-release--2023-08-30-211201 + image: bigbluebutton/bbb-build:bbb27-2023-06-13-java17 # 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 @@ -47,11 +47,13 @@ get_external_dependencies: - bbb-etherpad - bbb-webhooks - bbb-webrtc-sfu + - bbb-webrtc-recorder - freeswitch - bbb-pads - bbb-playback + - bbb-transcription-controller expire_in: 1h 30min - + # template job for build step .build_job: stage: build @@ -170,6 +172,16 @@ bbb-webrtc-sfu-build: script: - build/setup-inside-docker.sh bbb-webrtc-sfu +bbb-webrtc-recorder-build: + extends: .build_job + script: + - build/setup-inside-docker.sh bbb-webrtc-recorder + +bbb-transcription-controller-build: + extends: .build_job + script: + - build/setup-inside-docker.sh bbb-transcription-controller + bigbluebutton-build: extends: .build_job script: @@ -180,12 +192,12 @@ 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) + + # 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 - - + + diff --git a/SECURITY.md b/SECURITY.md index 55e5c504809c..13a89650005a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,14 +6,14 @@ We actively support BigBlueButton through the community forums and through secur | Version | Supported | | ------- | ------------------ | -| 2.4.x (or earlier) | :x: | -| 2.5.x   | :white_check_mark: | +| 2.5.x (or earlier) | :x: | | 2.6.x   | :white_check_mark: | -| 2.7.x   | :x: | +| 2.7.x   | :white_check_mark: | +| 2.8.x   | :x: | -We have released 2.6 to the community and are going to support both 2.5 and 2.6 together for the coming months (while we're actively developing the next release). Also, BigBlueButton 2.4 is now end of life. +We have released 2.7 to the community and are going to support both 2.6 and 2.7 together for the coming months (while we're actively developing the next release). Also, BigBlueButton 2.5 is now end of life. -As such, we recommend that all administrators deploy 2.6 going forward. You'll find [many improvements](https://docs.bigbluebutton.org/2.6/new.html) in this newer version. +As such, we recommend that all administrators deploy 2.7 going forward. You'll find [many improvements](https://docs.bigbluebutton.org/2.7/new-features) in this newer version. ## Reporting a Vulnerability diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/SystemConfiguration.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/SystemConfiguration.scala index d1de9769a983..0df81de5dfb8 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/SystemConfiguration.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/SystemConfiguration.scala @@ -41,6 +41,7 @@ trait SystemConfiguration { lazy val syncVoiceUsersStatusInterval = Try(config.getInt("voiceConf.syncUserStatusInterval")).getOrElse(43) lazy val ejectRogueVoiceUsers = Try(config.getBoolean("voiceConf.ejectRogueVoiceUsers")).getOrElse(true) lazy val dialInApprovalAudioPath = Try(config.getString("voiceConf.dialInApprovalAudioPath")).getOrElse("ivr/ivr-please_hold_while_party_contacted.wav") + lazy val toggleListenOnlyAfterMuteTimer = Try(config.getInt("voiceConf.toggleListenOnlyAfterMuteTimer")).getOrElse(4) lazy val recordingChapterBreakLengthInMinutes = Try(config.getInt("recording.chapterBreakLengthInMinutes")).getOrElse(0) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/BreakoutModel.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/BreakoutModel.scala index a369fe8fed0e..cf83653d870d 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/BreakoutModel.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/BreakoutModel.scala @@ -27,7 +27,8 @@ object BreakoutModel { case class BreakoutModel( startedOn: Option[Long], durationInSeconds: Int, - rooms: Map[String, BreakoutRoom2x] + rooms: Map[String, BreakoutRoom2x], + sendInviteToModerators: Boolean, ) { def find(id: String): Option[BreakoutRoom2x] = { diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/ScreenshareModel.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/ScreenshareModel.scala index 49ee49939ea7..372d0842bdc6 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/ScreenshareModel.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/ScreenshareModel.scala @@ -91,6 +91,14 @@ object ScreenshareModel { def getHasAudio(status: ScreenshareModel): Boolean = { status.hasAudio } + + def setContentType(status: ScreenshareModel, contentType: String): Unit = { + status.contentType = contentType + } + + def getContentType(status: ScreenshareModel): String = { + status.contentType + } } class ScreenshareModel { @@ -103,4 +111,5 @@ class ScreenshareModel { private var screenshareConf: String = "" private var timestamp: String = "" private var hasAudio = false + private var contentType = "camera" } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/TimerModel.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/TimerModel.scala new file mode 100644 index 000000000000..13761abe34e7 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/TimerModel.scala @@ -0,0 +1,100 @@ +package org.bigbluebutton.core.apps + +object TimerModel { + def createTimer( + model: TimerModel, + stopwatch: Boolean = true, + time: Int = 0, + accumulated: Int = 0, + track: String = "", + ): Unit = { + model.stopwatch = stopwatch + model.time = time + model.accumulated = accumulated + model.track = track + } + + def reset(model: TimerModel, stopwatch: Boolean, time: Int, accumulated: Int, startedAt: Long, track: String) : Unit = { + model.stopwatch = stopwatch + model.time = time + model.accumulated = accumulated + model.startedAt = startedAt + model.track = track + model.endedAt = 0 + } + + def setIsActive(model: TimerModel, active: Boolean): Unit = { + model.isActive = active + } + + def getIsACtive(model: TimerModel): Boolean = { + model.isActive + } + + def setStartedAt(model: TimerModel, timestamp: Long): Unit = { + model.startedAt = timestamp + } + + def getStartedAt(model: TimerModel): Long = { + model.startedAt + } + + def setAccumulated(model: TimerModel, accumulated: Int): Unit = { + model.accumulated = accumulated + } + + def getAccumulated(model: TimerModel): Int = { + model.accumulated + } + + def setRunning(model: TimerModel, running: Boolean): Unit = { + model.running = running + } + + def getRunning(model: TimerModel): Boolean = { + model.running + } + + def setStopwatch(model: TimerModel, stopwatch: Boolean): Unit = { + model.stopwatch = stopwatch + } + + def getStopwatch(model: TimerModel): Boolean = { + model.stopwatch + } + + def setTrack(model: TimerModel, track: String): Unit = { + model.track = track + } + + def getTrack(model: TimerModel): String = { + model.track + } + + def setTime(model: TimerModel, time: Int): Unit = { + model.time = time + } + + def getTime(model: TimerModel): Int = { + model.time + } + + def setEndedAt(model: TimerModel, timestamp: Long): Unit = { + model.endedAt = timestamp + } + + def getEndedAt(model: TimerModel): Long = { + model.endedAt + } +} + +class TimerModel { + private var startedAt: Long = 0 + private var endedAt: Long = 0 + private var accumulated: Int = 0 + private var running: Boolean = false + private var time: Int = 0 + private var stopwatch: Boolean = true + private var track: String = "" + private var isActive: Boolean = false +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/audiocaptions/UpdateTranscriptPubMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/audiocaptions/UpdateTranscriptPubMsgHdlr.scala index c690fa13891c..c4115dc1d9ae 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/audiocaptions/UpdateTranscriptPubMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/audiocaptions/UpdateTranscriptPubMsgHdlr.scala @@ -11,11 +11,11 @@ trait UpdateTranscriptPubMsgHdlr { def handle(msg: UpdateTranscriptPubMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { val meetingId = liveMeeting.props.meetingProp.intId - def broadcastEvent(userId: String, transcriptId: String, transcript: String, locale: String): Unit = { + def broadcastEvent(userId: String, transcriptId: String, transcript: String, locale: String, result: Boolean): Unit = { val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, "nodeJSapp") val envelope = BbbCoreEnvelope(TranscriptUpdatedEvtMsg.NAME, routing) val header = BbbClientMsgHeader(TranscriptUpdatedEvtMsg.NAME, meetingId, userId) - val body = TranscriptUpdatedEvtMsgBody(transcriptId, transcript, locale) + val body = TranscriptUpdatedEvtMsgBody(transcriptId, transcript, locale, result) val event = TranscriptUpdatedEvtMsg(header, body) val msgEvent = BbbCommonEnvCoreMsg(envelope, event) @@ -67,7 +67,8 @@ trait UpdateTranscriptPubMsgHdlr { msg.header.userId, msg.body.transcriptId, transcript, - msg.body.locale + msg.body.locale, + msg.body.result, ) } } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/BreakoutRoomCreatedMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/BreakoutRoomCreatedMsgHdlr.scala index 7f2b91fa1730..4d3daaaf7e7b 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/BreakoutRoomCreatedMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/BreakoutRoomCreatedMsgHdlr.scala @@ -35,12 +35,12 @@ trait BreakoutRoomCreatedMsgHdlr { } } - def buildBreakoutRoomsListEvtMsg(meetingId: String, rooms: Vector[BreakoutRoomInfo], roomsReady: Boolean): BbbCommonEnvCoreMsg = { + def buildBreakoutRoomsListEvtMsg(meetingId: String, rooms: Vector[BreakoutRoomInfo], roomsReady: Boolean, sendInviteToModerators: Boolean): BbbCommonEnvCoreMsg = { val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, "not-used") val envelope = BbbCoreEnvelope(BreakoutRoomsListEvtMsg.NAME, routing) val header = BbbClientMsgHeader(BreakoutRoomsListEvtMsg.NAME, meetingId, "not-used") - val body = BreakoutRoomsListEvtMsgBody(meetingId, rooms, roomsReady) + val body = BreakoutRoomsListEvtMsgBody(meetingId, rooms, roomsReady, sendInviteToModerators) val event = BreakoutRoomsListEvtMsg(header, body) BbbCommonEnvCoreMsg(envelope, event) } @@ -57,7 +57,7 @@ trait BreakoutRoomCreatedMsgHdlr { log.info("Sending breakout rooms list to {} with containing {} room(s)", liveMeeting.props.meetingProp.intId, breakoutRooms.length) - val msgEvent = buildBreakoutRoomsListEvtMsg(liveMeeting.props.meetingProp.intId, breakoutRooms, true) + val msgEvent = buildBreakoutRoomsListEvtMsg(liveMeeting.props.meetingProp.intId, breakoutRooms, true, breakoutModel.sendInviteToModerators) outGW.send(msgEvent) breakoutModel diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/BreakoutRoomsListMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/BreakoutRoomsListMsgHdlr.scala index 9a7efafbb7bb..81e9c0f8a516 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/BreakoutRoomsListMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/BreakoutRoomsListMsgHdlr.scala @@ -11,14 +11,14 @@ trait BreakoutRoomsListMsgHdlr { def handleBreakoutRoomsListMsg(msg: BreakoutRoomsListMsg, state: MeetingState2x): MeetingState2x = { - def broadcastEvent(rooms: Vector[BreakoutRoomInfo], roomsReady: Boolean): Unit = { + def broadcastEvent(rooms: Vector[BreakoutRoomInfo], roomsReady: Boolean, sendInviteToModerators: Boolean): Unit = { log.info("Sending breakout rooms list to {} with containing {} room(s)", props.meetingProp.intId, rooms.length) val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, props.meetingProp.intId, msg.header.userId) val envelope = BbbCoreEnvelope(BreakoutRoomsListEvtMsg.NAME, routing) val header = BbbClientMsgHeader(BreakoutRoomsListEvtMsg.NAME, props.meetingProp.intId, msg.header.userId) - val body = BreakoutRoomsListEvtMsgBody(msg.body.meetingId, rooms, roomsReady) + val body = BreakoutRoomsListEvtMsgBody(msg.body.meetingId, rooms, roomsReady, sendInviteToModerators) val event = BreakoutRoomsListEvtMsg(header, body) val msgEvent = BbbCommonEnvCoreMsg(envelope, event) outGW.send(msgEvent) @@ -31,7 +31,7 @@ trait BreakoutRoomsListMsgHdlr { new BreakoutRoomInfo(r.name, r.externalId, r.id, r.sequence, r.shortName, r.isDefaultName, r.freeJoin, Map(), r.captureNotes, r.captureSlides) } val ready = breakoutModel.hasAllStarted() - broadcastEvent(rooms, ready) + broadcastEvent(rooms, ready, breakoutModel.sendInviteToModerators) } state diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/CreateBreakoutRoomsCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/CreateBreakoutRoomsCmdMsgHdlr.scala index 3acca0ea456a..0b3c5f768d92 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/CreateBreakoutRoomsCmdMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/CreateBreakoutRoomsCmdMsgHdlr.scala @@ -84,7 +84,7 @@ trait CreateBreakoutRoomsCmdMsgHdlr extends RightsManagementTrait { outGW.send(event) } - val breakoutModel = new BreakoutModel(None, msg.body.durationInMinutes * 60, rooms) + val breakoutModel = new BreakoutModel(None, msg.body.durationInMinutes * 60, rooms, msg.body.sendInviteToModerators) state.update(Some(breakoutModel)) } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationWithAnnotationsMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/MakePresentationDownloadReqMsgHdlr.scala similarity index 78% rename from akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationWithAnnotationsMsgHdlr.scala rename to akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/MakePresentationDownloadReqMsgHdlr.scala index fe83b2032389..60ccda285b9d 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationWithAnnotationsMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/MakePresentationDownloadReqMsgHdlr.scala @@ -8,10 +8,11 @@ import org.bigbluebutton.core.bus.MessageBus import org.bigbluebutton.core.domain.MeetingState2x import org.bigbluebutton.core.running.LiveMeeting import org.bigbluebutton.core.util.RandomStringGenerator -import org.bigbluebutton.core.models.{ PresentationPod, PresentationPage, PresentationInPod } +import org.bigbluebutton.core.models.{ PresentationInPod, PresentationPage, PresentationPod } + import java.io.File -trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait { +trait MakePresentationDownloadReqMsgHdlr extends RightsManagementTrait { this: PresentationPodHdlrs => object JobTypes { @@ -40,20 +41,25 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait { BbbCommonEnvCoreMsg(envelope, event) } - def buildNewPresAnnFileAvailable(fileURI: String, presId: String): NewPresAnnFileAvailableMsg = { - val header = BbbClientMsgHeader(NewPresAnnFileAvailableMsg.NAME, "not-used", "not-used") - val body = NewPresAnnFileAvailableMsgBody(fileURI, presId) + def buildNewPresFileAvailable(annotatedFileURI: String, originalFileURI: String, convertedFileURI: String, + presId: String, fileStateType: String): NewPresFileAvailableMsg = { + val header = BbbClientMsgHeader(NewPresFileAvailableMsg.NAME, "not-used", "not-used") + val body = NewPresFileAvailableMsgBody(annotatedFileURI, originalFileURI, convertedFileURI, presId, fileStateType) - NewPresAnnFileAvailableMsg(header, body) + NewPresFileAvailableMsg(header, body) } - def buildBroadcastNewPresAnnFileAvailable(newPresAnnFileAvailableMsg: NewPresAnnFileAvailableMsg, liveMeeting: LiveMeeting): BbbCommonEnvCoreMsg = { + def buildBroadcastNewPresFileAvailable(newPresFileAvailableMsg: NewPresFileAvailableMsg, liveMeeting: LiveMeeting): BbbCommonEnvCoreMsg = { val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, liveMeeting.props.meetingProp.intId, "not-used") val envelope = BbbCoreEnvelope(PresentationPageConvertedEventMsg.NAME, routing) - val header = BbbClientMsgHeader(NewPresAnnFileAvailableEvtMsg.NAME, liveMeeting.props.meetingProp.intId, "not-used") - val body = NewPresAnnFileAvailableEvtMsgBody(fileURI = newPresAnnFileAvailableMsg.body.fileURI, presId = newPresAnnFileAvailableMsg.body.presId) - val event = NewPresAnnFileAvailableEvtMsg(header, body) - + val header = BbbClientMsgHeader(NewPresFileAvailableEvtMsg.NAME, liveMeeting.props.meetingProp.intId, "not-used") + val body = NewPresFileAvailableEvtMsgBody( + annotatedFileURI = newPresFileAvailableMsg.body.annotatedFileURI, + originalFileURI = newPresFileAvailableMsg.body.originalFileURI, + convertedFileURI = newPresFileAvailableMsg.body.convertedFileURI, presId = newPresFileAvailableMsg.body.presId, + fileStateType = newPresFileAvailableMsg.body.fileStateType + ) + val event = NewPresFileAvailableEvtMsg(header, body) BbbCommonEnvCoreMsg(envelope, event) } @@ -112,7 +118,7 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait { } } - def handle(m: MakePresentationWithAnnotationDownloadReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { + def handle(m: MakePresentationDownloadReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { val meetingId = liveMeeting.props.meetingProp.intId val userId = m.header.userId @@ -125,9 +131,18 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait { val currentPres: Option[PresentationInPod] = presentationPods.flatMap(_.getPresentation(presId)).headOption - if (liveMeeting.props.meetingProp.disabledFeatures.contains("downloadPresentationWithAnnotations")) { + if (liveMeeting.props.meetingProp.disabledFeatures.contains("downloadPresentationWithAnnotations") + && m.body.fileStateType == "Annotated") { val reason = "Annotated presentation download disabled for this meeting." PermissionCheck.ejectUserForFailedPermission(meetingId, userId, reason, bus.outGW, liveMeeting) + } else if (liveMeeting.props.meetingProp.disabledFeatures.contains("downloadPresentationOriginalFile") + && m.body.fileStateType == "Original") { + val reason = "Original presentation download disabled for this meeting." + PermissionCheck.ejectUserForFailedPermission(meetingId, userId, reason, bus.outGW, liveMeeting) + } else if (liveMeeting.props.meetingProp.disabledFeatures.contains("downloadPresentationConvertedToPdf") + && m.body.fileStateType == "Converted") { + val reason = "Converted presentation download disabled for this meeting. (PDF format)" + PermissionCheck.ejectUserForFailedPermission(meetingId, userId, reason, bus.outGW, liveMeeting) } else if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, userId)) { val reason = "No permission to download presentation." PermissionCheck.ejectUserForFailedPermission(meetingId, userId, reason, bus.outGW, liveMeeting) @@ -145,9 +160,9 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait { val exportJob: ExportJob = new ExportJob(jobId, JobTypes.DOWNLOAD, "annotated_slides", presId, presLocation, allPages, pagesRange, meetingId, ""); val storeAnnotationPages: List[PresentationPageForExport] = getPresentationPagesForExport(pagesRange, pageCount, presId, currentPres, liveMeeting); - val annotationCount: Int = storeAnnotationPages.map(_.annotations.size).sum + val isPresentationOriginalOrConverted = m.body.fileStateType == "Original" || m.body.fileStateType == "Converted" - if (annotationCount > 0) { + if (!isPresentationOriginalOrConverted) { // Send Export Job to Redis val job = buildStoreExportJobInRedisSysMsg(exportJob, liveMeeting) bus.outGW.send(job) @@ -157,13 +172,19 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait { bus.outGW.send(buildStoreAnnotationsInRedisSysMsg(annotations, liveMeeting)) } else { // Return existing uploaded file directly - val filename = currentPres.get.name - val presFilenameExt = filename.split("\\.").last + val convertedFileName = currentPres.get.filenameConverted + val originalFilename = currentPres.get.name + val originalFileExt = originalFilename.split("\\.").last + val convertedFileExt = if (convertedFileName != "") convertedFileName.split("\\.").last else "" - PresentationSender.broadcastSetPresentationDownloadableEvtMsg(bus, meetingId, "DEFAULT_PRESENTATION_POD", "not-used", presId, true, filename) + val convertedFileURI = if (convertedFileName != "") List("presentation", "download", meetingId, + s"${presId}?presFilename=${presId}.${convertedFileExt}&filename=${convertedFileName}").mkString("", File.separator, "") + else "" + val originalFileURI = List("presentation", "download", meetingId, + s"${presId}?presFilename=${presId}.${originalFileExt}&filename=${originalFilename}").mkString("", File.separator, "") - val fileURI = List("presentation", "download", meetingId, s"${presId}?presFilename=${presId}.${presFilenameExt}").mkString("", File.separator, "") - val event = buildNewPresAnnFileAvailable(fileURI, presId) + val event = buildNewPresFileAvailable("", originalFileURI, convertedFileURI, presId, + m.body.fileStateType) handle(event, liveMeeting, bus) } @@ -221,9 +242,12 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait { } } - def handle(m: NewPresAnnFileAvailableMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { - log.info("Received NewPresAnnFileAvailableMsg meetingId={} presId={} fileUrl={}", liveMeeting.props.meetingProp.intId, m.body.presId, m.body.fileURI) - bus.outGW.send(buildBroadcastNewPresAnnFileAvailable(m, liveMeeting)) + def handle(m: NewPresFileAvailableMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { + log.info( + "Received NewPresFileAvailableMsg meetingId={} presId={}", + liveMeeting.props.meetingProp.intId, m.body.presId + ) + bus.outGW.send(buildBroadcastNewPresFileAvailable(m, liveMeeting)) } def handle(m: CaptureSharedNotesReqInternalMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationConversionCompletedSysPubMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationConversionCompletedSysPubMsgHdlr.scala index 23db2e3ae327..2aeeccfcb45f 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationConversionCompletedSysPubMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationConversionCompletedSysPubMsgHdlr.scala @@ -3,6 +3,7 @@ package org.bigbluebutton.core.apps.presentationpod import org.bigbluebutton.common2.msgs._ import org.bigbluebutton.core.bus.MessageBus import org.bigbluebutton.core.domain.MeetingState2x +import org.bigbluebutton.core.models.PresentationInPod import org.bigbluebutton.core.running.LiveMeeting trait PresentationConversionCompletedSysPubMsgHdlr { @@ -22,8 +23,7 @@ trait PresentationConversionCompletedSysPubMsgHdlr { pres <- pod.getPresentation(msg.body.presentation.id) } yield { val presVO = PresentationPodsApp.translatePresentationToPresentationVO(pres, temporaryPresentationId, - msg.body.presentation.isInitialPresentation) - + msg.body.presentation.isInitialPresentation, msg.body.presentation.filenameConverted) PresentationSender.broadcastPresentationConversionCompletedEvtMsg( bus, meetingId, @@ -31,8 +31,10 @@ trait PresentationConversionCompletedSysPubMsgHdlr { msg.header.userId, msg.body.messageKey, msg.body.code, - presVO + presVO, ) + + val originalDownloadableExtension = pres.name.split("\\.").last PresentationSender.broadcastSetPresentationDownloadableEvtMsg( bus, meetingId, @@ -40,11 +42,14 @@ trait PresentationConversionCompletedSysPubMsgHdlr { msg.header.userId, pres.id, pres.downloadable, - pres.name + pres.name, + originalDownloadableExtension ) + val presWithConvertedName = PresentationInPod(pres.id, pres.name, pres.current, pres.pages, + pres.downloadable, pres.removable, msg.body.presentation.filenameConverted) var pods = state.presentationPodManager.addPod(pod) - pods = pods.addPresentationToPod(pod.id, pres) + pods = pods.addPresentationToPod(pod.id, presWithConvertedName) state.update(pods) } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPodHdlrs.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPodHdlrs.scala index c35fdf7976b7..e784a65f983b 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPodHdlrs.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPodHdlrs.scala @@ -19,7 +19,7 @@ class PresentationPodHdlrs(implicit val context: ActorContext) with PresentationPageCountErrorPubMsgHdlr with PresentationUploadedFileTooLargeErrorPubMsgHdlr with PresentationUploadTokenReqMsgHdlr - with PresentationWithAnnotationsMsgHdlr + with MakePresentationDownloadReqMsgHdlr with ResizeAndMovePagePubMsgHdlr with SyncGetPresentationPodsMsgHdlr with RemovePresentationPodPubMsgHdlr diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPodsApp.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPodsApp.scala index 6b993b175187..56979c892107 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPodsApp.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPodsApp.scala @@ -58,7 +58,7 @@ object PresentationPodsApp { } PresentationVO(p.id, "", p.name, p.current, - pages.toVector, p.downloadable, p.removable, false) + pages.toVector, p.downloadable, p.removable, false, "") } PresentationPodVO(pod.id, pod.currentPresenter, presentationVOs.toVector) @@ -74,7 +74,7 @@ object PresentationPodsApp { } def translatePresentationToPresentationVO(pres: PresentationInPod, temporaryPresentationId: String, - isInitialPresentation: Boolean): PresentationVO = { + isInitialPresentation: Boolean, filenameConverted: String): PresentationVO = { val pages = pres.pages.values.map { page => PageVO( id = page.id, @@ -89,8 +89,8 @@ object PresentationPodsApp { heightRatio = page.heightRatio ) } - PresentationVO(pres.id, temporaryPresentationId, pres.name, pres.current, pages.toVector, pres.downloadable, pres.removable, - isInitialPresentation) + PresentationVO(pres.id, temporaryPresentationId, pres.name, pres.current, pages.toVector, pres.downloadable, + pres.removable, isInitialPresentation, filenameConverted) } def setCurrentPresentationInPod(state: MeetingState2x, podId: String, nextCurrentPresId: String): Option[PresentationPod] = { diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationSender.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationSender.scala index 5edd80d1f5e1..5fec709db8d8 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationSender.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationSender.scala @@ -8,10 +8,12 @@ object PresentationSender { def broadcastSetPresentationDownloadableEvtMsg( bus: MessageBus, meetingId: String, - podId: String, userId: String, + podId: String, + userId: String, presentationId: String, downloadable: Boolean, - presFilename: String + presFilename: String, + downloadableExtension: String ): Unit = { val routing = Routing.addMsgToClientRouting( MessageTypes.BROADCAST_TO_MEETING, @@ -20,17 +22,16 @@ object PresentationSender { val envelope = BbbCoreEnvelope(SetPresentationDownloadableEvtMsg.NAME, routing) val header = BbbClientMsgHeader(SetPresentationDownloadableEvtMsg.NAME, meetingId, userId) - val body = SetPresentationDownloadableEvtMsgBody(podId, presentationId, downloadable, presFilename) + val body = SetPresentationDownloadableEvtMsgBody(podId, presentationId, downloadable, presFilename, downloadableExtension) val event = SetPresentationDownloadableEvtMsg(header, body) val msgEvent = BbbCommonEnvCoreMsg(envelope, event) bus.outGW.send(msgEvent) } - def broadcastPresentationConversionCompletedEvtMsg( bus: MessageBus, meetingId: String, podId: String, userId: String, messageKey: String, - code: String, presentation: PresentationVO + code: String, presentation: PresentationVO, ): Unit = { val routing = Routing.addMsgToClientRouting( MessageTypes.BROADCAST_TO_MEETING, diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/SetPresentationDownloadablePubMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/SetPresentationDownloadablePubMsgHdlr.scala index c9d47a7fd486..8ffce475fa37 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/SetPresentationDownloadablePubMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/SetPresentationDownloadablePubMsgHdlr.scala @@ -17,8 +17,21 @@ trait SetPresentationDownloadablePubMsgHdlr extends RightsManagementTrait { val meetingId = liveMeeting.props.meetingProp.intId if (filterPresentationMessage(liveMeeting.users2x, msg.header.userId) && - permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) { - val reason = "No permission to remove presentation from meeting." + permissionFailed( + PermissionCheck.GUEST_LEVEL, + PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId + )) { + val reason = "No permission to make presentation downloadable for meeting." + PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) + state + } else if (liveMeeting.props.meetingProp.disabledFeatures.contains("downloadPresentationOriginalFile") + && msg.body.fileStateType == "Original") { + val reason = "Download original presentation is disabled for meeting." + PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) + state + } else if (liveMeeting.props.meetingProp.disabledFeatures.contains("downloadPresentationConvertedToPdf") + && msg.body.fileStateType == "Converted") { + val reason = "Download converted presentation is disabled for meeting." PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) state } else { @@ -31,8 +44,11 @@ trait SetPresentationDownloadablePubMsgHdlr extends RightsManagementTrait { pod <- PresentationPodsApp.getPresentationPod(state, podId) pres <- pod.getPresentation(presentationId) } yield { + val downloadableExtension = if (msg.body.fileStateType == "Original") + pres.name.split("\\.").last else pres.filenameConverted.split("\\.").last + PresentationSender.broadcastSetPresentationDownloadableEvtMsg(bus, meetingId, pod.id, - msg.header.userId, presentationId, downloadable, pres.name) + msg.header.userId, presentationId, downloadable, pres.name, downloadableExtension) val pods = state.presentationPodManager.setPresentationDownloadableInPod(pod.id, presentationId, downloadable) state.update(pods) @@ -46,4 +62,4 @@ trait SetPresentationDownloadablePubMsgHdlr extends RightsManagementTrait { } } -} \ No newline at end of file +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/GetScreenBroadcastPermissionReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/GetScreenBroadcastPermissionReqMsgHdlr.scala index 0fddbf87de16..4414d7d7d33a 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/GetScreenBroadcastPermissionReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/GetScreenBroadcastPermissionReqMsgHdlr.scala @@ -17,11 +17,8 @@ trait GetScreenBroadcastPermissionReqMsgHdlr { for { user <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId) } yield { - if (props.meetingProp.disabledFeatures.contains("screenshare")) { - val meetingId = liveMeeting.props.meetingProp.intId - val reason = "Screen sharing is disabled for this meeting." - PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting) - } else if (permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) { + if (permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, + liveMeeting.users2x, msg.header.userId)) { val meetingId = liveMeeting.props.meetingProp.intId val reason = "No permission to share the screen." PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/GetScreenshareStatusReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/GetScreenshareStatusReqMsgHdlr.scala index 1edea1b84c0f..9bc604776aa4 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/GetScreenshareStatusReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/GetScreenshareStatusReqMsgHdlr.scala @@ -26,9 +26,10 @@ trait GetScreenshareStatusReqMsgHdlr { val vidHeight = ScreenshareModel.getScreenshareVideoHeight(liveMeeting.screenshareModel) val timestamp = ScreenshareModel.getTimestamp(liveMeeting.screenshareModel) val hasAudio = ScreenshareModel.getHasAudio(liveMeeting.screenshareModel) + val contentType = ScreenshareModel.getContentType(liveMeeting.screenshareModel) val body = ScreenshareRtmpBroadcastStartedEvtMsgBody(voiceConf, screenshareConf, - stream, vidWidth, vidHeight, timestamp, hasAudio) + stream, vidWidth, vidHeight, timestamp, hasAudio, contentType) val event = ScreenshareRtmpBroadcastStartedEvtMsg(header, body) BbbCommonEnvCoreMsg(envelope, event) } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr.scala index 678d09eae89b..6ae5cc5f25f4 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr.scala @@ -10,7 +10,7 @@ trait ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr { def handle(msg: ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { def broadcastEvent(voiceConf: String, screenshareConf: String, stream: String, vidWidth: Int, vidHeight: Int, - timestamp: String, hasAudio: Boolean): BbbCommonEnvCoreMsg = { + timestamp: String, hasAudio: Boolean, contentType: String): BbbCommonEnvCoreMsg = { val routing = Routing.addMsgToClientRouting( MessageTypes.BROADCAST_TO_MEETING, @@ -23,7 +23,7 @@ trait ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr { ) val body = ScreenshareRtmpBroadcastStartedEvtMsgBody(voiceConf, screenshareConf, - stream, vidWidth, vidHeight, timestamp, hasAudio) + stream, vidWidth, vidHeight, timestamp, hasAudio, contentType) val event = ScreenshareRtmpBroadcastStartedEvtMsg(header, body) BbbCommonEnvCoreMsg(envelope, event) } @@ -32,28 +32,39 @@ trait ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr { ScreenshareModel.isBroadcastingRTMP(liveMeeting.screenshareModel) + " URL:" + ScreenshareModel.getRTMPBroadcastingUrl(liveMeeting.screenshareModel)) - // 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) - ScreenshareModel.setScreenshareVideoHeight(liveMeeting.screenshareModel, msg.body.vidHeight) - ScreenshareModel.setVoiceConf(liveMeeting.screenshareModel, msg.body.voiceConf) - ScreenshareModel.setScreenshareConf(liveMeeting.screenshareModel, msg.body.screenshareConf) - ScreenshareModel.setTimestamp(liveMeeting.screenshareModel, msg.body.timestamp) - ScreenshareModel.setHasAudio(liveMeeting.screenshareModel, msg.body.hasAudio) - - log.info("START broadcast ALLOWED when isBroadcastingRTMP=false") - - // Notify viewers in the meeting that there's an rtmp stream to view - val msgEvent = broadcastEvent(msg.body.voiceConf, msg.body.screenshareConf, msg.body.stream, - msg.body.vidWidth, msg.body.vidHeight, msg.body.timestamp, msg.body.hasAudio) - bus.outGW.send(msgEvent) + if (msg.body.contentType == "camera" && liveMeeting.props.meetingProp.disabledFeatures.contains("cameraAsContent")) { + log.error( + "Camera as a content is disabled for meeting {}, meetingID = {}", + liveMeeting.props.meetingProp.name, liveMeeting.props.meetingProp.intId + ) + } else if (msg.body.contentType == "screenshare" && liveMeeting.props.meetingProp.disabledFeatures.contains("screenshare")) { + val meetingId = liveMeeting.props.meetingProp.intId + log.error("Screen sharing is disabled for this meeting, meetingID = {}", meetingId) } else { - log.info("START broadcast NOT ALLOWED when isBroadcastingRTMP=true") + // 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) + ScreenshareModel.setScreenshareVideoHeight(liveMeeting.screenshareModel, msg.body.vidHeight) + ScreenshareModel.setVoiceConf(liveMeeting.screenshareModel, msg.body.voiceConf) + ScreenshareModel.setScreenshareConf(liveMeeting.screenshareModel, msg.body.screenshareConf) + ScreenshareModel.setTimestamp(liveMeeting.screenshareModel, msg.body.timestamp) + ScreenshareModel.setHasAudio(liveMeeting.screenshareModel, msg.body.hasAudio) + ScreenshareModel.setContentType(liveMeeting.screenshareModel, msg.body.contentType) + + log.info("START broadcast ALLOWED when isBroadcastingRTMP=false") + + // Notify viewers in the meeting that there's an rtmp stream to view + val msgEvent = broadcastEvent(msg.body.voiceConf, msg.body.screenshareConf, msg.body.stream, + msg.body.vidWidth, msg.body.vidHeight, msg.body.timestamp, msg.body.hasAudio, msg.body.contentType) + bus.outGW.send(msgEvent) + } else { + log.info("START broadcast NOT ALLOWED when isBroadcastingRTMP=true") + } } } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/SyncGetScreenshareInfoRespMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/SyncGetScreenshareInfoRespMsgHdlr.scala index b49077328eec..7e5c9d69ded0 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/SyncGetScreenshareInfoRespMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/SyncGetScreenshareInfoRespMsgHdlr.scala @@ -27,7 +27,8 @@ trait SyncGetScreenshareInfoRespMsgHdlr { ScreenshareModel.getScreenshareVideoWidth(liveMeeting.screenshareModel), ScreenshareModel.getScreenshareVideoHeight(liveMeeting.screenshareModel), ScreenshareModel.getTimestamp(liveMeeting.screenshareModel), - ScreenshareModel.getHasAudio(liveMeeting.screenshareModel) + ScreenshareModel.getHasAudio(liveMeeting.screenshareModel), + ScreenshareModel.getContentType(liveMeeting.screenshareModel) ) val event = SyncGetScreenshareInfoRespMsg(header, body) val msgEvent = BbbCommonEnvCoreMsg(envelope, event) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/ActivateTimerReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/ActivateTimerReqMsgHdlr.scala new file mode 100644 index 000000000000..ea88d4b962f4 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/ActivateTimerReqMsgHdlr.scala @@ -0,0 +1,50 @@ +package org.bigbluebutton.core.apps.timer + +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.bus.MessageBus +import org.bigbluebutton.core.running.LiveMeeting +import org.bigbluebutton.core.apps.{ TimerModel, PermissionCheck, RightsManagementTrait } + +trait ActivateTimerReqMsgHdlr extends RightsManagementTrait { + this: TimerApp2x => + + def handle(msg: ActivateTimerReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { + log.debug("Received ActivateTimerReqMsg {}", ActivateTimerReqMsg) + def broadcastEvent( + stopwatch: Boolean, + running: Boolean, + time: Int, + accumulated: Int, + track: String + ): Unit = { + val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka") + val envelope = BbbCoreEnvelope(ActivateTimerRespMsg.NAME, routing) + val header = BbbCoreHeaderWithMeetingId( + ActivateTimerRespMsg.NAME, + liveMeeting.props.meetingProp.intId + ) + val body = ActivateTimerRespMsgBody(msg.header.userId, stopwatch, running, time, accumulated, track) + val event = ActivateTimerRespMsg(header, body) + val msgEvent = BbbCommonEnvCoreMsg(envelope, event) + bus.outGW.send(msgEvent) + } + + val isTimerFeatureEnabled: Boolean = !liveMeeting.props.meetingProp.disabledFeatures.contains("timer") + + if (!isTimerFeatureEnabled) { + log.error("Timer feature is disabled for meeting {}, meetingId={}", liveMeeting.props.meetingProp.name, + liveMeeting.props.meetingProp.intId) + } else { + if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) && + 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 or moderator to activate timer" + PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) + } else { + TimerModel.reset(liveMeeting.timerModel, msg.body.stopwatch, msg.body.time, msg.body.accumulated, msg.body.timestamp, msg.body.track) + TimerModel.setIsActive(liveMeeting.timerModel, true) + broadcastEvent(msg.body.stopwatch, msg.body.running, msg.body.time, msg.body.accumulated, msg.body.track) + } + } + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/CreateTimerReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/CreateTimerReqMsgHdlr.scala new file mode 100644 index 000000000000..452b94e01033 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/CreateTimerReqMsgHdlr.scala @@ -0,0 +1,15 @@ +package org.bigbluebutton.core.apps.timer + +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.bus.MessageBus +import org.bigbluebutton.core.running.LiveMeeting +import org.bigbluebutton.core.apps.{ TimerModel, PermissionCheck, RightsManagementTrait } + +trait CreateTimerPubMsgHdlr extends RightsManagementTrait { + this: TimerApp2x => + + def handle(msg: CreateTimerPubMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { + log.debug("Received CreateTimerPubMsg {}", CreateTimerPubMsg) + TimerModel.createTimer(liveMeeting.timerModel, msg.body.stopwatch, msg.body.time, msg.body.accumulated, msg.body.track) + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/DeactivateTimerReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/DeactivateTimerReqMsgHdlr.scala new file mode 100644 index 000000000000..65683cc17412 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/DeactivateTimerReqMsgHdlr.scala @@ -0,0 +1,36 @@ +package org.bigbluebutton.core.apps.timer + +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.bus.MessageBus +import org.bigbluebutton.core.running.LiveMeeting +import org.bigbluebutton.core.apps.{ TimerModel, PermissionCheck, RightsManagementTrait } + +trait DeactivateTimerReqMsgHdlr extends RightsManagementTrait { + this: TimerApp2x => + + def handle(msg: DeactivateTimerReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { + log.debug("Received deactivateTimerReqMsg {}", DeactivateTimerReqMsg) + def broadcastEvent(): Unit = { + val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka") + val envelope = BbbCoreEnvelope(DeactivateTimerRespMsg.NAME, routing) + val header = BbbCoreHeaderWithMeetingId( + DeactivateTimerRespMsg.NAME, + liveMeeting.props.meetingProp.intId + ) + val body = DeactivateTimerRespMsgBody(msg.header.userId) + val event = DeactivateTimerRespMsg(header, body) + val msgEvent = BbbCommonEnvCoreMsg(envelope, event) + bus.outGW.send(msgEvent) + } + + if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) && + 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 or moderator to deactivate timer" + PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) + } else { + TimerModel.setIsActive(liveMeeting.timerModel, false); + broadcastEvent() + } + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/ResetTimerReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/ResetTimerReqMsgHdlr.scala new file mode 100644 index 000000000000..d20701d3f990 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/ResetTimerReqMsgHdlr.scala @@ -0,0 +1,35 @@ +package org.bigbluebutton.core.apps.timer + +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.bus.MessageBus +import org.bigbluebutton.core.running.LiveMeeting +import org.bigbluebutton.core.apps.{ TimerModel, PermissionCheck, RightsManagementTrait } + +trait ResetTimerReqMsgHdlr extends RightsManagementTrait { + this: TimerApp2x => + + def handle(msg: ResetTimerReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { + log.debug("Received resetTimerReqMsg {}", ResetTimerReqMsg) + def broadcastEvent(): Unit = { + val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka") + val envelope = BbbCoreEnvelope(ResetTimerRespMsg.NAME, routing) + val header = BbbCoreHeaderWithMeetingId( + ResetTimerRespMsg.NAME, + liveMeeting.props.meetingProp.intId + ) + val body = ResetTimerRespMsgBody(msg.header.userId) + val event = ResetTimerRespMsg(header, body) + val msgEvent = BbbCommonEnvCoreMsg(envelope, event) + bus.outGW.send(msgEvent) + } + + if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) && + 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 or moderator to reset timer" + PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) + } else { + broadcastEvent() + } + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/SetTimerReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/SetTimerReqMsgHdlr.scala new file mode 100644 index 000000000000..c4bf82ef7528 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/SetTimerReqMsgHdlr.scala @@ -0,0 +1,38 @@ +package org.bigbluebutton.core.apps.timer + +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.bus.MessageBus +import org.bigbluebutton.core.running.LiveMeeting +import org.bigbluebutton.core.apps.{ TimerModel, PermissionCheck, RightsManagementTrait } + +trait SetTimerReqMsgHdlr extends RightsManagementTrait { + this: TimerApp2x => + + def handle(msg: SetTimerReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { + log.debug("Received setTimerReqMsg {}", SetTimerReqMsg) + def broadcastEvent( + time: Int + ): Unit = { + val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka") + val envelope = BbbCoreEnvelope(SetTimerRespMsg.NAME, routing) + val header = BbbCoreHeaderWithMeetingId( + SetTimerRespMsg.NAME, + liveMeeting.props.meetingProp.intId + ) + val body = SetTimerRespMsgBody(msg.header.userId, time) + val event = SetTimerRespMsg(header, body) + val msgEvent = BbbCommonEnvCoreMsg(envelope, event) + bus.outGW.send(msgEvent) + } + + if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) && + 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 or moderator to set timer" + PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) + } else { + TimerModel.setTime(liveMeeting.timerModel, msg.body.time) + broadcastEvent(msg.body.time) + } + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/SetTrackReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/SetTrackReqMsgHdlr.scala new file mode 100644 index 000000000000..bf2c50f6aa53 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/SetTrackReqMsgHdlr.scala @@ -0,0 +1,38 @@ +package org.bigbluebutton.core.apps.timer + +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.bus.MessageBus +import org.bigbluebutton.core.running.LiveMeeting +import org.bigbluebutton.core.apps.{ TimerModel, PermissionCheck, RightsManagementTrait } + +trait SetTrackReqMsgHdlr extends RightsManagementTrait { + this: TimerApp2x => + + def handle(msg: SetTrackReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { + log.debug("Received setTrackReqMsg {}", SetTrackReqMsg) + def broadcastEvent( + track: String + ): Unit = { + val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka") + val envelope = BbbCoreEnvelope(SetTrackRespMsg.NAME, routing) + val header = BbbCoreHeaderWithMeetingId( + SetTrackRespMsg.NAME, + liveMeeting.props.meetingProp.intId + ) + val body = SetTrackRespMsgBody(msg.header.userId, track) + val event = SetTrackRespMsg(header, body) + val msgEvent = BbbCommonEnvCoreMsg(envelope, event) + bus.outGW.send(msgEvent) + } + + if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) && + 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 or moderator to set track" + PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) + } else { + TimerModel.setTrack(liveMeeting.timerModel, msg.body.track) + broadcastEvent(msg.body.track) + } + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/StartTimerReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/StartTimerReqMsgHdlr.scala new file mode 100644 index 000000000000..03bee1be3a97 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/StartTimerReqMsgHdlr.scala @@ -0,0 +1,37 @@ +package org.bigbluebutton.core.apps.timer + +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.bus.MessageBus +import org.bigbluebutton.core.running.LiveMeeting +import org.bigbluebutton.core.apps.{ TimerModel, PermissionCheck, RightsManagementTrait } + +trait StartTimerReqMsgHdlr extends RightsManagementTrait { + this: TimerApp2x => + + def handle(msg: StartTimerReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { + log.debug("Received startTimerReqMsg {}", StartTimerReqMsg) + def broadcastEvent(): Unit = { + val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka") + val envelope = BbbCoreEnvelope(StartTimerRespMsg.NAME, routing) + val header = BbbCoreHeaderWithMeetingId( + StartTimerRespMsg.NAME, + liveMeeting.props.meetingProp.intId + ) + val body = StartTimerRespMsgBody(msg.header.userId) + val event = StartTimerRespMsg(header, body) + val msgEvent = BbbCommonEnvCoreMsg(envelope, event) + bus.outGW.send(msgEvent) + } + + if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) && + 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 or moderator to start timer" + PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) + } else { + TimerModel.setStartedAt(liveMeeting.timerModel, System.currentTimeMillis()) + TimerModel.setRunning(liveMeeting.timerModel, true) + broadcastEvent() + } + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/StopTimerReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/StopTimerReqMsgHdlr.scala new file mode 100644 index 000000000000..660b06339e0d --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/StopTimerReqMsgHdlr.scala @@ -0,0 +1,40 @@ +package org.bigbluebutton.core.apps.timer + +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.bus.MessageBus +import org.bigbluebutton.core.running.LiveMeeting +import org.bigbluebutton.core.apps.{ TimerModel, PermissionCheck, RightsManagementTrait } + +trait StopTimerReqMsgHdlr extends RightsManagementTrait { + this: TimerApp2x => + + def handle(msg: StopTimerReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { + log.debug("Received stopTimerReqMsg {}", StopTimerReqMsg) + def broadcastEvent( + accumulated: Int + ): Unit = { + val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka") + val envelope = BbbCoreEnvelope(StopTimerRespMsg.NAME, routing) + val header = BbbCoreHeaderWithMeetingId( + StopTimerRespMsg.NAME, + liveMeeting.props.meetingProp.intId + ) + val body = StopTimerRespMsgBody(msg.header.userId, accumulated) + val event = StopTimerRespMsg(header, body) + val msgEvent = BbbCommonEnvCoreMsg(envelope, event) + bus.outGW.send(msgEvent) + } + + if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) && + permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId) && + msg.header.userId != "nodeJSapp") { + val meetingId = liveMeeting.props.meetingProp.intId + val reason = "You need to be the presenter or moderator to stop timer" + PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) + } else { + TimerModel.setAccumulated(liveMeeting.timerModel, msg.body.accumulated) + TimerModel.setRunning(liveMeeting.timerModel, false) + broadcastEvent(msg.body.accumulated) + } + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/SwitchTimerReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/SwitchTimerReqMsgHdlr.scala new file mode 100644 index 000000000000..0e129c858d02 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/SwitchTimerReqMsgHdlr.scala @@ -0,0 +1,42 @@ +package org.bigbluebutton.core.apps.timer + +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.bus.MessageBus +import org.bigbluebutton.core.running.LiveMeeting +import org.bigbluebutton.core.apps.{ TimerModel, PermissionCheck, RightsManagementTrait } + +trait SwitchTimerReqMsgHdlr extends RightsManagementTrait { + this: TimerApp2x => + + def handle(msg: SwitchTimerReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { + log.debug("Received switchTimerReqMsg {}", SwitchTimerReqMsg) + def broadcastEvent( + stopwatch: Boolean + ): Unit = { + val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka") + val envelope = BbbCoreEnvelope(SwitchTimerRespMsg.NAME, routing) + val header = BbbCoreHeaderWithMeetingId( + SwitchTimerRespMsg.NAME, + liveMeeting.props.meetingProp.intId + ) + val body = SwitchTimerRespMsgBody(msg.header.userId, stopwatch) + val event = SwitchTimerRespMsg(header, body) + val msgEvent = BbbCommonEnvCoreMsg(envelope, event) + bus.outGW.send(msgEvent) + } + + if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) && + 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 or moderator to switch timer" + PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) + } else { + if (TimerModel.getStopwatch(liveMeeting.timerModel) != msg.body.stopwatch) { + TimerModel.setStopwatch(liveMeeting.timerModel, msg.body.stopwatch) + broadcastEvent(msg.body.stopwatch) + } else { + log.debug("Timer is already in this stopwatch mode"); + } + } + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/TimerApp2x.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/TimerApp2x.scala new file mode 100644 index 000000000000..fbefacbcc16a --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/TimerApp2x.scala @@ -0,0 +1,19 @@ +package org.bigbluebutton.core.apps.timer + +import akka.actor.ActorContext +import akka.event.Logging + +class TimerApp2x(implicit val context: ActorContext) + extends CreateTimerPubMsgHdlr + with ActivateTimerReqMsgHdlr + with DeactivateTimerReqMsgHdlr + with StartTimerReqMsgHdlr + with StopTimerReqMsgHdlr + with SwitchTimerReqMsgHdlr + with SetTimerReqMsgHdlr + with ResetTimerReqMsgHdlr + with SetTrackReqMsgHdlr + with TimerEndedPubMsgHdlr { + + val log = Logging(context.system, getClass) +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/TimerEndedPubMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/TimerEndedPubMsgHdlr.scala new file mode 100644 index 000000000000..42a8f66b328c --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/TimerEndedPubMsgHdlr.scala @@ -0,0 +1,29 @@ +package org.bigbluebutton.core.apps.timer + +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.bus.MessageBus +import org.bigbluebutton.core.running.LiveMeeting +import org.bigbluebutton.core.apps.{ TimerModel, PermissionCheck, RightsManagementTrait } + +trait TimerEndedPubMsgHdlr extends RightsManagementTrait { + this: TimerApp2x => + + def handle(msg: TimerEndedPubMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { + log.debug("Received timerEndedPubMsg {}", TimerEndedPubMsg) + def broadcastEvent(): Unit = { + val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka") + val envelope = BbbCoreEnvelope(TimerEndedEvtMsg.NAME, routing) + val header = BbbCoreHeaderWithMeetingId( + TimerEndedEvtMsg.NAME, + liveMeeting.props.meetingProp.intId + ) + val body = TimerEndedEvtMsgBody() + val event = TimerEndedEvtMsg(header, body) + val msgEvent = BbbCommonEnvCoreMsg(envelope, event) + bus.outGW.send(msgEvent) + } + + TimerModel.setEndedAt(liveMeeting.timerModel, System.currentTimeMillis()) + broadcastEvent() + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeLockSettingsInMeetingCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeLockSettingsInMeetingCmdMsgHdlr.scala index 07ace7b4839b..560b1e639698 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeLockSettingsInMeetingCmdMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeLockSettingsInMeetingCmdMsgHdlr.scala @@ -31,7 +31,8 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait { hideUserList = msg.body.hideUserList, lockOnJoin = msg.body.lockOnJoin, lockOnJoinConfigurable = msg.body.lockOnJoinConfigurable, - hideViewersCursor = msg.body.hideViewersCursor + hideViewersCursor = msg.body.hideViewersCursor, + hideViewersAnnotation = msg.body.hideViewersAnnotation ) if (!MeetingStatus2x.permissionsEqual(liveMeeting.status, settings) || !MeetingStatus2x.permisionsInitialized(liveMeeting.status)) { @@ -224,6 +225,7 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait { lockOnJoin = settings.lockOnJoin, lockOnJoinConfigurable = settings.lockOnJoinConfigurable, hideViewersCursor = settings.hideViewersCursor, + hideViewersAnnotation = settings.hideViewersAnnotation, msg.body.setBy ) val header = BbbClientMsgHeader( diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeUserAwayReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeUserAwayReqMsgHdlr.scala new file mode 100644 index 000000000000..81e30beab015 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeUserAwayReqMsgHdlr.scala @@ -0,0 +1,50 @@ +package org.bigbluebutton.core.apps.users + +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.apps.RightsManagementTrait +import org.bigbluebutton.core.models.{ UserState, Users2x } +import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter } +import org.bigbluebutton.core2.message.senders.MsgBuilder + +trait ChangeUserAwayReqMsgHdlr extends RightsManagementTrait { + this: UsersApp => + + val liveMeeting: LiveMeeting + val outGW: OutMsgRouter + + def handleChangeUserAwayReqMsg(msg: ChangeUserAwayReqMsg): Unit = { + log.info("handleChangeUserAwayReqMsg: away={} userId={}", msg.body.away, msg.body.userId) + + def broadcast(user: UserState, away: Boolean): Unit = { + val routingChange = Routing.addMsgToClientRouting( + MessageTypes.BROADCAST_TO_MEETING, + liveMeeting.props.meetingProp.intId, user.intId + ) + val envelopeChange = BbbCoreEnvelope(UserAwayChangedEvtMsg.NAME, routingChange) + val headerChange = BbbClientMsgHeader(UserAwayChangedEvtMsg.NAME, liveMeeting.props.meetingProp.intId, + user.intId) + + val bodyChange = UserAwayChangedEvtMsgBody(user.intId, away) + val eventChange = UserAwayChangedEvtMsg(headerChange, bodyChange) + val msgEventChange = BbbCommonEnvCoreMsg(envelopeChange, eventChange) + outGW.send(msgEventChange) + } + + for { + user <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId) + newUserState <- Users2x.setUserAway(liveMeeting.users2x, user.intId, msg.body.away) + } yield { + if (msg.body.away && user.emoji == "") { + Users2x.setEmojiStatus(liveMeeting.users2x, msg.body.userId, "away") + outGW.send(MsgBuilder.buildUserEmojiChangedEvtMsg(liveMeeting.props.meetingProp.intId, msg.body.userId, "away")) + } + + if (msg.body.away == false && user.emoji == "away") { + Users2x.setEmojiStatus(liveMeeting.users2x, msg.body.userId, "none") + outGW.send(MsgBuilder.buildUserEmojiChangedEvtMsg(liveMeeting.props.meetingProp.intId, msg.body.userId, "none")) + } + + broadcast(newUserState, msg.body.away) + } + } +} \ No newline at end of file diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeUserEmojiCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeUserEmojiCmdMsgHdlr.scala index 80b6862f9171..2e82e780de9d 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeUserEmojiCmdMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeUserEmojiCmdMsgHdlr.scala @@ -4,6 +4,7 @@ import org.bigbluebutton.common2.msgs._ import org.bigbluebutton.core.models.Users2x import org.bigbluebutton.core.running.{ BaseMeetingActor, LiveMeeting, OutMsgRouter } import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait } +import org.bigbluebutton.core2.message.senders.MsgBuilder trait ChangeUserEmojiCmdMsgHdlr extends RightsManagementTrait { this: BaseMeetingActor => @@ -28,31 +29,37 @@ trait ChangeUserEmojiCmdMsgHdlr extends RightsManagementTrait { msg.header.userId ) - val initialEmojiState = Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId).get.emoji - val nextEmojiState = msg.body.emoji + for { + user <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId) + } yield { + val initialEmojiState = user.emoji + val nextEmojiState = msg.body.emoji - if (isUserSettingOwnEmoji - || isUserModerator && nextEmojiState.equals("none") - || isUserPresenter && initialEmojiState.equals("raiseHand") && nextEmojiState.equals("none")) { - for { - uvo <- Users2x.setEmojiStatus(liveMeeting.users2x, msg.body.userId, msg.body.emoji) - } yield { - sendUserEmojiChangedEvtMsg(outGW, liveMeeting.props.meetingProp.intId, msg.body.userId, msg.body.emoji) + if (isUserSettingOwnEmoji + || isUserModerator && nextEmojiState.equals("none") + || isUserPresenter && initialEmojiState.equals("raiseHand") && nextEmojiState.equals("none")) { + for { + uvo <- Users2x.setEmojiStatus(liveMeeting.users2x, msg.body.userId, msg.body.emoji) + } yield { + outGW.send(MsgBuilder.buildUserEmojiChangedEvtMsg(liveMeeting.props.meetingProp.intId, msg.body.userId, msg.body.emoji)) + + if (initialEmojiState == "raiseHand" || nextEmojiState == "raiseHand") { + Users2x.setUserRaiseHand(liveMeeting.users2x, msg.body.userId, msg.body.emoji == "raiseHand") + outGW.send(MsgBuilder.buildUserRaiseHandChangedEvtMsg(liveMeeting.props.meetingProp.intId, msg.body.userId, msg.body.emoji == "raiseHand")) + } + + if (initialEmojiState == "away" || nextEmojiState == "away") { + Users2x.setUserAway(liveMeeting.users2x, msg.body.userId, msg.body.emoji == "away") + outGW.send(MsgBuilder.buildUserAwayChangedEvtMsg(liveMeeting.props.meetingProp.intId, msg.body.userId, msg.body.emoji == "away")) + } + + } + } else { + val meetingId = liveMeeting.props.meetingProp.intId + val reason = "No permission to clear change user emoji status." + PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting) } - } else { - val meetingId = liveMeeting.props.meetingProp.intId - val reason = "No permission to clear change user emoji status." - PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting) } } - def sendUserEmojiChangedEvtMsg(outGW: OutMsgRouter, meetingId: String, userId: String, emoji: String): Unit = { - val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId) - val envelope = BbbCoreEnvelope(UserEmojiChangedEvtMsg.NAME, routing) - val header = BbbClientMsgHeader(UserEmojiChangedEvtMsg.NAME, meetingId, userId) - val body = UserEmojiChangedEvtMsgBody(userId, emoji) - val event = UserEmojiChangedEvtMsg(header, body) - val msgEvent = BbbCommonEnvCoreMsg(envelope, event) - outGW.send(msgEvent) - } } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeUserRaiseHandReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeUserRaiseHandReqMsgHdlr.scala new file mode 100644 index 000000000000..1eab60093b8f --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeUserRaiseHandReqMsgHdlr.scala @@ -0,0 +1,74 @@ +package org.bigbluebutton.core.apps.users + +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait } +import org.bigbluebutton.core.models.{ UserState, Users2x } +import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter } +import org.bigbluebutton.core2.message.senders.MsgBuilder + +trait ChangeUserRaiseHandReqMsgHdlr extends RightsManagementTrait { + this: UsersApp => + + val liveMeeting: LiveMeeting + val outGW: OutMsgRouter + + def handleChangeUserRaiseHandReqMsg(msg: ChangeUserRaiseHandReqMsg): Unit = { + log.info("handleChangeUserRaiseHandReqMsg: raiseHand={} userId={}", msg.body.raiseHand, msg.body.userId) + + def broadcast(user: UserState, raiseHand: Boolean): Unit = { + val routingChange = Routing.addMsgToClientRouting( + MessageTypes.BROADCAST_TO_MEETING, + liveMeeting.props.meetingProp.intId, user.intId + ) + val envelopeChange = BbbCoreEnvelope(UserRaiseHandChangedEvtMsg.NAME, routingChange) + val headerChange = BbbClientMsgHeader(UserRaiseHandChangedEvtMsg.NAME, liveMeeting.props.meetingProp.intId, + user.intId) + + val bodyChange = UserRaiseHandChangedEvtMsgBody(user.intId, raiseHand) + val eventChange = UserRaiseHandChangedEvtMsg(headerChange, bodyChange) + val msgEventChange = BbbCommonEnvCoreMsg(envelopeChange, eventChange) + outGW.send(msgEventChange) + } + + val isUserSettingOwnProps = (msg.header.userId == msg.body.userId) + + val isUserModerator = !permissionFailed( + PermissionCheck.MOD_LEVEL, + PermissionCheck.VIEWER_LEVEL, + liveMeeting.users2x, + msg.header.userId + ) + + val isUserPresenter = !permissionFailed( + PermissionCheck.VIEWER_LEVEL, + PermissionCheck.PRESENTER_LEVEL, + liveMeeting.users2x, + msg.header.userId + ) + + if (isUserSettingOwnProps || isUserModerator || isUserPresenter) { + for { + user <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId) + newUserState <- Users2x.setUserRaiseHand(liveMeeting.users2x, user.intId, msg.body.raiseHand) + } yield { + + if (msg.body.raiseHand && user.emoji == "") { + Users2x.setEmojiStatus(liveMeeting.users2x, msg.body.userId, "raiseHand") + outGW.send(MsgBuilder.buildUserEmojiChangedEvtMsg(liveMeeting.props.meetingProp.intId, msg.body.userId, "raiseHand")) + } + + if (msg.body.raiseHand == false && user.emoji == "raiseHand") { + Users2x.setEmojiStatus(liveMeeting.users2x, msg.body.userId, "none") + outGW.send(MsgBuilder.buildUserEmojiChangedEvtMsg(liveMeeting.props.meetingProp.intId, msg.body.userId, "none")) + } + + broadcast(newUserState, msg.body.raiseHand) + } + } else { + val meetingId = liveMeeting.props.meetingProp.intId + val reason = "No permission to change user raiseHand prop." + PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting) + } + + } +} \ No newline at end of file diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeUserReactionEmojiReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeUserReactionEmojiReqMsgHdlr.scala new file mode 100644 index 000000000000..7fd7ad99c5f3 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeUserReactionEmojiReqMsgHdlr.scala @@ -0,0 +1,41 @@ +package org.bigbluebutton.core.apps.users + +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.apps.RightsManagementTrait +import org.bigbluebutton.core.models.{ UserState, Users2x } +import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter } + +trait ChangeUserReactionEmojiReqMsgHdlr extends RightsManagementTrait { + this: UsersApp => + + val liveMeeting: LiveMeeting + val outGW: OutMsgRouter + + def handleChangeUserReactionEmojiReqMsg(msg: ChangeUserReactionEmojiReqMsg): Unit = { + log.info("handleChangeUserReactionEmojiReqMsg: reactionEmoji={} userId={}", msg.body.reactionEmoji, msg.body.userId) + + def broadcast(user: UserState, reactionEmoji: String): Unit = { + val routingChange = Routing.addMsgToClientRouting( + MessageTypes.BROADCAST_TO_MEETING, + liveMeeting.props.meetingProp.intId, user.intId + ) + val envelopeChange = BbbCoreEnvelope(UserReactionEmojiChangedEvtMsg.NAME, routingChange) + val headerChange = BbbClientMsgHeader(UserReactionEmojiChangedEvtMsg.NAME, liveMeeting.props.meetingProp.intId, + user.intId) + + val bodyChange = UserReactionEmojiChangedEvtMsgBody(user.intId, reactionEmoji) + val eventChange = UserReactionEmojiChangedEvtMsg(headerChange, bodyChange) + val msgEventChange = BbbCommonEnvCoreMsg(envelopeChange, eventChange) + outGW.send(msgEventChange) + } + + for { + user <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId) + newUserState <- Users2x.setReactionEmoji(liveMeeting.users2x, user.intId, msg.body.reactionEmoji) + } yield { + if (user.reactionEmoji != msg.body.reactionEmoji) { + broadcast(newUserState, msg.body.reactionEmoji) + } + } + } +} \ No newline at end of file diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ClearAllUsersEmojiCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ClearAllUsersEmojiCmdMsgHdlr.scala new file mode 100644 index 000000000000..b50d6d1c20c6 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ClearAllUsersEmojiCmdMsgHdlr.scala @@ -0,0 +1,47 @@ +package org.bigbluebutton.core.apps.users + +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.models.Users2x +import org.bigbluebutton.core.running.{ BaseMeetingActor, LiveMeeting, OutMsgRouter } +import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait } + +trait ClearAllUsersEmojiCmdMsgHdlr extends RightsManagementTrait { + this: BaseMeetingActor => + + val liveMeeting: LiveMeeting + val outGW: OutMsgRouter + + def handleClearAllUsersEmojiCmdMsg(msg: ClearAllUsersEmojiCmdMsg) { + val isUserModerator = !permissionFailed( + PermissionCheck.MOD_LEVEL, + PermissionCheck.VIEWER_LEVEL, + liveMeeting.users2x, + msg.header.userId + ) + + if (isUserModerator) { + for { + user <- Users2x.findAll(liveMeeting.users2x) + } yield { + Users2x.setEmojiStatus(liveMeeting.users2x, user.intId, "none") + Users2x.setUserAway(liveMeeting.users2x, user.intId, false) + Users2x.setUserRaiseHand(liveMeeting.users2x, user.intId, false) + } + sendClearedAllUsersEmojiEvtMsg(outGW, liveMeeting.props.meetingProp.intId, msg.header.userId) + } else { + val meetingId = liveMeeting.props.meetingProp.intId + val reason = "No permission to clear users reactions." + PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting) + } + } + + def sendClearedAllUsersEmojiEvtMsg(outGW: OutMsgRouter, meetingId: String, userId: String): Unit = { + val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId) + val envelope = BbbCoreEnvelope(ClearedAllUsersEmojiEvtMsg.NAME, routing) + val header = BbbClientMsgHeader(ClearedAllUsersEmojiEvtMsg.NAME, meetingId, userId) + val body = ClearedAllUsersEmojiEvtMsgBody() + val event = ClearedAllUsersEmojiEvtMsg(header, body) + val msgEvent = BbbCommonEnvCoreMsg(envelope, event) + outGW.send(msgEvent) + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ClearAllUsersReactionCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ClearAllUsersReactionCmdMsgHdlr.scala new file mode 100644 index 000000000000..aeaa483281a0 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ClearAllUsersReactionCmdMsgHdlr.scala @@ -0,0 +1,46 @@ +package org.bigbluebutton.core.apps.users + +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait } +import org.bigbluebutton.core.models.Users2x +import org.bigbluebutton.core.running.{ BaseMeetingActor, LiveMeeting, OutMsgRouter } + +trait ClearAllUsersReactionCmdMsgHdlr extends RightsManagementTrait { + this: BaseMeetingActor => + + val liveMeeting: LiveMeeting + val outGW: OutMsgRouter + + def handleClearAllUsersReactionCmdMsg(msg: ClearAllUsersReactionCmdMsg) { + val isUserModerator = !permissionFailed( + PermissionCheck.MOD_LEVEL, + PermissionCheck.VIEWER_LEVEL, + liveMeeting.users2x, + msg.header.userId + ) + + if (isUserModerator) { + for { + user <- Users2x.findAll(liveMeeting.users2x) + } yield { + //Don't clear away and RaiseHand + Users2x.setReactionEmoji(liveMeeting.users2x, user.intId, "none") + } + sendClearedAllUsersReactionEvtMsg(outGW, liveMeeting.props.meetingProp.intId, msg.header.userId) + } else { + val meetingId = liveMeeting.props.meetingProp.intId + val reason = "No permission to clear users reactions." + PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting) + } + } + + def sendClearedAllUsersReactionEvtMsg(outGW: OutMsgRouter, meetingId: String, userId: String): Unit = { + val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId) + val envelope = BbbCoreEnvelope(ClearedAllUsersReactionEvtMsg.NAME, routing) + val header = BbbClientMsgHeader(ClearedAllUsersReactionEvtMsg.NAME, meetingId, userId) + val body = ClearedAllUsersReactionEvtMsgBody() + val event = ClearedAllUsersReactionEvtMsg(header, body) + val msgEvent = BbbCommonEnvCoreMsg(envelope, event) + outGW.send(msgEvent) + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/GetLockSettingsReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/GetLockSettingsReqMsgHdlr.scala index 99d1d4553598..a20b6fb07866 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/GetLockSettingsReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/GetLockSettingsReqMsgHdlr.scala @@ -24,7 +24,8 @@ trait GetLockSettingsReqMsgHdlr { hideUserList = settings.hideUserList, lockOnJoin = settings.lockOnJoin, lockOnJoinConfigurable = settings.lockOnJoinConfigurable, - hideViewersCursor = settings.hideViewersCursor + hideViewersCursor = settings.hideViewersCursor, + hideViewersAnnotation = settings.hideViewersAnnotation ) val header = BbbClientMsgHeader(GetLockSettingsRespMsg.NAME, meetingId, requestedBy) val event = GetLockSettingsRespMsg(header, body) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/SetUserSpeechLocaleMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/SetUserSpeechLocaleMsgHdlr.scala new file mode 100644 index 000000000000..212ab2e5480f --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/SetUserSpeechLocaleMsgHdlr.scala @@ -0,0 +1,41 @@ +package org.bigbluebutton.core.apps.users + +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.models.{ UserState, Users2x } +import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter } +import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait } +import org.bigbluebutton.core.domain.MeetingState2x + +trait SetUserSpeechLocaleMsgHdlr extends RightsManagementTrait { + this: UsersApp => + + val liveMeeting: LiveMeeting + val outGW: OutMsgRouter + + def handleSetUserSpeechLocaleReqMsg(msg: SetUserSpeechLocaleReqMsg): Unit = { + log.info("handleSetUserSpeechLocaleReqMsg: locale={} provider={} userId={}", msg.body.locale, msg.body.provider, msg.header.userId) + + def broadcastUserSpeechLocaleChanged(user: UserState, locale: String, provider: String): Unit = { + val routingChange = Routing.addMsgToClientRouting( + MessageTypes.BROADCAST_TO_MEETING, + liveMeeting.props.meetingProp.intId, user.intId + ) + val envelopeChange = BbbCoreEnvelope(UserSpeechLocaleChangedEvtMsg.NAME, routingChange) + val headerChange = BbbClientMsgHeader(UserSpeechLocaleChangedEvtMsg.NAME, liveMeeting.props.meetingProp.intId, user.intId) + + val bodyChange = UserSpeechLocaleChangedEvtMsgBody(locale, provider) + val eventChange = UserSpeechLocaleChangedEvtMsg(headerChange, bodyChange) + val msgEventChange = BbbCommonEnvCoreMsg(envelopeChange, eventChange) + outGW.send(msgEventChange) + } + + for { + user <- Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId) + } yield { + var changeLocale: Option[UserState] = None; + changeLocale = Users2x.setUserSpeechLocale(liveMeeting.users2x, msg.header.userId, msg.body.locale) + broadcastUserSpeechLocaleChanged(user, msg.body.locale, msg.body.provider) + } + + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserConnectedToGlobalAudioMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserConnectedToGlobalAudioMsgHdlr.scala index 63a6f57abbd2..0685f7e7f12f 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserConnectedToGlobalAudioMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserConnectedToGlobalAudioMsgHdlr.scala @@ -9,9 +9,9 @@ trait UserConnectedToGlobalAudioMsgHdlr { val outGW: OutMsgRouter - def handleUserConnectedToGlobalAudioMsg(msg: UserConnectedToGlobalAudioMsg) { + def handleUserConnectedToGlobalAudioMsg(msg: UserConnectedToGlobalAudioMsg): Unit = { log.info("Handling UserConnectedToGlobalAudio: meetingId=" + props.meetingProp.intId + " userId=" + msg.body.userId) - + def broadcastEvent(vu: VoiceUserState): Unit = { val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, props.meetingProp.intId, vu.intId) @@ -44,6 +44,8 @@ trait UserConnectedToGlobalAudioMsgHdlr { System.currentTimeMillis(), floor = false, lastFloorTime = "0", + hold = false, + uuid = "unused" ) VoiceUsers.add(liveMeeting.voiceUsers, vu) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserReactionTimeExpiredCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserReactionTimeExpiredCmdMsgHdlr.scala new file mode 100644 index 000000000000..0f1902a3e1d5 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserReactionTimeExpiredCmdMsgHdlr.scala @@ -0,0 +1,19 @@ +package org.bigbluebutton.core.apps.users + +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.models.Users2x +import org.bigbluebutton.core.running.{ BaseMeetingActor, LiveMeeting, OutMsgRouter } +import org.bigbluebutton.core.apps.{ RightsManagementTrait } + +trait UserReactionTimeExpiredCmdMsgHdlr extends RightsManagementTrait { + this: BaseMeetingActor => + + val liveMeeting: LiveMeeting + + def handleUserReactionTimeExpiredCmdMsg(msg: UserReactionTimeExpiredCmdMsg) { + val isNodeUser = msg.header.userId.equals("nodeJSapp") + if (isNodeUser) { + Users2x.setReactionEmoji(liveMeeting.users2x, msg.body.userId, "none") + } + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala index 5d821efc3581..daccca66b925 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala @@ -149,6 +149,7 @@ class UsersApp( with GetUsersMeetingReqMsgHdlr with RegisterUserReqMsgHdlr with ChangeUserRoleCmdMsgHdlr + with SetUserSpeechLocaleMsgHdlr with SyncGetUsersMeetingRespMsgHdlr with LogoutAndEndMeetingCmdMsgHdlr with SetRecordingStatusCmdMsgHdlr @@ -159,6 +160,9 @@ class UsersApp( with AssignPresenterReqMsgHdlr with ChangeUserPinStateReqMsgHdlr with ChangeUserMobileFlagReqMsgHdlr + with ChangeUserReactionEmojiReqMsgHdlr + with ChangeUserRaiseHandReqMsgHdlr + with ChangeUserAwayReqMsgHdlr with EjectUserFromMeetingCmdMsgHdlr with EjectUserFromMeetingSysMsgHdlr with MuteUserCmdMsgHdlr { diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp2x.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp2x.scala index a5d1872f27c1..f2a4fe4f530d 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp2x.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp2x.scala @@ -7,7 +7,10 @@ trait UsersApp2x with LockUserInMeetingCmdMsgHdlr with LockUsersInMeetingCmdMsgHdlr with GetLockSettingsReqMsgHdlr - with ChangeUserEmojiCmdMsgHdlr { + with ChangeUserEmojiCmdMsgHdlr + with ClearAllUsersEmojiCmdMsgHdlr + with ClearAllUsersReactionCmdMsgHdlr + with UserReactionTimeExpiredCmdMsgHdlr { this: MeetingActor => diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/ChannelHoldChangedVoiceConfEvtMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/ChannelHoldChangedVoiceConfEvtMsgHdlr.scala new file mode 100644 index 000000000000..41383d9c4f0d --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/ChannelHoldChangedVoiceConfEvtMsgHdlr.scala @@ -0,0 +1,21 @@ +package org.bigbluebutton.core.apps.voice + +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.running.{ MeetingActor, LiveMeeting, OutMsgRouter } + +trait ChannelHoldChangedVoiceConfEvtMsgHdlr { + this: MeetingActor => + + val liveMeeting: LiveMeeting + val outGW: OutMsgRouter + + def handleChannelHoldChangedVoiceConfEvtMsg(msg: ChannelHoldChangedVoiceConfEvtMsg): Unit = { + VoiceApp.handleChannelHoldChanged( + liveMeeting, + outGW, + msg.body.intId, + msg.body.uuid, + msg.body.hold + ) + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/ListenOnlyModeToggledInSfuEvtMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/ListenOnlyModeToggledInSfuEvtMsgHdlr.scala new file mode 100644 index 000000000000..0b16a39a6155 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/ListenOnlyModeToggledInSfuEvtMsgHdlr.scala @@ -0,0 +1,25 @@ +package org.bigbluebutton.core.apps.voice + +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.models.VoiceUsers +import org.bigbluebutton.core.running.{ BaseMeetingActor, LiveMeeting, OutMsgRouter } + +trait ListenOnlyModeToggledInSfuEvtMsgHdlr { + this: BaseMeetingActor => + + val liveMeeting: LiveMeeting + val outGW: OutMsgRouter + + def handleListenOnlyModeToggledInSfuEvtMsg(msg: ListenOnlyModeToggledInSfuEvtMsg): Unit = { + for { + vu <- VoiceUsers.findWithIntId(liveMeeting.voiceUsers, msg.body.userId) + } yield { + VoiceApp.holdChannelInVoiceConf( + liveMeeting, + outGW, + vu.uuid, + msg.body.enabled + ) + } + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/UserJoinedVoiceConfEvtMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/UserJoinedVoiceConfEvtMsgHdlr.scala index 6bfa2907eee5..3d838beba7aa 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/UserJoinedVoiceConfEvtMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/UserJoinedVoiceConfEvtMsgHdlr.scala @@ -49,6 +49,9 @@ trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration { authed = true, guestStatus = GuestStatus.WAIT, emoji = "none", + reactionEmoji = "none", + raiseHand = false, + away = false, pin = false, mobile = false, presenter = false, @@ -91,7 +94,9 @@ trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration { userColor, msg.body.muted, msg.body.talking, - "freeswitch" + "freeswitch", + msg.body.hold, + msg.body.uuid ) } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp.scala index 96fe80b810bc..96a2b121c934 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp.scala @@ -1,5 +1,6 @@ package org.bigbluebutton.core.apps.voice +import akka.actor.{ ActorContext, ActorSystem, Cancellable } import org.bigbluebutton.SystemConfiguration import org.bigbluebutton.LockSettingsUtil import org.bigbluebutton.core.apps.breakout.BreakoutHdlrHelpers @@ -12,9 +13,14 @@ import org.bigbluebutton.core.models._ import org.bigbluebutton.core.apps.users.UsersApp import org.bigbluebutton.core.util.ColorPicker import org.bigbluebutton.core.util.TimeUtil +import scala.collection.immutable.Map +import scala.concurrent.duration._ object VoiceApp extends SystemConfiguration { + // Key is userId + var toggleListenOnlyTasks: Map[String, Cancellable] = Map() + def genRecordPath( recordDir: String, meetingId: String, @@ -104,7 +110,7 @@ object VoiceApp extends SystemConfiguration { outGW: OutMsgRouter, voiceUserId: String, muted: Boolean - ): Unit = { + )(implicit context: ActorContext): Unit = { for { mutedUser <- VoiceUsers.userMuted(liveMeeting.voiceUsers, voiceUserId, muted) } yield { @@ -117,13 +123,32 @@ object VoiceApp extends SystemConfiguration { ) } - broadcastUserMutedVoiceEvtMsg( - liveMeeting.props.meetingProp.intId, - mutedUser, - liveMeeting.props.voiceProp.voiceConf, - outGW + // Ask for the audio channel to be switched to listen only mode + // if the user is muted, otherwise switch back to normal mode + // This is only effective if the "transparent listen only" mode is active + // for the target user. + toggleListenOnlyMode( + liveMeeting, + outGW, + mutedUser.intId, + muted, + toggleListenOnlyAfterMuteTimer ) + // If the user is muted or unmuted with an unheld channel, broadcast + // the event right away. + // If the user is unmuted, but channel is held, we need to wait for the + // channel to be active again to broadcast the event. See + // VoiceApp.handleChannelHoldChanged for this second case. + if (muted || (!muted && !mutedUser.hold)) { + broadcastUserMutedVoiceEvtMsg( + liveMeeting.props.meetingProp.intId, + mutedUser, + liveMeeting.props.voiceProp.voiceConf, + outGW + ) + } + } } @@ -132,7 +157,7 @@ object VoiceApp extends SystemConfiguration { outGW: OutMsgRouter, eventBus: InternalEventBus, users: Vector[ConfVoiceUser] - ): Unit = { + )(implicit context: ActorContext): Unit = { users foreach { cvu => VoiceUsers.findWithVoiceUserId( liveMeeting.voiceUsers, @@ -179,7 +204,9 @@ object VoiceApp extends SystemConfiguration { ColorPicker.nextColor(liveMeeting.props.meetingProp.intId), cvu.muted, cvu.talking, - cvu.calledInto + cvu.calledInto, + cvu.hold, + cvu.uuid, ) } } @@ -229,7 +256,9 @@ object VoiceApp extends SystemConfiguration { color: String, muted: Boolean, talking: Boolean, - callingInto: String + callingInto: String, + hold: Boolean, + uuid: String = "unused" ): Unit = { def broadcastEvent(voiceUserState: VoiceUserState): Unit = { @@ -289,7 +318,9 @@ object VoiceApp extends SystemConfiguration { callingInto, System.currentTimeMillis(), floor = false, - lastFloorTime = "0" + lastFloorTime = "0", + hold, + uuid ) VoiceUsers.add(liveMeeting.voiceUsers, voiceUserState) @@ -431,4 +462,108 @@ object VoiceApp extends SystemConfiguration { ) outGW.send(deafEvent) } + + def removeToggleListenOnlyTask(userId: String): Unit = { + toggleListenOnlyTasks get userId match { + case Some(task) => + task.cancel() + toggleListenOnlyTasks = toggleListenOnlyTasks - userId + case _ => + } + } + + def toggleListenOnlyMode( + liveMeeting: LiveMeeting, + outGW: OutMsgRouter, + userId: String, + enabled: Boolean, + delay: Int = 0 + )(implicit context: ActorContext): Unit = { + implicit def executionContext = context.system.dispatcher + def broacastEvent(): Unit = { + val event = MsgBuilder.buildToggleListenOnlyModeSysMsg( + liveMeeting.props.meetingProp.intId, + liveMeeting.props.voiceProp.voiceConf, + userId, + enabled + ) + outGW.send(event) + } + + // Guarantee there are no other tasks for this channel + removeToggleListenOnlyTask(userId) + + if (enabled && delay > 0) { + // If we are enabling listen only mode, we wait a bit before actually + // dispatching the command - the idea is that recently muted users + // are more likely to unmute themselves right after the action, so this + // should make frequent mute-unmute transitions smoother. + // This is just one of the heuristics we have to implement for this to + // work seamlessly, but it's a start. - prlanzarin Aug 04 2023 + val newTask = context.system.scheduler.scheduleOnce(delay seconds) { + broacastEvent() + removeToggleListenOnlyTask(userId) + } + + toggleListenOnlyTasks = toggleListenOnlyTasks + (userId -> newTask) + } else { + // If we are disabling listen only mode, we can broadcast the event + // right away + broacastEvent() + } + } + + def holdChannelInVoiceConf( + liveMeeting: LiveMeeting, + outGW: OutMsgRouter, + uuid: String, + hold: Boolean + ): Unit = { + val event = MsgBuilder.buildHoldChannelInVoiceConfSysMsg( + liveMeeting.props.meetingProp.intId, + liveMeeting.props.voiceProp.voiceConf, + uuid, + hold + ) + + outGW.send(event) + } + + def handleChannelHoldChanged( + liveMeeting: LiveMeeting, + outGW: OutMsgRouter, + intId: String, + uuid: String, + hold: Boolean + )(implicit context: ActorContext): Unit = { + VoiceUsers.holdStateChanged( + liveMeeting.voiceUsers, + intId, + uuid, + hold + ) match { + case Some(vu) => + // Mute vs hold state mismatch, enforce hold state again. + // Mute state is the predominant one here. + if (vu.muted != hold) { + toggleListenOnlyMode( + liveMeeting, + outGW, + intId, + vu.muted + ) + } + + // User unmuted and channel is not on hold, broadcast user unmuted + if (!vu.muted && !vu.hold) { + broadcastUserMutedVoiceEvtMsg( + liveMeeting.props.meetingProp.intId, + vu, + liveMeeting.props.voiceProp.voiceConf, + outGW + ) + } + case _ => + } + } } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp2x.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp2x.scala index 409f29112aac..301f0041cbc8 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp2x.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp2x.scala @@ -20,7 +20,9 @@ trait VoiceApp2x extends UserJoinedVoiceConfEvtMsgHdlr with SyncGetVoiceUsersMsgHdlr with AudioFloorChangedVoiceConfEvtMsgHdlr with VoiceConfCallStateEvtMsgHdlr - with UserStatusVoiceConfEvtMsgHdlr { + with UserStatusVoiceConfEvtMsgHdlr + with ChannelHoldChangedVoiceConfEvtMsgHdlr + with ListenOnlyModeToggledInSfuEvtMsgHdlr { this: MeetingActor => } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/domain/BreakoutRoom2x.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/domain/BreakoutRoom2x.scala index 3c1546a7d99b..009a2b018dae 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/domain/BreakoutRoom2x.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/domain/BreakoutRoom2x.scala @@ -18,7 +18,7 @@ case class BreakoutRoom2x( captureNotes: Boolean, captureSlides: Boolean, captureNotesFilename: String, - captureSlidesFilename: String + captureSlidesFilename: String, ) { } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/PresentationPods.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/PresentationPods.scala index 136b40e708ff..677446397117 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/PresentationPods.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/PresentationPods.scala @@ -62,6 +62,7 @@ case class PresentationInPod( pages: scala.collection.immutable.Map[String, PresentationPage], downloadable: Boolean, removable: Boolean, + filenameConverted: String = "", ) object PresentationPod { diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Users2x.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Users2x.scala index 9f94230fda4a..039499520c5b 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Users2x.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Users2x.scala @@ -178,10 +178,42 @@ object Users2x { u <- findWithIntId(users, intId) } yield { val newUser = u.modify(_.emoji).setTo(emoji) + users.save(newUser) newUser } } + def setReactionEmoji(users: Users2x, intId: String, reactionEmoji: String): Option[UserState] = { + for { + u <- findWithIntId(users, intId) + } yield { + val newUser = u.modify(_.reactionEmoji).setTo(reactionEmoji) + .modify(_.reactionChangedOn).setTo(System.currentTimeMillis()) + + users.save(newUser) + newUser + } + } + + def setUserRaiseHand(users: Users2x, intId: String, raiseHand: Boolean): Option[UserState] = { + for { + u <- findWithIntId(users, intId) + } yield { + val newUserState = u.modify(_.raiseHand).setTo(raiseHand) + users.save(newUserState) + newUserState + } + } + + def setUserAway(users: Users2x, intId: String, away: Boolean): Option[UserState] = { + for { + u <- findWithIntId(users, intId) + } yield { + val newUserState = u.modify(_.away).setTo(away) + users.save(newUserState) + newUserState + } + } def setUserLocked(users: Users2x, intId: String, locked: Boolean): Option[UserState] = { for { @@ -203,6 +235,16 @@ object Users2x { } } + def setUserSpeechLocale(users: Users2x, intId: String, locale: String): Option[UserState] = { + for { + u <- findWithIntId(users, intId) + } yield { + val newUser = u.modify(_.speechLocale).setTo(locale) + users.save(newUser) + newUser + } + } + def hasPresenter(users: Users2x): Boolean = { findPresenter(users) match { case Some(p) => true @@ -364,6 +406,10 @@ case class UserState( authed: Boolean, guestStatus: String, emoji: String, + reactionEmoji: String, + reactionChangedOn: Long = 0, + raiseHand: Boolean, + away: Boolean, locked: Boolean, presenter: Boolean, avatar: String, @@ -373,7 +419,8 @@ case class UserState( lastInactivityInspect: Long = 0, clientType: String, pickExempted: Boolean, - userLeftFlag: UserLeftFlag + userLeftFlag: UserLeftFlag, + speechLocale: String = "" ) case class UserIdAndName(id: String, name: String) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/VoiceUsers.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/VoiceUsers.scala index 9886fba1c4d1..8b9adbb15fd5 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/VoiceUsers.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/VoiceUsers.scala @@ -11,6 +11,10 @@ object VoiceUsers { users.toVector.find(u => u.intId == intId) } + def findWithIntIdAndUUID(users: VoiceUsers, intId: String, uuid: String): Option[VoiceUserState] = { + users.toVector.find(u => u.uuid == uuid && u.intId == intId) + } + def findAll(users: VoiceUsers): Vector[VoiceUserState] = users.toVector def findAllNonListenOnlyVoiceUsers(users: VoiceUsers): Vector[VoiceUserState] = users.toVector.filter(u => u.listenOnly == false) @@ -91,6 +95,17 @@ object VoiceUsers { } } + def holdStateChanged(users: VoiceUsers, intId: String, uuid: String, hold: Boolean): Option[VoiceUserState] = { + for { + u <- findWithIntIdAndUUID(users, intId, uuid) + } yield { + val vu = u.modify(_.hold).setTo(hold) + .modify(_.lastStatusUpdateOn).setTo(System.currentTimeMillis()) + users.save(vu) + vu + } + } + def setLastStatusUpdate(users: VoiceUsers, user: VoiceUserState): VoiceUserState = { val vu = user.copy(lastStatusUpdateOn = System.currentTimeMillis()) users.save(vu) @@ -165,7 +180,9 @@ case class VoiceUserVO2x( callingWith: String, listenOnly: Boolean, floor: Boolean, - lastFloorTime: String + lastFloorTime: String, + hold: Boolean, + uuid: String ) case class VoiceUserState( @@ -181,5 +198,7 @@ case class VoiceUserState( calledInto: String, lastStatusUpdateOn: Long, floor: Boolean, - lastFloorTime: String + lastFloorTime: String, + hold: Boolean, + uuid: String ) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala index 98808f3e900e..0897b4efd160 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala @@ -111,6 +111,8 @@ class ReceivedJsonMsgHandlerActor( routeGenericMsg[ChangeUserPinStateReqMsg](envelope, jsonNode) case ChangeUserMobileFlagReqMsg.NAME => routeGenericMsg[ChangeUserMobileFlagReqMsg](envelope, jsonNode) + case SetUserSpeechLocaleReqMsg.NAME => + routeGenericMsg[SetUserSpeechLocaleReqMsg](envelope, jsonNode) case SelectRandomViewerReqMsg.NAME => routeGenericMsg[SelectRandomViewerReqMsg](envelope, jsonNode) @@ -221,6 +223,10 @@ class ReceivedJsonMsgHandlerActor( routeGenericMsg[GetGlobalAudioPermissionReqMsg](envelope, jsonNode) case GetMicrophonePermissionReqMsg.NAME => routeGenericMsg[GetMicrophonePermissionReqMsg](envelope, jsonNode) + case ChannelHoldChangedVoiceConfEvtMsg.NAME => + routeVoiceMsg[ChannelHoldChangedVoiceConfEvtMsg](envelope, jsonNode) + case ListenOnlyModeToggledInSfuEvtMsg.NAME => + routeVoiceMsg[ListenOnlyModeToggledInSfuEvtMsg](envelope, jsonNode) // Breakout rooms case BreakoutRoomsListMsg.NAME => @@ -250,8 +256,20 @@ class ReceivedJsonMsgHandlerActor( case UserLeaveReqMsg.NAME => routeGenericMsg[UserLeaveReqMsg](envelope, jsonNode) + case ChangeUserRaiseHandReqMsg.NAME => + routeGenericMsg[ChangeUserRaiseHandReqMsg](envelope, jsonNode) + case ChangeUserAwayReqMsg.NAME => + routeGenericMsg[ChangeUserAwayReqMsg](envelope, jsonNode) case ChangeUserEmojiCmdMsg.NAME => routeGenericMsg[ChangeUserEmojiCmdMsg](envelope, jsonNode) + case ChangeUserReactionEmojiReqMsg.NAME => + routeGenericMsg[ChangeUserReactionEmojiReqMsg](envelope, jsonNode) + case UserReactionTimeExpiredCmdMsg.NAME => + routeGenericMsg[UserReactionTimeExpiredCmdMsg](envelope, jsonNode) + case ClearAllUsersEmojiCmdMsg.NAME => + routeGenericMsg[ClearAllUsersEmojiCmdMsg](envelope, jsonNode) + case ClearAllUsersReactionCmdMsg.NAME => + routeGenericMsg[ClearAllUsersReactionCmdMsg](envelope, jsonNode) case ChangeUserRoleCmdMsg.NAME => routeGenericMsg[ChangeUserRoleCmdMsg](envelope, jsonNode) @@ -314,10 +332,10 @@ class ReceivedJsonMsgHandlerActor( routeGenericMsg[PdfConversionInvalidErrorSysPubMsg](envelope, jsonNode) case AssignPresenterReqMsg.NAME => routeGenericMsg[AssignPresenterReqMsg](envelope, jsonNode) - case MakePresentationWithAnnotationDownloadReqMsg.NAME => - routeGenericMsg[MakePresentationWithAnnotationDownloadReqMsg](envelope, jsonNode) - case NewPresAnnFileAvailableMsg.NAME => - routeGenericMsg[NewPresAnnFileAvailableMsg](envelope, jsonNode) + case MakePresentationDownloadReqMsg.NAME => + routeGenericMsg[MakePresentationDownloadReqMsg](envelope, jsonNode) + case NewPresFileAvailableMsg.NAME => + routeGenericMsg[NewPresFileAvailableMsg](envelope, jsonNode) case PresAnnStatusMsg.NAME => routeGenericMsg[PresAnnStatusMsg](envelope, jsonNode) @@ -404,6 +422,29 @@ class ReceivedJsonMsgHandlerActor( routeGenericMsg[UpdateExternalVideoPubMsg](envelope, jsonNode) case StopExternalVideoPubMsg.NAME => routeGenericMsg[StopExternalVideoPubMsg](envelope, jsonNode) + + // Timer + case CreateTimerPubMsg.NAME => + routeGenericMsg[CreateTimerPubMsg](envelope, jsonNode) + case ActivateTimerReqMsg.NAME => + routeGenericMsg[ActivateTimerReqMsg](envelope, jsonNode) + case DeactivateTimerReqMsg.NAME => + routeGenericMsg[DeactivateTimerReqMsg](envelope, jsonNode) + case StartTimerReqMsg.NAME => + routeGenericMsg[StartTimerReqMsg](envelope, jsonNode) + case StopTimerReqMsg.NAME => + routeGenericMsg[StopTimerReqMsg](envelope, jsonNode) + case SwitchTimerReqMsg.NAME => + routeGenericMsg[SwitchTimerReqMsg](envelope, jsonNode) + case SetTimerReqMsg.NAME => + routeGenericMsg[SetTimerReqMsg](envelope, jsonNode) + case ResetTimerReqMsg.NAME => + routeGenericMsg[ResetTimerReqMsg](envelope, jsonNode) + case SetTrackReqMsg.NAME => + routeGenericMsg[SetTrackReqMsg](envelope, jsonNode) + case TimerEndedPubMsg.NAME => + routeGenericMsg[TimerEndedPubMsg](envelope, jsonNode) + case _ => log.error("Cannot route envelope name " + envelope.name) // do nothing diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AbstractTimerRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AbstractTimerRecordEvent.scala new file mode 100644 index 000000000000..d92d3624c2ef --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AbstractTimerRecordEvent.scala @@ -0,0 +1,24 @@ +/** + * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ + * + * Copyright (c) 2019 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 . + * + */ + +package org.bigbluebutton.core.record.events + +trait AbstractTimerRecordEvent extends RecordEvent { + setModule("TIMER") +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/ActivateTimerRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/ActivateTimerRecordEvent.scala new file mode 100644 index 000000000000..570478c247c3 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/ActivateTimerRecordEvent.scala @@ -0,0 +1,59 @@ +/** + * 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 . + * + */ + +package org.bigbluebutton.core.record.events + +class ActivateTimerRecordEvent extends AbstractTimerRecordEvent { + import ActivateTimerRecordEvent._ + + setEvent("ActivateTimerEvent") + + def setStopwatch(value: Boolean) { + eventMap.put(STOPWATCH, value.toString) + } + + def setRunning(value: Boolean) { + eventMap.put(RUNNING, value.toString) + } + + def setTime(value: Int) { + eventMap.put(TIME, value.toString) + } + + def setAccumulated(value: Int) { + eventMap.put(ACCUMULATED, value.toString) + } + + def setTimestamp(value: Int) { + eventMap.put(TIMESTAMP, value.toString) + } + + def setTrack(value: String) { + eventMap.put(TRACK, value) + } +} + +object ActivateTimerRecordEvent { + protected final val STOPWATCH = "stopwatch" + protected final val RUNNING = "running" + protected final val TIME = "time" + protected final val ACCUMULATED = "accumulated" + protected final val TIMESTAMP = "timestamp" + protected final val TRACK = "track" +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/DeactivateTimerRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/DeactivateTimerRecordEvent.scala new file mode 100644 index 000000000000..87b5fbbc6e12 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/DeactivateTimerRecordEvent.scala @@ -0,0 +1,24 @@ +/** + * 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 . + * + */ + +package org.bigbluebutton.core.record.events + +class DeactivateTimerRecordEvent extends AbstractTimerRecordEvent { + setEvent("DeactivateTimerEvent") +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/ResetTimerRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/ResetTimerRecordEvent.scala new file mode 100644 index 000000000000..f0dd570eadb2 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/ResetTimerRecordEvent.scala @@ -0,0 +1,24 @@ +/** + * 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 . + * + */ + +package org.bigbluebutton.core.record.events + +class ResetTimerRecordEvent extends AbstractTimerRecordEvent { + setEvent("ResetTimerEvent") +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/SetTimerRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/SetTimerRecordEvent.scala new file mode 100644 index 000000000000..1f5cbde414f8 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/SetTimerRecordEvent.scala @@ -0,0 +1,34 @@ +/** + * 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 . + * + */ + +package org.bigbluebutton.core.record.events + +class SetTimerRecordEvent extends AbstractTimerRecordEvent { + import SetTimerRecordEvent._ + + setEvent("SetTimerEvent") + + def setTime(value: Int) { + eventMap.put(TIME, value.toString) + } +} + +object SetTimerRecordEvent { + protected final val TIME = "time" +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/SetTimerTrackRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/SetTimerTrackRecordEvent.scala new file mode 100644 index 000000000000..60017235a9a8 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/SetTimerTrackRecordEvent.scala @@ -0,0 +1,34 @@ +/** + * 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 . + * + */ + +package org.bigbluebutton.core.record.events + +class SetTimerTrackRecordEvent extends AbstractTimerRecordEvent { + import SetTimerTrackRecordEvent._ + + setEvent("SetTimerTrackEvent") + + def setTrack(value: String) { + eventMap.put(TRACK, value) + } +} + +object SetTimerTrackRecordEvent { + protected final val TRACK = "track" +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StartTimerRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StartTimerRecordEvent.scala new file mode 100644 index 000000000000..ee5f124eb96f --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StartTimerRecordEvent.scala @@ -0,0 +1,24 @@ +/** + * 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 . + * + */ + +package org.bigbluebutton.core.record.events + +class StartTimerRecordEvent extends AbstractTimerRecordEvent { + setEvent("StartTimerEvent") +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StopTimerRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StopTimerRecordEvent.scala new file mode 100644 index 000000000000..81de31ae5c7a --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StopTimerRecordEvent.scala @@ -0,0 +1,34 @@ +/** + * 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 . + * + */ + +package org.bigbluebutton.core.record.events + +class StopTimerRecordEvent extends AbstractTimerRecordEvent { + import StopTimerRecordEvent._ + + setEvent("StopTimerEvent") + + def setAccumulated(value: Int) { + eventMap.put(ACCUMULATED, value.toString) + } +} + +object StopTimerRecordEvent { + protected final val ACCUMULATED = "accumulated" +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/SwitchTimerRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/SwitchTimerRecordEvent.scala new file mode 100644 index 000000000000..0badc3ab447b --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/SwitchTimerRecordEvent.scala @@ -0,0 +1,34 @@ +/** + * 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 . + * + */ + +package org.bigbluebutton.core.record.events + +class SwitchTimerRecordEvent extends AbstractTimerRecordEvent { + import SwitchTimerRecordEvent._ + + setEvent("SwitchTimerEvent") + + def setStopwatch(value: Boolean) { + eventMap.put(STOPWATCH, value.toString) + } +} + +object SwitchTimerRecordEvent { + protected final val STOPWATCH = "stopwatch" +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/TimerEndedRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/TimerEndedRecordEvent.scala new file mode 100644 index 000000000000..74c064b80413 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/TimerEndedRecordEvent.scala @@ -0,0 +1,24 @@ +/** + * 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 . + * + */ + +package org.bigbluebutton.core.record.events + +class TimerEndedRecordEvent extends AbstractTimerRecordEvent { + setEvent("TimerEndedEvent") +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/HandlerHelpers.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/HandlerHelpers.scala index b620c9dc8cef..12e661d118ef 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/HandlerHelpers.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/HandlerHelpers.scala @@ -62,6 +62,9 @@ trait HandlerHelpers extends SystemConfiguration { authed = regUser.authed, guestStatus = regUser.guestStatus, emoji = "none", + reactionEmoji = "none", + raiseHand = false, + away = false, pin = false, mobile = false, presenter = false, diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/LiveMeeting.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/LiveMeeting.scala index 56adff640573..c6fd3f9730f1 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/LiveMeeting.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/LiveMeeting.scala @@ -10,6 +10,7 @@ class LiveMeeting( val status: MeetingStatus2x, val screenshareModel: ScreenshareModel, val audioCaptions: AudioCaptions, + val timerModel: TimerModel, val chatModel: ChatModel, val externalVideoModel: ExternalVideoModel, val layouts: Layouts, diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index 3a01f2d1454d..4232dd21b9d4 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -19,6 +19,7 @@ import org.bigbluebutton.core.apps.externalvideo.ExternalVideoApp2x import org.bigbluebutton.core.apps.pads.PadsApp2x import org.bigbluebutton.core.apps.screenshare.ScreenshareApp2x import org.bigbluebutton.core.apps.audiocaptions.AudioCaptionsApp2x +import org.bigbluebutton.core.apps.timer.TimerApp2x import org.bigbluebutton.core.apps.presentation.PresentationApp2x import org.bigbluebutton.core.apps.users.UsersApp2x import org.bigbluebutton.core.apps.webcam.WebcamApp2x @@ -136,6 +137,7 @@ class MeetingActor( val pollApp = new PollApp2x val webcamApp2x = new WebcamApp2x val wbApp = new WhiteboardApp2x + val timerApp2x = new TimerApp2x object ExpiryTrackerHelper extends MeetingExpiryTrackerHelper @@ -300,7 +302,8 @@ class MeetingActor( hideUserList = lockSettingsProp.hideUserList, lockOnJoin = lockSettingsProp.lockOnJoin, lockOnJoinConfigurable = lockSettingsProp.lockOnJoinConfigurable, - hideViewersCursor = lockSettingsProp.hideViewersCursor + hideViewersCursor = lockSettingsProp.hideViewersCursor, + hideViewersAnnotation = lockSettingsProp.hideViewersAnnotation ) MeetingStatus2x.initializePermissions(liveMeeting.status) @@ -384,11 +387,18 @@ class MeetingActor( case m: RecordAndClearPreviousMarkersCmdMsg => state = usersApp.handleRecordAndClearPreviousMarkersCmdMsg(m, state) updateUserLastActivity(m.body.setBy) - case m: GetRecordingStatusReqMsg => usersApp.handleGetRecordingStatusReqMsg(m) - case m: ChangeUserEmojiCmdMsg => handleChangeUserEmojiCmdMsg(m) - case m: SelectRandomViewerReqMsg => usersApp.handleSelectRandomViewerReqMsg(m) - case m: ChangeUserPinStateReqMsg => usersApp.handleChangeUserPinStateReqMsg(m) - case m: ChangeUserMobileFlagReqMsg => usersApp.handleChangeUserMobileFlagReqMsg(m) + case m: GetRecordingStatusReqMsg => usersApp.handleGetRecordingStatusReqMsg(m) + case m: ChangeUserEmojiCmdMsg => handleChangeUserEmojiCmdMsg(m) + case m: ChangeUserReactionEmojiReqMsg => usersApp.handleChangeUserReactionEmojiReqMsg(m) + case m: ChangeUserRaiseHandReqMsg => usersApp.handleChangeUserRaiseHandReqMsg(m) + case m: ChangeUserAwayReqMsg => usersApp.handleChangeUserAwayReqMsg(m) + case m: UserReactionTimeExpiredCmdMsg => handleUserReactionTimeExpiredCmdMsg(m) + case m: ClearAllUsersEmojiCmdMsg => handleClearAllUsersEmojiCmdMsg(m) + case m: ClearAllUsersReactionCmdMsg => handleClearAllUsersReactionCmdMsg(m) + case m: SelectRandomViewerReqMsg => usersApp.handleSelectRandomViewerReqMsg(m) + case m: ChangeUserPinStateReqMsg => usersApp.handleChangeUserPinStateReqMsg(m) + case m: ChangeUserMobileFlagReqMsg => usersApp.handleChangeUserMobileFlagReqMsg(m) + case m: SetUserSpeechLocaleReqMsg => usersApp.handleSetUserSpeechLocaleReqMsg(m) // Client requested to eject user case m: EjectUserFromMeetingCmdMsg => @@ -474,6 +484,10 @@ class MeetingActor( handleGetGlobalAudioPermissionReqMsg(m) case m: GetMicrophonePermissionReqMsg => handleGetMicrophonePermissionReqMsg(m) + case m: ChannelHoldChangedVoiceConfEvtMsg => + handleChannelHoldChangedVoiceConfEvtMsg(m) + case m: ListenOnlyModeToggledInSfuEvtMsg => + handleListenOnlyModeToggledInSfuEvtMsg(m) // Layout case m: GetCurrentLayoutReqMsg => handleGetCurrentLayoutReqMsg(m) @@ -505,8 +519,8 @@ class MeetingActor( // Presentation case m: PreuploadedPresentationsSysPubMsg => presentationApp2x.handle(m, liveMeeting, msgBus) case m: AssignPresenterReqMsg => state = handlePresenterChange(m, state) - case m: MakePresentationWithAnnotationDownloadReqMsg => presentationPodsApp.handle(m, state, liveMeeting, msgBus) - case m: NewPresAnnFileAvailableMsg => presentationPodsApp.handle(m, liveMeeting, msgBus) + case m: MakePresentationDownloadReqMsg => presentationPodsApp.handle(m, state, liveMeeting, msgBus) + case m: NewPresFileAvailableMsg => presentationPodsApp.handle(m, liveMeeting, msgBus) case m: PresAnnStatusMsg => presentationPodsApp.handle(m, liveMeeting, msgBus) case m: PadCapturePubMsg => presentationPodsApp.handle(m, liveMeeting, msgBus) @@ -596,6 +610,18 @@ class MeetingActor( case m: UpdateExternalVideoPubMsg => externalVideoApp2x.handle(m, liveMeeting, msgBus) case m: StopExternalVideoPubMsg => externalVideoApp2x.handle(m, liveMeeting, msgBus) + //Timer + case m: CreateTimerPubMsg => timerApp2x.handle(m, liveMeeting, msgBus) + case m: ActivateTimerReqMsg => timerApp2x.handle(m, liveMeeting, msgBus) + case m: DeactivateTimerReqMsg => timerApp2x.handle(m, liveMeeting, msgBus) + case m: StartTimerReqMsg => timerApp2x.handle(m, liveMeeting, msgBus) + case m: StopTimerReqMsg => timerApp2x.handle(m, liveMeeting, msgBus) + case m: SwitchTimerReqMsg => timerApp2x.handle(m, liveMeeting, msgBus) + case m: SetTimerReqMsg => timerApp2x.handle(m, liveMeeting, msgBus) + case m: ResetTimerReqMsg => timerApp2x.handle(m, liveMeeting, msgBus) + case m: SetTrackReqMsg => timerApp2x.handle(m, liveMeeting, msgBus) + case m: TimerEndedPubMsg => timerApp2x.handle(m, liveMeeting, msgBus) + case m: ValidateConnAuthTokenSysMsg => handleValidateConnAuthTokenSysMsg(m) case m: UserActivitySignCmdMsg => handleUserActivitySignCmdMsg(m) @@ -843,6 +869,7 @@ class MeetingActor( 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) + endAllBreakoutRooms(eventBus, liveMeeting, state, MeetingEndReason.ENDED_DUE_TO_NO_MODERATOR) sendEndMeetingDueToExpiry( MeetingEndReason.ENDED_DUE_TO_NO_MODERATOR, eventBus, outGW, liveMeeting, diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/RunningMeeting.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/RunningMeeting.scala index 412299ba3c98..2b13ee67b575 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/RunningMeeting.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/RunningMeeting.scala @@ -33,13 +33,14 @@ class RunningMeeting(val props: DefaultProps, outGW: OutMessageGateway, private val guestsWaiting = new GuestsWaiting private val deskshareModel = new ScreenshareModel private val audioCaptions = new AudioCaptions + private val timerModel = new TimerModel // meetingModel.setGuestPolicy(props.usersProp.guestPolicy) // We extract the meeting handlers into this class so it is // easy to test. - private val liveMeeting = new LiveMeeting(props, meetingStatux2x, deskshareModel, audioCaptions, chatModel, externalVideoModel, - layouts, pads, registeredUsers, polls2x, wbModel, presModel, captionModel, + private val liveMeeting = new LiveMeeting(props, meetingStatux2x, deskshareModel, audioCaptions, timerModel, + chatModel, externalVideoModel, layouts, pads, registeredUsers, polls2x, wbModel, presModel, captionModel, webcams, voiceUsers, users2x, guestsWaiting) GuestsWaiting.setGuestPolicy( diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala index 594919973885..d4f305dda549 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala @@ -102,6 +102,10 @@ class AnalyticsActor(val includeChat: Boolean) extends Actor with ActorLogging { logMessage(msg) case m: VoiceConfCallStateEvtMsg => logMessage(msg) case m: VoiceCallStateEvtMsg => logMessage(msg) + case m: HoldChannelInVoiceConfSysMsg => logMessage(msg) + case m: ChannelHoldChangedVoiceConfEvtMsg => logMessage(msg) + case m: ToggleListenOnlyModeSysMsg => logMessage(msg) + case m: ListenOnlyModeToggledInSfuEvtMsg => logMessage(msg) // Breakout case m: BreakoutRoomEndedEvtMsg => logMessage(msg) @@ -117,8 +121,8 @@ class AnalyticsActor(val includeChat: Boolean) extends Actor with ActorLogging { //case m: PresentationPageConvertedEventMsg => logMessage(msg) // case m: StoreAnnotationsInRedisSysMsg => logMessage(msg) // case m: StoreExportJobInRedisSysMsg => logMessage(msg) - case m: MakePresentationWithAnnotationDownloadReqMsg => logMessage(msg) - case m: NewPresAnnFileAvailableMsg => logMessage(msg) + case m: MakePresentationDownloadReqMsg => logMessage(msg) + case m: NewPresFileAvailableMsg => logMessage(msg) case m: PresentationPageConversionStartedSysMsg => logMessage(msg) case m: PresentationConversionEndedSysMsg => logMessage(msg) case m: PresentationConversionRequestReceivedSysMsg => logMessage(msg) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/FromAkkaAppsMsgSenderActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/FromAkkaAppsMsgSenderActor.scala index 31f495e4a9a4..7f7b005de2e0 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/FromAkkaAppsMsgSenderActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/FromAkkaAppsMsgSenderActor.scala @@ -67,6 +67,8 @@ class FromAkkaAppsMsgSenderActor(msgSender: MessageSender) msgSender.send(toVoiceConfRedisChannel, json) case GetUsersStatusToVoiceConfSysMsg.NAME => msgSender.send(toVoiceConfRedisChannel, json) + case HoldChannelInVoiceConfSysMsg.NAME => + msgSender.send(toVoiceConfRedisChannel, json) // Sent to SFU case EjectUserFromSfuSysMsg.NAME => @@ -75,6 +77,8 @@ class FromAkkaAppsMsgSenderActor(msgSender: MessageSender) msgSender.send(toSfuRedisChannel, json) case CamStreamUnsubscribeSysMsg.NAME => msgSender.send(toSfuRedisChannel, json) + case ToggleListenOnlyModeSysMsg.NAME => + msgSender.send(toSfuRedisChannel, json) //================================================================== // Send chat, presentation, and whiteboard in different channels so as not to diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/MeetingStatus2x.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/MeetingStatus2x.scala index 6525d1d24453..5762d9354308 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/MeetingStatus2x.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/MeetingStatus2x.scala @@ -13,7 +13,8 @@ case class Permissions( hideUserList: Boolean = false, lockOnJoin: Boolean = true, lockOnJoinConfigurable: Boolean = false, - hideViewersCursor: Boolean = false + hideViewersCursor: Boolean = false, + hideViewersAnnotation: Boolean = false ) case class MeetingExtensionProp(maxExtensions: Int = 2, numExtensions: Int = 0, extendByMinutes: Int = 20, diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/guests/GuestsWaitingApprovedMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/guests/GuestsWaitingApprovedMsgHdlr.scala index 491e47680021..80ed7e223694 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/guests/GuestsWaitingApprovedMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/guests/GuestsWaitingApprovedMsgHdlr.scala @@ -45,7 +45,9 @@ trait GuestsWaitingApprovedMsgHdlr extends HandlerHelpers with RightsManagementT dialInUser.color, MeetingStatus2x.isMeetingMuted(liveMeeting.status), false, - "freeswitch" + "freeswitch", + false, + "unused" ) VoiceUsers.findWithIntId( liveMeeting.voiceUsers, diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/senders/MsgBuilder.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/senders/MsgBuilder.scala index f71375406be1..de5e3e7389ad 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/senders/MsgBuilder.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/senders/MsgBuilder.scala @@ -590,4 +590,62 @@ object MsgBuilder { BbbCommonEnvCoreMsg(envelope, event) } + + def buildUserEmojiChangedEvtMsg(meetingId: String, userId: String, emoji: String) = { + val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId) + val envelope = BbbCoreEnvelope(UserEmojiChangedEvtMsg.NAME, routing) + val header = BbbClientMsgHeader(UserEmojiChangedEvtMsg.NAME, meetingId, userId) + val body = UserEmojiChangedEvtMsgBody(userId, emoji) + val event = UserEmojiChangedEvtMsg(header, body) + BbbCommonEnvCoreMsg(envelope, event) + } + + def buildUserAwayChangedEvtMsg(meetingId: String, userId: String, away: Boolean) = { + val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId) + val envelope = BbbCoreEnvelope(UserAwayChangedEvtMsg.NAME, routing) + val header = BbbClientMsgHeader(UserAwayChangedEvtMsg.NAME, meetingId, userId) + val body = UserAwayChangedEvtMsgBody(userId, away) + val event = UserAwayChangedEvtMsg(header, body) + BbbCommonEnvCoreMsg(envelope, event) + } + + def buildUserRaiseHandChangedEvtMsg(meetingId: String, userId: String, raiseHand: Boolean) = { + val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId) + val envelope = BbbCoreEnvelope(UserRaiseHandChangedEvtMsg.NAME, routing) + val header = BbbClientMsgHeader(UserRaiseHandChangedEvtMsg.NAME, meetingId, userId) + val body = UserRaiseHandChangedEvtMsgBody(userId, raiseHand) + val event = UserRaiseHandChangedEvtMsg(header, body) + BbbCommonEnvCoreMsg(envelope, event) + } + + def buildHoldChannelInVoiceConfSysMsg( + meetingId: String, + voiceConf: String, + uuid: String, + hold: Boolean + ): BbbCommonEnvCoreMsg = { + val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka") + val envelope = BbbCoreEnvelope(HoldChannelInVoiceConfSysMsg.NAME, routing) + val body = HoldChannelInVoiceConfSysMsgBody(voiceConf, uuid, hold) + val header = BbbCoreHeaderWithMeetingId(HoldChannelInVoiceConfSysMsg.NAME, meetingId) + val event = HoldChannelInVoiceConfSysMsg(header, body) + + BbbCommonEnvCoreMsg(envelope, event) + } + + def buildToggleListenOnlyModeSysMsg( + meetingId: String, + voiceConf: String, + userId: String, + enabled: Boolean + ): BbbCommonEnvCoreMsg = { + val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka") + val envelope = BbbCoreEnvelope(ToggleListenOnlyModeSysMsg.NAME, routing) + val body = ToggleListenOnlyModeSysMsgBody(voiceConf, userId, enabled) + val header = BbbCoreHeaderWithMeetingId(ToggleListenOnlyModeSysMsg.NAME, meetingId) + val event = ToggleListenOnlyModeSysMsg(header, body) + + BbbCommonEnvCoreMsg(envelope, event) + } + } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/senders/UserJoinedMeetingEvtMsgBuilder.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/senders/UserJoinedMeetingEvtMsgBuilder.scala index 4a6d3d7a9a99..4301e11fc316 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/senders/UserJoinedMeetingEvtMsgBuilder.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/senders/UserJoinedMeetingEvtMsgBuilder.scala @@ -12,6 +12,9 @@ object UserJoinedMeetingEvtMsgBuilder { role = userState.role, guest = userState.guest, authed = userState.authed, guestStatus = userState.guestStatus, emoji = userState.emoji, + reactionEmoji = userState.reactionEmoji, + raiseHand = userState.raiseHand, + away = userState.away, pin = userState.pin, presenter = userState.presenter, locked = userState.locked, avatar = userState.avatar, color = userState.color, clientType = userState.clientType) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/testdata/FakeTestData.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/testdata/FakeTestData.scala index 189be4419c4f..2e43892f8c9d 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/testdata/FakeTestData.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/testdata/FakeTestData.scala @@ -68,7 +68,8 @@ trait FakeTestData { def createFakeUser(liveMeeting: LiveMeeting, regUser: RegisteredUser): UserState = { UserState(intId = regUser.id, extId = regUser.externId, name = regUser.name, role = regUser.role, pin = false, mobile = false, guest = regUser.guest, authed = regUser.authed, guestStatus = regUser.guestStatus, - emoji = "none", locked = false, presenter = false, avatar = regUser.avatarURL, color = "#ff6242", clientType = "unknown", + emoji = "none", reactionEmoji = "none", raiseHand = false, away = false, locked = false, presenter = false, + avatar = regUser.avatarURL, color = "#ff6242", clientType = "unknown", pickExempted = false, userLeftFlag = UserLeftFlag(false, 0)) } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/testdata/FakeUserGenerator.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/testdata/FakeUserGenerator.scala index 85fb84d61fa7..3ac1a2d50f4a 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/testdata/FakeUserGenerator.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/testdata/FakeUserGenerator.scala @@ -65,7 +65,9 @@ object FakeUserGenerator { val voiceUserId = RandomStringGenerator.randomAlphanumericString(8) val lastFloorTime = System.currentTimeMillis().toString(); VoiceUserState(intId = user.id, voiceUserId = voiceUserId, callingWith, callerName = user.name, - callerNum = user.name, "#ff6242", muted, talking, listenOnly, "freeswitch", System.currentTimeMillis(), floor, lastFloorTime) + callerNum = user.name, "#ff6242", muted, talking, listenOnly, "freeswitch", System.currentTimeMillis(), floor, lastFloorTime, + false, + "9b3f4504-275d-4315-9922-21174262d88c") } def createFakeVoiceOnlyUser(callingWith: String, muted: Boolean, talking: Boolean, @@ -75,7 +77,9 @@ object FakeUserGenerator { val name = getRandomElement(firstNames, random) + " " + getRandomElement(lastNames, random) val lastFloorTime = System.currentTimeMillis().toString(); VoiceUserState(intId, voiceUserId = voiceUserId, callingWith, callerName = name, - callerNum = name, "#ff6242", muted, talking, listenOnly, "freeswitch", System.currentTimeMillis(), floor, lastFloorTime) + callerNum = name, "#ff6242", muted, talking, listenOnly, "freeswitch", System.currentTimeMillis(), floor, lastFloorTime, + false, + "9b3f4504-275d-4315-9922-21174262d88c") } def createFakeWebcamStreamFor(userId: String, subscribers: Set[String]): WebcamStream = { diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/LearningDashboardActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/LearningDashboardActor.scala index d0ad68473bcb..bdc12a78a4b1 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/LearningDashboardActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/LearningDashboardActor.scala @@ -39,6 +39,7 @@ case class User( answers: Map[String,Vector[String]] = Map(), talk: Talk = Talk(), emojis: Vector[Emoji] = Vector(), + reactions: Vector[Emoji] = Vector(), webcams: Vector[Webcam] = Vector(), totalOfMessages: Long = 0, ) @@ -140,6 +141,9 @@ class LearningDashboardActor( case m: UserLeaveReqMsg => handleUserLeaveReqMsg(m) case m: UserLeftMeetingEvtMsg => handleUserLeftMeetingEvtMsg(m) case m: UserEmojiChangedEvtMsg => handleUserEmojiChangedEvtMsg(m) + case m: UserAwayChangedEvtMsg => handleUserAwayChangedEvtMsg(m) + case m: UserRaiseHandChangedEvtMsg => handleUserRaiseHandChangedEvtMsg(m) + case m: UserReactionEmojiChangedEvtMsg => handleUserReactionEmojiChangedEvtMsg(m) case m: UserRoleChangedEvtMsg => handleUserRoleChangedEvtMsg(m) case m: UserBroadcastCamStartedEvtMsg => handleUserBroadcastCamStartedEvtMsg(m) case m: UserBroadcastCamStoppedEvtMsg => handleUserBroadcastCamStoppedEvtMsg(m) @@ -350,7 +354,7 @@ class LearningDashboardActor( meeting <- meetings.values.find(m => m.intId == msg.header.meetingId) user <- findUserByIntId(meeting, msg.body.userId) } yield { - if (msg.body.emoji != "none") { + if (msg.body.emoji != "none" && msg.body.emoji != "raiseHand" && msg.body.emoji != "away") { val updatedUser = user.copy(emojis = user.emojis :+ Emoji(msg.body.emoji)) val updatedMeeting = meeting.copy(users = meeting.users + (updatedUser.userKey -> updatedUser)) @@ -359,6 +363,48 @@ class LearningDashboardActor( } } + private def handleUserRaiseHandChangedEvtMsg(msg: UserRaiseHandChangedEvtMsg): Unit = { + for { + meeting <- meetings.values.find(m => m.intId == msg.header.meetingId) + user <- findUserByIntId(meeting, msg.body.userId) + } yield { + if (msg.body.raiseHand) { + val updatedUser = user.copy(emojis = user.emojis :+ Emoji("raiseHand")) + val updatedMeeting = meeting.copy(users = meeting.users + (updatedUser.userKey -> updatedUser)) + + meetings += (updatedMeeting.intId -> updatedMeeting) + } + } + } + + private def handleUserAwayChangedEvtMsg(msg: UserAwayChangedEvtMsg): Unit = { + for { + meeting <- meetings.values.find(m => m.intId == msg.header.meetingId) + user <- findUserByIntId(meeting, msg.body.userId) + } yield { + if (msg.body.away) { + val updatedUser = user.copy(emojis = user.emojis :+ Emoji("away")) + val updatedMeeting = meeting.copy(users = meeting.users + (updatedUser.userKey -> updatedUser)) + + meetings += (updatedMeeting.intId -> updatedMeeting) + } + } + } + + private def handleUserReactionEmojiChangedEvtMsg(msg: UserReactionEmojiChangedEvtMsg): Unit = { + for { + meeting <- meetings.values.find(m => m.intId == msg.header.meetingId) + user <- findUserByIntId(meeting, msg.body.userId) + } yield { + if (msg.body.reactionEmoji != "none") { + val updatedUser = user.copy(reactions = user.reactions :+ Emoji(msg.body.reactionEmoji)) + val updatedMeeting = meeting.copy(users = meeting.users + (updatedUser.userKey -> updatedUser)) + + meetings += (updatedMeeting.intId -> updatedMeeting) + } + } + } + private def handleUserRoleChangedEvtMsg(msg: UserRoleChangedEvtMsg) { for { meeting <- meetings.values.find(m => m.intId == msg.header.meetingId) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala index e634a89007b4..ae91ed9b5f26 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala @@ -135,6 +135,17 @@ class RedisRecorderActor( case m: UpdateExternalVideoEvtMsg => handleUpdateExternalVideoEvtMsg(m) case m: StopExternalVideoEvtMsg => handleStopExternalVideoEvtMsg(m) + // Timer + case m: ActivateTimerRespMsg => handleActivateTimerRespMsg(m) + case m: DeactivateTimerRespMsg => handleDeactivateTimerRespMsg(m) + case m: StartTimerRespMsg => handleStartTimerRespMsg(m) + case m: StopTimerRespMsg => handleStopTimerRespMsg(m) + case m: SwitchTimerRespMsg => handleSwitchTimerRespMsg(m) + case m: SetTimerRespMsg => handleSetTimerRespMsg(m) + case m: ResetTimerRespMsg => handleResetTimerRespMsg(m) + case m: TimerEndedEvtMsg => handleTimerEndedEvtMsg(m) + case m: SetTrackRespMsg => handleSetTrackRespMsg(m) + case _ => // message not to be recorded. } } @@ -545,6 +556,78 @@ class RedisRecorderActor( record(msg.header.meetingId, ev.toMap.asJava) } + private def handleActivateTimerRespMsg(msg: ActivateTimerRespMsg) { + val ev = new ActivateTimerRecordEvent() + ev.setMeetingId(msg.header.meetingId) + ev.setStopwatch(msg.body.stopwatch) + ev.setRunning(msg.body.running) + ev.setTime(msg.body.time) + ev.setAccumulated(msg.body.accumulated) + ev.setTrack(msg.body.track) + + record(msg.header.meetingId, ev.toMap.asJava) + } + + private def handleDeactivateTimerRespMsg(msg: DeactivateTimerRespMsg) { + val ev = new DeactivateTimerRecordEvent() + ev.setMeetingId(msg.header.meetingId) + + record(msg.header.meetingId, ev.toMap.asJava) + } + + private def handleStartTimerRespMsg(msg: StartTimerRespMsg) { + val ev = new StartTimerRecordEvent() + ev.setMeetingId(msg.header.meetingId) + + record(msg.header.meetingId, ev.toMap.asJava) + } + + private def handleStopTimerRespMsg(msg: StopTimerRespMsg) { + val ev = new StopTimerRecordEvent() + ev.setMeetingId(msg.header.meetingId) + ev.setAccumulated(msg.body.accumulated) + + record(msg.header.meetingId, ev.toMap.asJava) + } + + private def handleSwitchTimerRespMsg(msg: SwitchTimerRespMsg) { + val ev = new SwitchTimerRecordEvent() + ev.setMeetingId(msg.header.meetingId) + ev.setStopwatch(msg.body.stopwatch) + + record(msg.header.meetingId, ev.toMap.asJava) + } + + private def handleSetTimerRespMsg(msg: SetTimerRespMsg) { + val ev = new SetTimerRecordEvent() + ev.setMeetingId(msg.header.meetingId) + ev.setTime(msg.body.time) + + record(msg.header.meetingId, ev.toMap.asJava) + } + + private def handleResetTimerRespMsg(msg: ResetTimerRespMsg) { + val ev = new ResetTimerRecordEvent() + ev.setMeetingId(msg.header.meetingId) + + record(msg.header.meetingId, ev.toMap.asJava) + } + + private def handleTimerEndedEvtMsg(msg: TimerEndedEvtMsg) { + val ev = new TimerEndedRecordEvent() + ev.setMeetingId(msg.header.meetingId) + + record(msg.header.meetingId, ev.toMap.asJava) + } + + private def handleSetTrackRespMsg(msg: SetTrackRespMsg) { + val ev = new SetTimerTrackRecordEvent() + ev.setMeetingId(msg.header.meetingId) + ev.setTrack(msg.body.track) + + record(msg.header.meetingId, ev.toMap.asJava) + } + private def handleRecordingStatusChangedEvtMsg(msg: RecordingStatusChangedEvtMsg) { val ev = new RecordStatusRecordEvent() ev.setMeetingId(msg.header.meetingId) diff --git a/akka-bbb-apps/src/test/scala/org/bigbluebutton/core2/testdata/TestDataGen.scala b/akka-bbb-apps/src/test/scala/org/bigbluebutton/core2/testdata/TestDataGen.scala index 10cf8ed3cfc4..285408e333d7 100755 --- a/akka-bbb-apps/src/test/scala/org/bigbluebutton/core2/testdata/TestDataGen.scala +++ b/akka-bbb-apps/src/test/scala/org/bigbluebutton/core2/testdata/TestDataGen.scala @@ -24,7 +24,9 @@ object TestDataGen { listenOnly: Boolean): VoiceUserState = { val voiceUserId = RandomStringGenerator.randomAlphanumericString(8) VoiceUserState(intId = user.id, voiceUserId = voiceUserId, callingWith, callerName = user.name, - callerNum = user.name, "#ff6242", muted, talking, listenOnly) + callerNum = user.name, "#ff6242", muted, talking, listenOnly, + false, + "9b3f4504-275d-4315-9922-21174262d88c") } def createFakeVoiceOnlyUser(callingWith: String, muted: Boolean, talking: Boolean, @@ -32,7 +34,9 @@ object TestDataGen { val voiceUserId = RandomStringGenerator.randomAlphanumericString(8) val intId = "v_" + RandomStringGenerator.randomAlphanumericString(16) VoiceUserState(intId, voiceUserId = voiceUserId, callingWith, callerName = name, - callerNum = name, "#ff6242", muted, talking, listenOnly) + callerNum = name, "#ff6242", muted, talking, listenOnly + false, + "9b3f4504-275d-4315-9922-21174262d88c") } def createFakeWebcamStreamFor(userId: String, subscribers: Set[String]): WebcamStream = { @@ -43,8 +47,9 @@ object TestDataGen { def createUserFor(liveMeeting: LiveMeeting, regUser: RegisteredUser, presenter: Boolean): UserState = { val u = UserState(intId = regUser.id, extId = regUser.externId, name = regUser.name, role = regUser.role, guest = regUser.guest, authed = regUser.authed, guestStatus = regUser.guestStatus, - emoji = "none", locked = false, presenter = false, avatar = regUser.avatarURL, color = "#ff6242", - clientType = "unknown", userLeftFlag = UserLeftFlag(false, 0)) + emoji = "none", reactionEmoji = "none", raiseHand = false, away = false, pin = false, mobile = false, + locked = false, presenter = false, avatar = regUser.avatarURL, color = "#ff6242", + clientType = "unknown", pickExempted = false, userLeftFlag = UserLeftFlag(false, 0)) Users2x.add(liveMeeting.users2x, u) u } diff --git a/akka-bbb-apps/src/universal/conf/application.conf b/akka-bbb-apps/src/universal/conf/application.conf index 3f5ce4803f13..59e93bfdb1d9 100755 --- a/akka-bbb-apps/src/universal/conf/application.conf +++ b/akka-bbb-apps/src/universal/conf/application.conf @@ -92,6 +92,10 @@ voiceConf { # Path to the audio file being played when dial-in user is waiting for # approval. This can be relative to FreeSWITCH sounds folder dialInApprovalAudioPath = "ivr/ivr-please_hold_while_party_contacted.wav" + + # Time (seconds) to wait before requesting an audio channel hold after + # muting a user. Used in the experimental, transparent listen only mode. + toggleListenOnlyAfterMuteTimer = 4 } recording { @@ -102,4 +106,4 @@ recording { transcript { words = 8 # per line lines = 2 -} \ No newline at end of file +} diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/FreeswitchConferenceEventListener.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/FreeswitchConferenceEventListener.java index 7335cab7003c..076d82cc54a5 100755 --- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/FreeswitchConferenceEventListener.java +++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/FreeswitchConferenceEventListener.java @@ -57,8 +57,18 @@ public void run() { if (event instanceof VoiceUserJoinedEvent) { VoiceUserJoinedEvent evt = (VoiceUserJoinedEvent) event; vcs.userJoinedVoiceConf(evt.getRoom(), evt.getVoiceUserId(), evt.getUserId(), evt.getCallerIdName(), - evt.getCallerIdNum(), evt.getMuted(), evt.getSpeaking(), evt.getCallingWith()); - } else if (event instanceof VoiceConfRunningEvent) { + evt.getCallerIdNum(), evt.getMuted(), evt.getSpeaking(), evt.getCallingWith(), + evt.getHold(), + evt.getUUID()); + } else if (event instanceof ChannelHoldChangedEvent) { + ChannelHoldChangedEvent evt = (ChannelHoldChangedEvent) event; + vcs.channelHoldChanged( + evt.getRoom(), + evt.getUserId(), + evt.getUUID(), + evt.isHeld() + ); + } else if (event instanceof VoiceConfRunningEvent) { VoiceConfRunningEvent evt = (VoiceConfRunningEvent) event; vcs.voiceConfRunning(evt.getRoom(), evt.isRunning()); } else if (event instanceof VoiceUserLeftEvent) { diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/IVoiceConferenceService.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/IVoiceConferenceService.java index 1f2c362bf663..5ca5f7b4c6f6 100755 --- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/IVoiceConferenceService.java +++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/IVoiceConferenceService.java @@ -22,7 +22,9 @@ void userJoinedVoiceConf(String voiceConfId, String callerIdNum, Boolean muted, Boolean speaking, - String avatarURL); + String avatarURL, + Boolean hold, + String uuid); void voiceUsersStatus(String voiceConfId, java.util.List confMembers, @@ -67,4 +69,9 @@ void freeswitchStatusReplyEvent(Long sendCommandTimestamp, Long receivedResponseTimestamp); void freeswitchHeartbeatEvent(Map heartbeat); + + void channelHoldChanged(String voiceConfId, + String userId, + String uuid, + Boolean hold); } diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/ChannelHoldChangedEvent.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/ChannelHoldChangedEvent.java new file mode 100644 index 000000000000..10aa0a3bcf57 --- /dev/null +++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/ChannelHoldChangedEvent.java @@ -0,0 +1,51 @@ +/** + * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ + * + * Copyright (c) 2023 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 . + * + */ +package org.bigbluebutton.freeswitch.voice.events; + +public class ChannelHoldChangedEvent extends VoiceConferenceEvent { + + private final String userId; + private final String uuid; + private final boolean hold; + + public ChannelHoldChangedEvent( + String room, + String userId, + String uuid, + boolean hold + ) { + super(room); + this.userId = userId; + this.uuid = uuid; + this.hold = hold; + } + + public String getUserId() { + return userId; + } + + public String getUUID() { + return uuid; + } + + public boolean isHeld() { + return hold; + } + +} diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/ConfMember.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/ConfMember.java index 2470355c8bbf..27b240e27050 100755 --- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/ConfMember.java +++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/ConfMember.java @@ -9,6 +9,8 @@ public class ConfMember { public final Boolean locked = false; public final String userId; public final String callingWith; + public final Boolean hold; + public final String uuid; public ConfMember(String userId, String voiceUserId, @@ -16,7 +18,9 @@ public ConfMember(String userId, String callerIdName, Boolean muted, Boolean speaking, - String callingWith) { + String callingWith, + Boolean hold, + String uuid) { this.userId = userId; this.voiceUserId = voiceUserId; this.callerIdName = callerIdName; @@ -24,5 +28,7 @@ public ConfMember(String userId, this.muted = muted; this.speaking = speaking; this.callingWith = callingWith; + this.hold = hold; + this.uuid = uuid; } } diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/VoiceUserJoinedEvent.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/VoiceUserJoinedEvent.java index cb069481761c..7aa644baa2d9 100755 --- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/VoiceUserJoinedEvent.java +++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/VoiceUserJoinedEvent.java @@ -28,10 +28,14 @@ public class VoiceUserJoinedEvent extends VoiceConferenceEvent { private final Boolean locked = false; private final String userId; private final String callingWith; + private final Boolean hold; + private final String uuid; public VoiceUserJoinedEvent(String userId, String voiceUserId, String room, String callerIdNum, String callerIdName, - Boolean muted, Boolean speaking, String callingWith) { + Boolean muted, Boolean speaking, String callingWith, + Boolean hold, + String uuid) { super(room); this.userId = userId; this.voiceUserId = voiceUserId; @@ -40,6 +44,8 @@ public VoiceUserJoinedEvent(String userId, String voiceUserId, String room, this.muted = muted; this.speaking = speaking; this.callingWith = callingWith; + this.hold = hold; + this.uuid = uuid; } public String getUserId() { @@ -73,4 +79,12 @@ public Boolean isLocked() { public String getCallingWith() { return callingWith; } + + public String getUUID() { + return uuid; + } + + public Boolean getHold() { + return hold; + } } diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/ConnectionManager.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/ConnectionManager.java index f66a2e3a69b2..0725e7dd7feb 100755 --- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/ConnectionManager.java +++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/ConnectionManager.java @@ -94,6 +94,7 @@ private void connect() { //c.addEventFilter(EVENT_NAME, "background_job"); c.addEventFilter(EVENT_NAME, "CHANNEL_EXECUTE"); c.addEventFilter(EVENT_NAME, "CHANNEL_STATE"); + c.addEventFilter(EVENT_NAME, "CHANNEL_CALLSTATE"); subscribed = true; } else { // Let's check for status every minute. @@ -239,6 +240,13 @@ public void tranfer(TransferUserToMeetingCommand tutmc) { } } + public void holdChannel(HoldChannelCommand hcc) { + Client c = manager.getESLClient(); + if (c.canSend()) { + c.sendAsyncApiCommand(hcc.getCommand(), hcc.getCommandArgs()); + } + } + public void eject(EjectUserCommand mpc) { Client c = manager.getESLClient(); if (c.canSend()) { diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/ESLEventListener.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/ESLEventListener.java index 23c47386c1d6..a67e6e5768e5 100755 --- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/ESLEventListener.java +++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/ESLEventListener.java @@ -26,6 +26,9 @@ public class ESLEventListener implements IEslEventListener { private static final String CONFERENCE_CREATED_EVENT = "conference-create"; private static final String CONFERENCE_DESTROYED_EVENT = "conference-destroy"; private static final String FLOOR_CHANGE_EVENT = "video-floor-change"; + private static final String CHANNEL_CALLSTATE_EVENT = "CHANNEL_CALLSTATE"; + private static final String CHANNEL_CALLSTATE_HELD = "HELD"; + private static final String CHANNEL_CALLSTATE_ACTIVE = "ACTIVE"; private final ConferenceEventListener conferenceEventListener; @@ -59,12 +62,14 @@ public void exceptionCaught(ExceptionEvent e) { @Override public void conferenceEventJoin(String uniqueId, String confName, int confSize, EslEvent event) { - Integer memberId = this.getMemberIdFromEvent(event); + Integer memberId = this.getMemberId(event); Map headers = event.getEventHeaders(); - String callerId = this.getCallerIdFromEvent(event); + String callerId = this.getCallerId(event); String callerIdName = this.getCallerIdNameFromEvent(event); + String channelCallState = this.getChannelCallState(headers); boolean muted = headers.get("Speak").equals("true") ? false : true; //Was inverted which was causing a State issue boolean speaking = headers.get("Talking").equals("true") ? true : false; + boolean hold = channelCallState.equals(CHANNEL_CALLSTATE_HELD); String voiceUserId = callerIdName; @@ -124,14 +129,16 @@ public void conferenceEventJoin(String uniqueId, String confName, int confSize, callerIdName, muted, speaking, - "none"); + "none", + hold, + callerUUID); conferenceEventListener.handleConferenceEvent(pj); } @Override public void conferenceEventLeave(String uniqueId, String confName, int confSize, EslEvent event) { - Integer memberId = this.getMemberIdFromEvent(event); - String callerId = this.getCallerIdFromEvent(event); + Integer memberId = this.getMemberId(event); + String callerId = this.getCallerId(event); String callerIdName = this.getCallerIdNameFromEvent(event); String callerUUID = this.getMemberUUIDFromEvent(event); @@ -146,14 +153,14 @@ public void conferenceEventLeave(String uniqueId, String confName, int confSize, @Override public void conferenceEventMute(String uniqueId, String confName, int confSize, EslEvent event) { - Integer memberId = this.getMemberIdFromEvent(event); + Integer memberId = this.getMemberId(event); VoiceUserMutedEvent pm = new VoiceUserMutedEvent(memberId.toString(), confName, true); conferenceEventListener.handleConferenceEvent(pm); } @Override public void conferenceEventUnMute(String uniqueId, String confName, int confSize, EslEvent event) { - Integer memberId = this.getMemberIdFromEvent(event); + Integer memberId = this.getMemberId(event); VoiceUserMutedEvent pm = new VoiceUserMutedEvent(memberId.toString(), confName, false); conferenceEventListener.handleConferenceEvent(pm); } @@ -165,11 +172,11 @@ public void conferenceEventAction(String uniqueId, String confName, int confSize } if (action.equals(START_TALKING_EVENT)) { - Integer memberId = this.getMemberIdFromEvent(event); + Integer memberId = this.getMemberId(event); VoiceUserTalkingEvent pt = new VoiceUserTalkingEvent(memberId.toString(), confName, true); conferenceEventListener.handleConferenceEvent(pt); } else if (action.equals(STOP_TALKING_EVENT)) { - Integer memberId = this.getMemberIdFromEvent(event); + Integer memberId = this.getMemberId(event); VoiceUserTalkingEvent pt = new VoiceUserTalkingEvent(memberId.toString(), confName, false); conferenceEventListener.handleConferenceEvent(pt); } else if (action.equals(CONFERENCE_CREATED_EVENT)) { @@ -437,16 +444,92 @@ public void eventReceived(EslEvent event) { ); conferenceEventListener.handleConferenceEvent(csEvent); } + } else if (event.getEventName().equals(CHANNEL_CALLSTATE_EVENT)) { + Map eventHeaders = event.getEventHeaders(); + String channelCallState = this.getChannelCallState(eventHeaders); + String originalChannelCallState = eventHeaders.get("Original-Channel-Call-State"); + if (channelCallState == null + || originalChannelCallState == null + || channelCallState.equals(originalChannelCallState) + || !(channelCallState.equals(CHANNEL_CALLSTATE_HELD) || channelCallState.equals(CHANNEL_CALLSTATE_ACTIVE))) { + // No call state info, or no change in call state, or not a call state we care about + return; + } + + String intId = this.getIntId(event); + + if (intId == null) { + return; + } + + Boolean hold = channelCallState.equals(CHANNEL_CALLSTATE_HELD); + String uuid = this.getMemberUUIDFromEvent(event); + String conference = eventHeaders.get("Caller-Destination-Number"); + Matcher callerDestNumberMatcher = ECHO_TEST_DEST_PATTERN.matcher(conference); + + if (callerDestNumberMatcher.matches()) { + conference = callerDestNumberMatcher.group(1).trim(); + } + ChannelHoldChangedEvent csEvent = new ChannelHoldChangedEvent( + conference, + intId, + uuid, + hold + ); + conferenceEventListener.handleConferenceEvent(csEvent); } + + } + + private String getIntId(EslEvent event) { + return this.getIntId(event.getEventHeaders()); + } + + private String getIntId(Map eventHeaders) { + String origCallerIdName = this.getCallerId(eventHeaders); + Integer memberId = this.getMemberId(eventHeaders); + Matcher callerListenOnly = CALLERNAME_LISTENONLY_PATTERN.matcher(origCallerIdName); + Matcher callWithSess = CALLERNAME_WITH_SESS_INFO_PATTERN.matcher(origCallerIdName); + if (callWithSess.matches()) { + return callWithSess.group(1).trim(); + } else if (callerListenOnly.matches()) { + return callerListenOnly.group(1).trim(); + } else if (memberId != null) { + return "v_" + memberId.toString(); + } else { + return null; + } + } + + private Integer getMemberId(EslEvent event) { + return this.getMemberId(event.getEventHeaders()); + } + + private Integer getMemberId(Map eventHeaders) { + String memberId = eventHeaders.get("Member-ID"); + + if (memberId == null) { + return null; + } + + return Integer.valueOf(memberId); + } + + private String getCallerId(EslEvent event) { + return this.getCallerId(event.getEventHeaders()); + } + + private String getCallerId(Map eventHeaders) { + return eventHeaders.get("Caller-Caller-ID-Number"); } - private Integer getMemberIdFromEvent(EslEvent e) { - return new Integer(e.getEventHeaders().get("Member-ID")); + private String getChannelCallState(EslEvent event) { + return this.getChannelCallState(event.getEventHeaders()); } - private String getCallerIdFromEvent(EslEvent e) { - return e.getEventHeaders().get("Caller-Caller-ID-Number"); + private String getChannelCallState(Map eventHeaders) { + return eventHeaders.get("Channel-Call-State"); } private String getMemberUUIDFromEvent(EslEvent e) { diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/FreeswitchApplication.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/FreeswitchApplication.java index 9acdcc56726e..2c0627bd583a 100755 --- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/FreeswitchApplication.java +++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/FreeswitchApplication.java @@ -34,6 +34,7 @@ import org.bigbluebutton.freeswitch.voice.freeswitch.actions.StopSoundCommand; import org.bigbluebutton.freeswitch.voice.freeswitch.actions.RecordConferenceCommand; import org.bigbluebutton.freeswitch.voice.freeswitch.actions.TransferUserToMeetingCommand; +import org.bigbluebutton.freeswitch.voice.freeswitch.actions.HoldChannelCommand; import org.bigbluebutton.freeswitch.voice.freeswitch.actions.*; import org.slf4j.Logger; @@ -157,6 +158,11 @@ public void ejectAll(String voiceConfId) { queueMessage(mpc); } + public void holdChannel(String voiceConfId, String uuid, Boolean hold) { + HoldChannelCommand hcc = new HoldChannelCommand(voiceConfId, uuid, hold, USER); + queueMessage(hcc); + } + private Long genTimestamp() { return TimeUnit.NANOSECONDS.toMillis(System.nanoTime()); } @@ -220,6 +226,10 @@ public void run() { manager.forceEjectUser((ForceEjectUserCommand) command); } else if (command instanceof GetUsersStatusCommand) { manager.getUsersStatus((GetUsersStatusCommand) command); + } else if (command instanceof HoldChannelCommand) { + manager.holdChannel((HoldChannelCommand) command); + } else { + log.warn("Unknown command: " + command.getCommand()); } } catch (RuntimeException e) { log.warn(e.getMessage()); diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/actions/GetAllUsersCommand.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/actions/GetAllUsersCommand.java index 5bf6fae2b4ff..5ecca8445526 100755 --- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/actions/GetAllUsersCommand.java +++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/actions/GetAllUsersCommand.java @@ -108,7 +108,9 @@ public void handleResponse(EslMessage response, ConferenceEventListener eventLis } VoiceUserJoinedEvent pj = new VoiceUserJoinedEvent(voiceUserId, member.getId().toString(), confXML.getConferenceRoom(), - callerId, callerIdName, member.getMuted(), member.getSpeaking(), "none"); + callerId, callerIdName, member.getMuted(), member.getSpeaking(), "none", + member.getHold(), + uuid); eventListener.handleConferenceEvent(pj); } else if ("recording_node".equals(member.getMemberType())) { diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/actions/GetUsersStatusCommand.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/actions/GetUsersStatusCommand.java index 1d5c6ac8ea22..be83ada4ff0b 100755 --- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/actions/GetUsersStatusCommand.java +++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/actions/GetUsersStatusCommand.java @@ -106,7 +106,9 @@ public void handleResponse(EslMessage response, ConferenceEventListener eventLis callerId, callerIdName, member.getMuted(), member.getSpeaking(), - "none"); + "none", + member.getHold(), + member.getUUID()); confMembers.add(confMember); } } else if ("recording_node".equals(member.getMemberType())) { diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/actions/HoldChannelCommand.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/actions/HoldChannelCommand.java new file mode 100644 index 000000000000..a2a23e8d42a6 --- /dev/null +++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/actions/HoldChannelCommand.java @@ -0,0 +1,44 @@ +/** + * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ + * + * Copyright (c) 2023 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 . + * + */ +package org.bigbluebutton.freeswitch.voice.freeswitch.actions; + +public class HoldChannelCommand extends FreeswitchCommand { + private final String uuid; + private final Boolean hold; + + public HoldChannelCommand(String room, String uuid, Boolean hold, String requesterId) { + super(room, requesterId); + this.uuid = uuid; + this.hold = hold; + } + + @Override + public String getCommand() { + return "uuid_hold"; + } + + @Override + public String getCommandArgs() { + if (hold) { + return "toggle" + SPACE + uuid; + } else { + return "off" + SPACE + uuid; + } + } +} diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/response/ConferenceMember.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/response/ConferenceMember.java index 2133b73542b9..1dc577f8e4e2 100755 --- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/response/ConferenceMember.java +++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/response/ConferenceMember.java @@ -62,6 +62,10 @@ public boolean getSpeaking() { return flags.getIsSpeaking(); } + public boolean getHold() { + return flags.getHold(); + } + public void setFlags(ConferenceMemberFlags flags) { this.flags = flags; } diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/response/ConferenceMemberFlags.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/response/ConferenceMemberFlags.java index f571f5da4fcf..5f06a227e2f8 100755 --- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/response/ConferenceMemberFlags.java +++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/response/ConferenceMemberFlags.java @@ -27,6 +27,7 @@ public class ConferenceMemberFlags { //private boolean canHear = false; private boolean canSpeak = false; private boolean talking = false; + private boolean hold = false; //private boolean hasVideo = false; //private boolean hasFloor = false; //private boolean isModerator = false; @@ -51,4 +52,11 @@ void setTalking(String tempVal) { talking = tempVal.equals("true") ? true : false; } + void setHold(String tempVal) { + hold = tempVal.equals("true") ? true : false; + } + + boolean getHold() { + return hold; + } } diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/response/XMLResponseConferenceListParser.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/response/XMLResponseConferenceListParser.java index 29e9ec175686..b73fa9c741df 100755 --- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/response/XMLResponseConferenceListParser.java +++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/response/XMLResponseConferenceListParser.java @@ -131,6 +131,8 @@ public void endElement(String uri, String localName, String qName) throws SAXExc tempFlags.setCanSpeak(tempVal); }else if (qName.equalsIgnoreCase("talking")) { tempFlags.setTalking(tempVal); + } else if (qName.equalsIgnoreCase("hold")) { + tempFlags.setHold(tempVal); } }else if (qName.equalsIgnoreCase("id")) { try { diff --git a/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/RxJsonMsgDeserializer.scala b/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/RxJsonMsgDeserializer.scala index c6557a5317b1..01353e90a29d 100755 --- a/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/RxJsonMsgDeserializer.scala +++ b/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/RxJsonMsgDeserializer.scala @@ -243,4 +243,21 @@ trait RxJsonMsgDeserializer { } } + def routeHoldChannelInVoiceConfMsg(envelope: BbbCoreEnvelope, jsonNode: JsonNode): Unit = { + def deserialize(jsonNode: JsonNode): Option[HoldChannelInVoiceConfSysMsg] = { + val (result, error) = JsonDeserializer.toBbbCommonMsg[HoldChannelInVoiceConfSysMsg](jsonNode) + result match { + case Some(msg) => Some(msg.asInstanceOf[HoldChannelInVoiceConfSysMsg]) + case None => + log.error("Failed to deserialize message: error: {} \n msg: {}", error, jsonNode) + None + } + } + + for { + m <- deserialize(jsonNode) + } yield { + fsApp.holdChannel(m.body.voiceConf, m.body.uuid, m.body.hold) + } + } } diff --git a/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/RxJsonMsgHdlrActor.scala b/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/RxJsonMsgHdlrActor.scala index 51a812eca070..e5857be8f07d 100755 --- a/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/RxJsonMsgHdlrActor.scala +++ b/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/RxJsonMsgHdlrActor.scala @@ -60,6 +60,8 @@ class RxJsonMsgHdlrActor(val fsApp: FreeswitchApplication) extends Actor with Ac routeCheckRunningAndRecordingToVoiceConfSysMsg(envelope, jsonNode) case GetUsersStatusToVoiceConfSysMsg.NAME => routeGetUsersStatusToVoiceConfSysMsg(envelope, jsonNode) + case HoldChannelInVoiceConfSysMsg.NAME => + routeHoldChannelInVoiceConfMsg(envelope, jsonNode) case _ => // do nothing } } diff --git a/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/VoiceConferenceService.scala b/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/VoiceConferenceService.scala index 4ed6f823a3bc..4e520a82dcc1 100755 --- a/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/VoiceConferenceService.scala +++ b/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/VoiceConferenceService.scala @@ -90,7 +90,9 @@ class VoiceConferenceService(healthz: HealthzService, cm.muted, cm.speaking, cm.callingWith, - "freeswitch" + "freeswitch", + cm.hold, + cm.uuid ) } @@ -119,12 +121,16 @@ class VoiceConferenceService(healthz: HealthzService, callerIdNum: String, muted: java.lang.Boolean, talking: java.lang.Boolean, - callingWith: String - ) { + callingWith: String, + hold: java.lang.Boolean, + uuid: String + ): Unit = { val header = BbbCoreVoiceConfHeader(UserJoinedVoiceConfEvtMsg.NAME, voiceConfId) val body = UserJoinedVoiceConfEvtMsgBody(voiceConfId, voiceUserId, userId, callerIdName, callerIdNum, - muted.booleanValue(), talking.booleanValue(), callingWith) + muted.booleanValue(), talking.booleanValue(), callingWith, + hold, + uuid); val envelope = BbbCoreEnvelope(UserJoinedVoiceConfEvtMsg.NAME, Map("voiceConf" -> voiceConfId)) val msg = new UserJoinedVoiceConfEvtMsg(header, body) @@ -248,6 +254,28 @@ class VoiceConferenceService(healthz: HealthzService, sender.publish(fromVoiceConfRedisChannel, json) } + def channelHoldChanged( + voiceConfId: String, + voiceUserId: String, + uuid: String, + hold: java.lang.Boolean + ): Unit = { + val header = BbbCoreVoiceConfHeader(ChannelHoldChangedVoiceConfEvtMsg.NAME, voiceConfId) + val body = ChannelHoldChangedVoiceConfEvtMsgBody( + voiceConfId, + voiceUserId, + uuid, + hold + ); + val envelope = BbbCoreEnvelope(ChannelHoldChangedVoiceConfEvtMsg.NAME, Map("voiceConf" -> voiceConfId)) + + val msg = new ChannelHoldChangedVoiceConfEvtMsg(header, body) + val msgEvent = BbbCommonEnvCoreMsg(envelope, msg) + + val json = JsonUtil.toJson(msgEvent) + sender.publish(fromVoiceConfRedisChannel, json) + } + def freeswitchStatusReplyEvent( sendCommandTimestamp: java.lang.Long, status: java.util.List[String], diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala index d3fd4db93006..d224b8f3a2e4 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala @@ -63,7 +63,8 @@ case class LockSettingsProps( hideUserList: Boolean, lockOnJoin: Boolean, lockOnJoinConfigurable: Boolean, - hideViewersCursor: Boolean + hideViewersCursor: Boolean, + hideViewersAnnotation: Boolean ) case class SystemProps( diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Presentation.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Presentation.scala index 752e1cd9a9c3..2eefc67aef34 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Presentation.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Presentation.scala @@ -2,7 +2,7 @@ package org.bigbluebutton.common2.domain case class PresentationVO(id: String, temporaryPresentationId: String, name: String, current: Boolean = false, pages: Vector[PageVO], downloadable: Boolean, removable: Boolean, - isInitialPresentation: Boolean) + isInitialPresentation: Boolean, filenameConverted: String) case class PageVO(id: String, num: Int, thumbUri: String = "", txtUri: String, svgUri: String, current: Boolean = false, xOffset: Double = 0, diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/AudioCaptionsMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/AudioCaptionsMsgs.scala index 838c5f611a6e..495f325f45e3 100644 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/AudioCaptionsMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/AudioCaptionsMsgs.scala @@ -9,10 +9,11 @@ case class UpdateTranscriptPubMsgBody( end: Int, text: String, transcript: String, - locale: String + locale: String, + result: Boolean, ) // Out messages object TranscriptUpdatedEvtMsg { val NAME = "TranscriptUpdatedEvtMsg" } case class TranscriptUpdatedEvtMsg(header: BbbClientMsgHeader, body: TranscriptUpdatedEvtMsgBody) extends BbbCoreMsg -case class TranscriptUpdatedEvtMsgBody(transcriptId: String, transcript: String, locale: String) +case class TranscriptUpdatedEvtMsgBody(transcriptId: String, transcript: String, locale: String, result: Boolean) diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/BreakoutMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/BreakoutMsgs.scala index f5ef4bc2dd75..7ac03144e30d 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/BreakoutMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/BreakoutMsgs.scala @@ -12,7 +12,7 @@ case class BreakoutRoomJoinURLEvtMsgBody(parentId: String, breakoutId: String, e // Outgoing messages 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 BreakoutRoomsListEvtMsgBody(meetingId: String, rooms: Vector[BreakoutRoomInfo], roomsReady: Boolean, sendInviteToModerators: Boolean) case class BreakoutRoomInfo(name: String, externalId: String, breakoutId: String, sequence: Int, shortName: String, isDefaultName: Boolean, freeJoin: Boolean, html5JoinUrls: Map[String, String], captureNotes: Boolean, captureSlides: Boolean) object BreakoutRoomsListMsg { val NAME = "BreakoutRoomsListMsg" } @@ -70,7 +70,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, captureNotes: Boolean, captureSlides: Boolean, rooms: Vector[BreakoutRoomMsgBody]) +case class CreateBreakoutRoomsCmdMsgBody(meetingId: String, durationInMinutes: Int, record: Boolean, captureNotes: Boolean, captureSlides: Boolean, rooms: Vector[BreakoutRoomMsgBody], sendInviteToModerators: Boolean) case class BreakoutRoomMsgBody(name: String, sequence: Int, shortName: String, captureNotesFilename: String, captureSlidesFilename: String, isDefaultName: Boolean, freeJoin: Boolean, users: Vector[String]) // Sent by user to request ending all the breakout rooms diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationMsgs.scala index 6899291b7f2e..f5467b258a30 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationMsgs.scala @@ -10,13 +10,14 @@ object PreuploadedPresentationsSysPubMsg { val NAME = "PreuploadedPresentationsS case class PreuploadedPresentationsSysPubMsg(header: BbbClientMsgHeader, body: PreuploadedPresentationsSysPubMsgBody) extends StandardMsg case class PreuploadedPresentationsSysPubMsgBody(presentations: Vector[PresentationVO]) -object MakePresentationWithAnnotationDownloadReqMsg { val NAME = "MakePresentationWithAnnotationDownloadReqMsg" } -case class MakePresentationWithAnnotationDownloadReqMsg(header: BbbClientMsgHeader, body: MakePresentationWithAnnotationDownloadReqMsgBody) extends StandardMsg -case class MakePresentationWithAnnotationDownloadReqMsgBody(presId: String, allPages: Boolean, pages: List[Int]) +object MakePresentationDownloadReqMsg { val NAME = "MakePresentationDownloadReqMsg" } +case class MakePresentationDownloadReqMsg(header: BbbClientMsgHeader, body: MakePresentationDownloadReqMsgBody) extends StandardMsg +case class MakePresentationDownloadReqMsgBody(presId: String, allPages: Boolean, pages: List[Int], fileStateType: String) -object NewPresAnnFileAvailableMsg { val NAME = "NewPresAnnFileAvailableMsg" } -case class NewPresAnnFileAvailableMsg(header: BbbClientMsgHeader, body: NewPresAnnFileAvailableMsgBody) extends StandardMsg -case class NewPresAnnFileAvailableMsgBody(fileURI: String, presId: String) +object NewPresFileAvailableMsg { val NAME = "NewPresFileAvailableMsg" } +case class NewPresFileAvailableMsg(header: BbbClientMsgHeader, body: NewPresFileAvailableMsgBody) extends StandardMsg +case class NewPresFileAvailableMsgBody(annotatedFileURI: String, originalFileURI: String, convertedFileURI: String, + presId: String, fileStateType: String) object PresAnnStatusMsg { val NAME = "PresAnnStatusMsg" } case class PresAnnStatusMsg(header: BbbClientMsgHeader, body: PresAnnStatusMsgBody) extends StandardMsg @@ -37,9 +38,10 @@ object NewPresentationEvtMsg { val NAME = "NewPresentationEvtMsg" } case class NewPresentationEvtMsg(header: BbbClientMsgHeader, body: NewPresentationEvtMsgBody) extends BbbCoreMsg case class NewPresentationEvtMsgBody(presentation: PresentationVO) -object NewPresAnnFileAvailableEvtMsg { val NAME = "NewPresAnnFileAvailableEvtMsg" } -case class NewPresAnnFileAvailableEvtMsg(header: BbbClientMsgHeader, body: NewPresAnnFileAvailableEvtMsgBody) extends BbbCoreMsg -case class NewPresAnnFileAvailableEvtMsgBody(fileURI: String, presId: String) +object NewPresFileAvailableEvtMsg { val NAME = "NewPresFileAvailableEvtMsg" } +case class NewPresFileAvailableEvtMsg(header: BbbClientMsgHeader, body: NewPresFileAvailableEvtMsgBody) extends BbbCoreMsg +case class NewPresFileAvailableEvtMsgBody(annotatedFileURI: String, originalFileURI: String, convertedFileURI: String, + presId: String, fileStateType: String) object PresAnnStatusEvtMsg { val NAME = "PresAnnStatusEvtMsg" } case class PresAnnStatusEvtMsg(header: BbbClientMsgHeader, body: PresAnnStatusEvtMsgBody) extends BbbCoreMsg diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationPodsMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationPodsMsgs.scala index ed248d5bd752..e37d3568160d 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationPodsMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationPodsMsgs.scala @@ -33,7 +33,8 @@ case class RemovePresentationPubMsgBody(podId: String, presentationId: String) object SetPresentationDownloadablePubMsg { val NAME = "SetPresentationDownloadablePubMsg" } case class SetPresentationDownloadablePubMsg(header: BbbClientMsgHeader, body: SetPresentationDownloadablePubMsgBody) extends StandardMsg -case class SetPresentationDownloadablePubMsgBody(podId: String, presentationId: String, downloadable: Boolean) +case class SetPresentationDownloadablePubMsgBody(podId: String, presentationId: String, downloadable: Boolean, + fileStateType: String) object ResizeAndMovePagePubMsg { val NAME = "ResizeAndMovePagePubMsg" } case class ResizeAndMovePagePubMsg(header: BbbClientMsgHeader, body: ResizeAndMovePagePubMsgBody) extends StandardMsg @@ -331,7 +332,8 @@ case class RemovePresentationEvtMsgBody(podId: String, presentationId: String) object SetPresentationDownloadableEvtMsg { val NAME = "SetPresentationDownloadableEvtMsg" } case class SetPresentationDownloadableEvtMsg(header: BbbClientMsgHeader, body: SetPresentationDownloadableEvtMsgBody) extends BbbCoreMsg -case class SetPresentationDownloadableEvtMsgBody(podId: String, presentationId: String, downloadable: Boolean, presFilename: String) +case class SetPresentationDownloadableEvtMsgBody(podId: String, presentationId: String, downloadable: Boolean, + presFilename: String, downloadableExtension: String) object ResizeAndMovePageEvtMsg { val NAME = "ResizeAndMovePageEvtMsg" } case class ResizeAndMovePageEvtMsg(header: BbbClientMsgHeader, body: ResizeAndMovePageEvtMsgBody) extends BbbCoreMsg diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/TimerMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/TimerMsgs.scala new file mode 100644 index 000000000000..8e31df841253 --- /dev/null +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/TimerMsgs.scala @@ -0,0 +1,79 @@ +package org.bigbluebutton.common2.msgs + +/* In Messages */ +object CreateTimerPubMsg { val NAME = "CreateTimerPubMsg" } +case class CreateTimerPubMsg(header: BbbClientMsgHeader, body: CreateTimerPubMsgBody) extends StandardMsg +case class CreateTimerPubMsgBody(stopwatch: Boolean, running: Boolean, time: Int, accumulated: Int, timestamp: Int, track: String) + +object ActivateTimerReqMsg { val NAME = "ActivateTimerReqMsg" } +case class ActivateTimerReqMsg(header: BbbClientMsgHeader, body: ActivateTimerReqMsgBody) extends StandardMsg +case class ActivateTimerReqMsgBody(stopwatch: Boolean, running: Boolean, time: Int, accumulated: Int, timestamp: Int, track: String) + +object DeactivateTimerReqMsg { val NAME = "DeactivateTimerReqMsg" } +case class DeactivateTimerReqMsg(header: BbbClientMsgHeader, body: DeactivateTimerReqMsgBody) extends StandardMsg +case class DeactivateTimerReqMsgBody() + +object StartTimerReqMsg { val NAME = "StartTimerReqMsg" } +case class StartTimerReqMsg(header: BbbClientMsgHeader, body: StartTimerReqMsgBody) extends StandardMsg +case class StartTimerReqMsgBody() + +object StopTimerReqMsg { val NAME = "StopTimerReqMsg" } +case class StopTimerReqMsg(header: BbbClientMsgHeader, body: StopTimerReqMsgBody) extends StandardMsg +case class StopTimerReqMsgBody(accumulated: Int) + +object SwitchTimerReqMsg { val NAME = "SwitchTimerReqMsg" } +case class SwitchTimerReqMsg(header: BbbClientMsgHeader, body: SwitchTimerReqMsgBody) extends StandardMsg +case class SwitchTimerReqMsgBody(stopwatch: Boolean) + +object SetTimerReqMsg { val NAME = "SetTimerReqMsg" } +case class SetTimerReqMsg(header: BbbClientMsgHeader, body: SetTimerReqMsgBody) extends StandardMsg +case class SetTimerReqMsgBody(time: Int) + +object ResetTimerReqMsg { val NAME = "ResetTimerReqMsg" } +case class ResetTimerReqMsg(header: BbbClientMsgHeader, body: ResetTimerReqMsgBody) extends StandardMsg +case class ResetTimerReqMsgBody() + +object TimerEndedPubMsg { val NAME = "TimerEndedPubMsg" } +case class TimerEndedPubMsg(header: BbbClientMsgHeader, body: TimerEndedPubMsgBody) extends StandardMsg +case class TimerEndedPubMsgBody() + +object SetTrackReqMsg { val NAME = "SetTrackReqMsg" } +case class SetTrackReqMsg(header: BbbClientMsgHeader, body: SetTrackReqMsgBody) extends StandardMsg +case class SetTrackReqMsgBody(track: String) + +/* Out Messages */ +object ActivateTimerRespMsg { val NAME = "ActivateTimerRespMsg" } +case class ActivateTimerRespMsg(header: BbbCoreHeaderWithMeetingId, body: ActivateTimerRespMsgBody) extends BbbCoreMsg +case class ActivateTimerRespMsgBody(userId: String, stopwatch: Boolean, running: Boolean, time: Int, accumulated: Int, track: String) + +object DeactivateTimerRespMsg { val NAME = "DeactivateTimerRespMsg" } +case class DeactivateTimerRespMsg(header: BbbCoreHeaderWithMeetingId, body: DeactivateTimerRespMsgBody) extends BbbCoreMsg +case class DeactivateTimerRespMsgBody(userId: String) + +object StartTimerRespMsg { val NAME = "StartTimerRespMsg" } +case class StartTimerRespMsg(header: BbbCoreHeaderWithMeetingId, body: StartTimerRespMsgBody) extends BbbCoreMsg +case class StartTimerRespMsgBody(userId: String) + +object StopTimerRespMsg { val NAME = "StopTimerRespMsg" } +case class StopTimerRespMsg(header: BbbCoreHeaderWithMeetingId, body: StopTimerRespMsgBody) extends BbbCoreMsg +case class StopTimerRespMsgBody(userId: String, accumulated: Int) + +object SwitchTimerRespMsg { val NAME = "SwitchTimerRespMsg" } +case class SwitchTimerRespMsg(header: BbbCoreHeaderWithMeetingId, body: SwitchTimerRespMsgBody) extends BbbCoreMsg +case class SwitchTimerRespMsgBody(userId: String, stopwatch: Boolean) + +object SetTimerRespMsg { val NAME = "SetTimerRespMsg" } +case class SetTimerRespMsg(header: BbbCoreHeaderWithMeetingId, body: SetTimerRespMsgBody) extends BbbCoreMsg +case class SetTimerRespMsgBody(userId: String, time: Int) + +object ResetTimerRespMsg { val NAME = "ResetTimerRespMsg" } +case class ResetTimerRespMsg(header: BbbCoreHeaderWithMeetingId, body: ResetTimerRespMsgBody) extends BbbCoreMsg +case class ResetTimerRespMsgBody(userId: String) + +object TimerEndedEvtMsg { val NAME = "TimerEndedEvtMsg" } +case class TimerEndedEvtMsg(header: BbbCoreHeaderWithMeetingId, body: TimerEndedEvtMsgBody) extends BbbCoreMsg +case class TimerEndedEvtMsgBody() + +object SetTrackRespMsg { val NAME = "SetTrackRespMsg" } +case class SetTrackRespMsg(header: BbbCoreHeaderWithMeetingId, body: SetTrackRespMsgBody) extends BbbCoreMsg +case class SetTrackRespMsgBody(userId: String, track: String) diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UsersMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UsersMsgs.scala index 3dd1bbb87c3d..5ba72853635b 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UsersMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UsersMsgs.scala @@ -89,20 +89,23 @@ case class UserJoinedMeetingEvtMsg( body: UserJoinedMeetingEvtMsgBody ) extends BbbCoreMsg case class UserJoinedMeetingEvtMsgBody( - intId: String, - extId: String, - name: String, - role: String, - guest: Boolean, - authed: Boolean, - guestStatus: String, - emoji: String, - pin: Boolean, - presenter: Boolean, - locked: Boolean, - avatar: String, - color: String, - clientType: String + intId: String, + extId: String, + name: String, + role: String, + guest: Boolean, + authed: Boolean, + guestStatus: String, + emoji: String, + reactionEmoji: String, + raiseHand: Boolean, + away: Boolean, + pin: Boolean, + presenter: Boolean, + locked: Boolean, + avatar: String, + color: String, + clientType: String ) /** @@ -205,6 +208,83 @@ object UserEmojiChangedEvtMsg { val NAME = "UserEmojiChangedEvtMsg" } case class UserEmojiChangedEvtMsg(header: BbbClientMsgHeader, body: UserEmojiChangedEvtMsgBody) extends BbbCoreMsg case class UserEmojiChangedEvtMsgBody(userId: String, emoji: String) +/** + * Sent from client about a user changing RaiseHand. + */ +object ChangeUserRaiseHandReqMsg { val NAME = "ChangeUserRaiseHandReqMsg" } +case class ChangeUserRaiseHandReqMsg(header: BbbClientMsgHeader, body: ChangeUserRaiseHandReqMsgBody) extends StandardMsg +case class ChangeUserRaiseHandReqMsgBody(userId: String, raiseHand: Boolean) + +/** + * Sent to all clients about a user changing RaiseHand. + */ +object UserRaiseHandChangedEvtMsg { val NAME = "UserRaiseHandChangedEvtMsg" } +case class UserRaiseHandChangedEvtMsg(header: BbbClientMsgHeader, body: UserRaiseHandChangedEvtMsgBody) extends BbbCoreMsg +case class UserRaiseHandChangedEvtMsgBody(userId: String, raiseHand: Boolean) + +/** + * Sent from client about a user changing Away. + */ +object ChangeUserAwayReqMsg { val NAME = "ChangeUserAwayReqMsg" } +case class ChangeUserAwayReqMsg(header: BbbClientMsgHeader, body: ChangeUserAwayReqMsgBody) extends StandardMsg +case class ChangeUserAwayReqMsgBody(userId: String, away: Boolean) + +/** + * Sent to all clients about a user changing Away. + */ +object UserAwayChangedEvtMsg { val NAME = "UserAwayChangedEvtMsg" } +case class UserAwayChangedEvtMsg(header: BbbClientMsgHeader, body: UserAwayChangedEvtMsgBody) extends BbbCoreMsg +case class UserAwayChangedEvtMsgBody(userId: String, away: Boolean) + +/** + * Sent from client about a user changing ReactionEmoji. + */ +object ChangeUserReactionEmojiReqMsg { val NAME = "ChangeUserReactionEmojiReqMsg" } +case class ChangeUserReactionEmojiReqMsg(header: BbbClientMsgHeader, body: ChangeUserReactionEmojiReqMsgBody) extends StandardMsg +case class ChangeUserReactionEmojiReqMsgBody(userId: String, reactionEmoji: String) + +/** + * Sent to all clients about a user changing ReactionEmoji. + */ +object UserReactionEmojiChangedEvtMsg { val NAME = "UserReactionEmojiChangedEvtMsg" } +case class UserReactionEmojiChangedEvtMsg(header: BbbClientMsgHeader, body: UserReactionEmojiChangedEvtMsgBody) extends BbbCoreMsg +case class UserReactionEmojiChangedEvtMsgBody(userId: String, reactionEmoji: String) + +/** + * Sent from meteor about a user reaction's expiration. + */ +object UserReactionTimeExpiredCmdMsg { val NAME = "UserReactionTimeExpiredCmdMsg" } +case class UserReactionTimeExpiredCmdMsg(header: BbbClientMsgHeader, body: UserReactionTimeExpiredCmdMsgBody) extends StandardMsg +case class UserReactionTimeExpiredCmdMsgBody(userId: String) + +/** + * Sent from client about a mod clearing all users' emoji. + */ +object ClearAllUsersEmojiCmdMsg { val NAME = "ClearAllUsersEmojiCmdMsg" } +case class ClearAllUsersEmojiCmdMsg(header: BbbClientMsgHeader, body: ClearAllUsersEmojiCmdMsgBody) extends StandardMsg +case class ClearAllUsersEmojiCmdMsgBody(userId: String) + +/** + * Sent to all clients about clearing all users' emoji. + */ +object ClearedAllUsersEmojiEvtMsg { val NAME = "ClearedAllUsersEmojiEvtMsg" } +case class ClearedAllUsersEmojiEvtMsg(header: BbbClientMsgHeader, body: ClearedAllUsersEmojiEvtMsgBody) extends StandardMsg +case class ClearedAllUsersEmojiEvtMsgBody() + +/** + * Sent from client about a mod clearing all users' Reaction. + */ +object ClearAllUsersReactionCmdMsg { val NAME = "ClearAllUsersReactionCmdMsg" } +case class ClearAllUsersReactionCmdMsg(header: BbbClientMsgHeader, body: ClearAllUsersReactionCmdMsgBody) extends StandardMsg +case class ClearAllUsersReactionCmdMsgBody(userId: String) + +/** + * Sent to all clients about clearing all users' Reaction. + */ +object ClearedAllUsersReactionEvtMsg { val NAME = "ClearedAllUsersReactionEvtMsg" } +case class ClearedAllUsersReactionEvtMsg(header: BbbClientMsgHeader, body: ClearedAllUsersReactionEvtMsgBody) extends StandardMsg +case class ClearedAllUsersReactionEvtMsgBody() + /** * Sent from client about a user mobile flag. */ @@ -282,7 +362,7 @@ case class ChangeLockSettingsInMeetingCmdMsg( ) extends StandardMsg case class ChangeLockSettingsInMeetingCmdMsgBody(disableCam: Boolean, disableMic: Boolean, disablePrivChat: Boolean, disablePubChat: Boolean, disableNotes: Boolean, hideUserList: Boolean, lockOnJoin: Boolean, - lockOnJoinConfigurable: Boolean, hideViewersCursor: Boolean, setBy: String) + lockOnJoinConfigurable: Boolean, hideViewersCursor: Boolean, hideViewersAnnotation: Boolean, setBy: String) object LockSettingsInMeetingChangedEvtMsg { val NAME = "LockSettingsInMeetingChangedEvtMsg" } case class LockSettingsInMeetingChangedEvtMsg( @@ -291,7 +371,7 @@ case class LockSettingsInMeetingChangedEvtMsg( ) extends BbbCoreMsg case class LockSettingsInMeetingChangedEvtMsgBody(disableCam: Boolean, disableMic: Boolean, disablePrivChat: Boolean, disablePubChat: Boolean, disableNotes: Boolean, hideUserList: Boolean, lockOnJoin: Boolean, - lockOnJoinConfigurable: Boolean, hideViewersCursor: Boolean, setBy: String) + lockOnJoinConfigurable: Boolean, hideViewersCursor: Boolean, hideViewersAnnotation: Boolean, setBy: String) /** * Sent by client to query the lock settings. @@ -307,7 +387,7 @@ object GetLockSettingsRespMsg { val NAME = "GetLockSettingsRespMsg" } case class GetLockSettingsRespMsg(header: BbbClientMsgHeader, body: GetLockSettingsRespMsgBody) extends BbbCoreMsg case class GetLockSettingsRespMsgBody(disableCam: Boolean, disableMic: Boolean, disablePrivChat: Boolean, disablePubChat: Boolean, disableNotes: Boolean, hideUserList: Boolean, lockOnJoin: Boolean, - lockOnJoinConfigurable: Boolean, hideViewersCursor: Boolean) + lockOnJoinConfigurable: Boolean, hideViewersCursor: Boolean, hideViewersAnnotation: Boolean) object LockSettingsNotInitializedRespMsg { val NAME = "LockSettingsNotInitializedRespMsg" } case class LockSettingsNotInitializedRespMsg(header: BbbClientMsgHeader, body: LockSettingsNotInitializedRespMsgBody) extends BbbCoreMsg @@ -443,3 +523,11 @@ case class SelectRandomViewerReqMsgBody(requestedBy: String) object SelectRandomViewerRespMsg { val NAME = "SelectRandomViewerRespMsg" } case class SelectRandomViewerRespMsg(header: BbbClientMsgHeader, body: SelectRandomViewerRespMsgBody) extends StandardMsg case class SelectRandomViewerRespMsgBody(requestedBy: String, userIds: Vector[String], choice: String) + +object SetUserSpeechLocaleReqMsg { val NAME = "SetUserSpeechLocaleReqMsg" } +case class SetUserSpeechLocaleReqMsg(header: BbbClientMsgHeader, body: SetUserSpeechLocaleReqMsgBody) extends StandardMsg +case class SetUserSpeechLocaleReqMsgBody(locale: String, provider: String) + +object UserSpeechLocaleChangedEvtMsg { val NAME = "UserSpeechLocaleChangedEvtMsg" } +case class UserSpeechLocaleChangedEvtMsg(header: BbbClientMsgHeader, body: UserSpeechLocaleChangedEvtMsgBody) extends BbbCoreMsg +case class UserSpeechLocaleChangedEvtMsgBody(locale: String, provider: String) diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala index 7e54df08ec8f..86d878e6c124 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala @@ -17,7 +17,7 @@ case class ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg( extends VoiceStandardMsg case class ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgBody(voiceConf: String, screenshareConf: String, stream: String, vidWidth: Int, vidHeight: Int, - timestamp: String, hasAudio: Boolean) + timestamp: String, hasAudio: Boolean, contentType: String) /** * Sent to clients to notify them of an RTMP stream starting. @@ -30,7 +30,7 @@ case class ScreenshareRtmpBroadcastStartedEvtMsg( extends BbbCoreMsg case class ScreenshareRtmpBroadcastStartedEvtMsgBody(voiceConf: String, screenshareConf: String, stream: String, vidWidth: Int, vidHeight: Int, - timestamp: String, hasAudio: Boolean) + timestamp: String, hasAudio: Boolean, contentType: String) /** * Sync screenshare state with bbb-html5 @@ -48,7 +48,8 @@ case class SyncGetScreenshareInfoRespMsgBody( vidWidth: Int, vidHeight: Int, timestamp: String, - hasAudio: Boolean + hasAudio: Boolean, + contentType: String ) /** @@ -386,8 +387,9 @@ case class UserStatusVoiceConfEvtMsgBody(voiceConf: String, confUsers: Vector[Co case class ConfVoiceUser(voiceUserId: String, intId: String, callerIdName: String, callerIdNum: String, muted: Boolean, talking: Boolean, callingWith: String, - calledInto: String // freeswitch, kms - ) + calledInto: String, // freeswitch, kms + hold: Boolean, + uuid: String) case class ConfVoiceRecording(recordPath: String, recordStartTime: Long) /** @@ -400,7 +402,9 @@ case class UserJoinedVoiceConfEvtMsg( ) extends VoiceStandardMsg case class UserJoinedVoiceConfEvtMsgBody(voiceConf: String, voiceUserId: String, intId: String, callerIdName: String, callerIdNum: String, muted: Boolean, - talking: Boolean, callingWith: String) + talking: Boolean, callingWith: String, + hold: Boolean, + uuid: String) /** * Sent to client that a user has joined the voice conference. @@ -638,3 +642,64 @@ case class GetMicrophonePermissionRespMsgBody( sfuSessionId: String, allowed: Boolean ) + +/** + * Sent to FS to hold an audio channel + */ +object HoldChannelInVoiceConfSysMsg { val NAME = "HoldChannelInVoiceConfSysMsg" } +case class HoldChannelInVoiceConfSysMsg( + header: BbbCoreHeaderWithMeetingId, + body: HoldChannelInVoiceConfSysMsgBody +) extends BbbCoreMsg +case class HoldChannelInVoiceConfSysMsgBody( + voiceConf: String, + uuid: String, + hold: Boolean +) + +/** + * Received from FS that the user channel hold state has changed + */ +object ChannelHoldChangedVoiceConfEvtMsg { val NAME = "ChannelHoldChangedVoiceConfEvtMsg" } +case class ChannelHoldChangedVoiceConfEvtMsg( + header: BbbCoreVoiceConfHeader, + body: ChannelHoldChangedVoiceConfEvtMsgBody +) extends VoiceStandardMsg +case class ChannelHoldChangedVoiceConfEvtMsgBody( + voiceConf: String, + intId: String, + uuid: String, + hold: Boolean +) + +/** + * Sent to bbb-webrtc-sfu to request for userId's microphone connection + * to be toggled between bidirectional and unidirectional (listen only) modes + * (enabled = unidirectional, listen only, !enabled = bidirectional); + */ +object ToggleListenOnlyModeSysMsg { val NAME = "ToggleListenOnlyModeSysMsg" } +case class ToggleListenOnlyModeSysMsg( + header: BbbCoreHeaderWithMeetingId, + body: ToggleListenOnlyModeSysMsgBody +) extends BbbCoreMsg +case class ToggleListenOnlyModeSysMsgBody( + voiceConf: String, + userId: String, + enabled: Boolean +) + +/** + * Sent from bbb-webrtc-sfu to indicate that userId's microphone channel switched + * modes (enabled = unidirectional, listen only, !enabled = bidirectional); + */ +object ListenOnlyModeToggledInSfuEvtMsg { val NAME = "ListenOnlyModeToggledInSfuEvtMsg" } +case class ListenOnlyModeToggledInSfuEvtMsg( + header: BbbCoreVoiceConfHeader, + body: ListenOnlyModeToggledInSfuEvtMsgBody +) extends VoiceStandardMsg +case class ListenOnlyModeToggledInSfuEvtMsgBody( + meetingId: String, + voiceConf: String, + userId: String, + enabled: Boolean +) diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java index af0e43f4eeb5..63e709764e70 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java @@ -95,6 +95,7 @@ public class ApiParams { public static final String LOCK_SETTINGS_LOCK_ON_JOIN = "lockSettingsLockOnJoin"; public static final String LOCK_SETTINGS_LOCK_ON_JOIN_CONFIGURABLE = "lockSettingsLockOnJoinConfigurable"; public static final String LOCK_SETTINGS_HIDE_VIEWERS_CURSOR = "lockSettingsHideViewersCursor"; + public static final String LOCK_SETTINGS_HIDE_VIEWERS_ANNOTATION = "lockSettingsHideViewersAnnotation"; // New param passed on create call to callback when meeting ends. // This is a duplicate of the endCallbackUrl meta param as we want this diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/ClientConfigService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/ClientConfigService.java deleted file mode 100755 index 247d7a1d8813..000000000000 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/ClientConfigService.java +++ /dev/null @@ -1,53 +0,0 @@ -/** -* 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 . -* -*/ - -package org.bigbluebutton.api; - -import java.util.HashMap; -import java.util.Map; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class ClientConfigService { - private static Logger log = LoggerFactory.getLogger(ClientConfigService.class); - - private String configDir = "/var/bigbluebutton/configs"; - private IClientConfigServiceHelper helper; - - private Map configs = new HashMap<>(); - - public void init() { - configs = helper.getPreBuiltConfigs(configDir); - log.info("ClientConfigService initialised"); - } - - public String getConfig(String id) { - return configs.get(id); - } - - public void setConfigDir(String dir) { - configDir = dir; - } - - public void setClientConfigServiceHelper(IClientConfigServiceHelper r) { - helper = r; - } - -} diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/IClientConfigServiceHelper.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/IClientConfigServiceHelper.java deleted file mode 100755 index c72e4c8e3650..000000000000 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/IClientConfigServiceHelper.java +++ /dev/null @@ -1,26 +0,0 @@ -/** -* 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 . -* -*/ - -package org.bigbluebutton.api; - -import java.util.Map; - -public interface IClientConfigServiceHelper { - public Map getPreBuiltConfigs(String dir); -} diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/LearningDashboardService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/LearningDashboardService.java index 65a6c5c4ef47..ca3f3a8e164a 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/LearningDashboardService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/LearningDashboardService.java @@ -48,7 +48,7 @@ public void writeJsonDataFile(String meetingId, String learningDashboardAccessTo File jsonFile = this.getJsonDataFile(meetingId,learningDashboardAccessToken); FileOutputStream fileOutput = new FileOutputStream(jsonFile); - fileOutput.write(activityJson.getBytes()); + fileOutput.write(activityJson.getBytes("UTF-8")); fileOutput.close(); diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java index 10668b4329f0..587828197bf2 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java @@ -126,6 +126,7 @@ public class ParamsProcessorUtil { private boolean defaultLockSettingsLockOnJoin; private boolean defaultLockSettingsLockOnJoinConfigurable; private boolean defaultLockSettingsHideViewersCursor; + private boolean defaultLockSettingsHideViewersAnnotation; private Long maxPresentationFileUpload = 30000000L; // 30MB @@ -374,6 +375,12 @@ private LockSettingsParams processLockSettingsParams(Map params) lockSettingsHideViewersCursor = Boolean.parseBoolean(lockSettingsHideViewersCursorParam); } + Boolean lockSettingsHideViewersAnnotation = defaultLockSettingsHideViewersAnnotation; + String lockSettingsHideViewersAnnotationParam = params.get(ApiParams.LOCK_SETTINGS_HIDE_VIEWERS_ANNOTATION); + if (!StringUtils.isEmpty(lockSettingsHideViewersAnnotationParam)) { + lockSettingsHideViewersAnnotation = Boolean.parseBoolean(lockSettingsHideViewersAnnotationParam); + } + return new LockSettingsParams(lockSettingsDisableCam, lockSettingsDisableMic, lockSettingsDisablePrivateChat, @@ -382,7 +389,8 @@ private LockSettingsParams processLockSettingsParams(Map params) lockSettingsHideUserList, lockSettingsLockOnJoin, lockSettingsLockOnJoinConfigurable, - lockSettingsHideViewersCursor); + lockSettingsHideViewersCursor, + lockSettingsHideViewersAnnotation); } private ArrayList processGroupsParams(Map params) { @@ -1148,6 +1156,11 @@ public boolean isPostChecksumSame(String apiCall, Map params) return true; } + public boolean parentMeetingExists(String parentMeetingId) { + Meeting meeting = ServiceUtils.findMeetingFromMeetingID(parentMeetingId); + return meeting != null; + } + /************************************************* * Setters ************************************************/ @@ -1463,6 +1476,10 @@ public void setLockSettingsHideViewersCursor(Boolean lockSettingsHideViewersCurs this.defaultLockSettingsHideViewersCursor = lockSettingsHideViewersCursor; } + public void setLockSettingsHideViewersAnnotation(Boolean lockSettingsHideViewersAnnotation) { + this.defaultLockSettingsHideViewersAnnotation = lockSettingsHideViewersAnnotation; + } + public void setAllowDuplicateExtUserid(Boolean allow) { this.defaultAllowDuplicateExtUserid = allow; } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/Util.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/Util.java index c76d0b8d1bd5..52ea9a9e9647 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/Util.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/Util.java @@ -2,11 +2,14 @@ import java.io.File; import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; public final class Util { @@ -50,8 +53,19 @@ public static String generatePresentationId(String presFilename) { return DigestUtils.sha1Hex(presFilename + uuid) + "-" + timestamp; } + public static String extractFilenameFromUrl(String preUploadedPresentation) throws MalformedURLException { + URL url = new URL(preUploadedPresentation); + String filename = FilenameUtils.getName(url.getPath()); + String extension = FilenameUtils.getExtension(url.getPath()); + if (extension == null || extension.isEmpty()) return null; + return filename; + } public static String createNewFilename(String presId, String fileExt) { - return presId + "." + fileExt; + if (!fileExt.isEmpty()) { + return presId + "." + fileExt; + } else { + return presId; + } } public static File createPresentationDir(String meetingId, String presentationDir, String presentationId) { @@ -105,22 +119,36 @@ public String stripPresBaseDirFromPath(String presentationBaseDir, String path) return path; } - public static File getPresFileDownloadMarker(File presBaseDir, String presId) { + public static File getPresFileDownloadMarker(File presBaseDir, String presId, String downloadableExtension) { if (presBaseDir != null) { - String downloadMarker = presId.concat(".downloadable"); + String downloadMarker = presId.concat(".").concat(downloadableExtension).concat(".downloadable"); return new File(presBaseDir.getAbsolutePath() + File.separatorChar + downloadMarker); } return null; } + public static void deleteAllDownloadableMarksInPresentations(File presFileDir) { + // Delete files with .downloadable at the end of its filename + File[] presFiles = presFileDir.listFiles(); + for (File presFile : presFiles) { + if (presFile.isFile() && presFile.getName().endsWith(".downloadable")) { + presFile.delete(); + } + } + } + public static void makePresentationDownloadable( File presFileDir, String presId, - boolean downloadable + boolean downloadable, + String downloadableExtension ) throws IOException { - File downloadMarker = Util.getPresFileDownloadMarker(presFileDir, presId); + File downloadMarker = Util.getPresFileDownloadMarker(presFileDir, presId, downloadableExtension); if (downloadable && downloadMarker != null && ! downloadMarker.exists()) { + Util.deleteAllDownloadableMarksInPresentations(presFileDir); downloadMarker.createNewFile(); + } else if (!downloadable && downloadMarker != null && downloadMarker.exists()) { + downloadMarker.delete(); } } } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/LockSettingsParams.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/LockSettingsParams.java index efae9b6a4d39..c702201a0287 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/LockSettingsParams.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/LockSettingsParams.java @@ -10,6 +10,7 @@ public class LockSettingsParams { public final Boolean lockOnJoin; public final Boolean lockOnJoinConfigurable; public final Boolean hideViewersCursor; + public final Boolean hideViewersAnnotation; public LockSettingsParams(Boolean disableCam, Boolean disableMic, @@ -19,7 +20,8 @@ public LockSettingsParams(Boolean disableCam, Boolean hideUserList, Boolean lockOnJoin, Boolean lockOnJoinConfigurable, - Boolean hideViewersCursor) { + Boolean hideViewersCursor, + Boolean hideViewersAnnotation) { this.disableCam = disableCam; this.disableMic = disableMic; this.disablePrivateChat = disablePrivateChat; @@ -29,5 +31,6 @@ public LockSettingsParams(Boolean disableCam, this.lockOnJoin = lockOnJoin; this.lockOnJoinConfigurable = lockOnJoinConfigurable; this.hideViewersCursor = hideViewersCursor; + this.hideViewersAnnotation = hideViewersAnnotation; } } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/messaging/messages/MakePresentationDownloadableMsg.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/messaging/messages/MakePresentationDownloadableMsg.java index 2faa23411cd3..fed44d8d7948 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/messaging/messages/MakePresentationDownloadableMsg.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/messaging/messages/MakePresentationDownloadableMsg.java @@ -5,11 +5,14 @@ public class MakePresentationDownloadableMsg implements IMessage { public final String presId; public final String presFilename; public final Boolean downloadable; + public final String downloadableExtension; - public MakePresentationDownloadableMsg(String meetingId, String presId, String presFilename, Boolean downloadable) { + public MakePresentationDownloadableMsg(String meetingId, String presId, String presFilename, + Boolean downloadable, String downloadableExtension) { this.meetingId = meetingId; this.presId = presId; this.presFilename = presFilename; this.downloadable = downloadable; + this.downloadableExtension = downloadableExtension; } } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/RecordingServiceFileImpl.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/RecordingServiceFileImpl.java index 2038bf00f709..2e6e71265679 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/RecordingServiceFileImpl.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/RecordingServiceFileImpl.java @@ -78,7 +78,7 @@ private void copyPresentationFile(File presFile, File dlownloadableFile) { public void processMakePresentationDownloadableMsg(MakePresentationDownloadableMsg msg) { try { File presDir = Util.getPresentationDir(presentationBaseDir, msg.meetingId, msg.presId); - Util.makePresentationDownloadable(presDir, msg.presId, msg.downloadable); + Util.makePresentationDownloadable(presDir, msg.presId, msg.downloadable, msg.downloadableExtension); } catch (IOException e) { log.error("Failed to make presentation downloadable: {}", e); } @@ -96,7 +96,7 @@ public File getDownloadablePresentationFile(String meetingId, String presId, Str String presFilenameExt = FilenameUtils.getExtension(presFilename); File presDir = Util.getPresentationDir(presentationBaseDir, meetingId, presId); - File downloadMarker = Util.getPresFileDownloadMarker(presDir, presId); + File downloadMarker = Util.getPresFileDownloadMarker(presDir, presId, presFilenameExt); if (presDir != null && downloadMarker != null && downloadMarker.exists()) { String safePresFilename = presId.concat(".").concat(presFilenameExt); File presFile = new File(presDir.getAbsolutePath() + File.separatorChar + safePresFilename); diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/MimeTypeUtils.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/MimeTypeUtils.java index fa29c20541ab..5e7372476cf9 100644 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/MimeTypeUtils.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/MimeTypeUtils.java @@ -1,5 +1,7 @@ package org.bigbluebutton.presentation; +import org.bigbluebutton.api.domain.Extension; + import java.util.*; import static org.bigbluebutton.presentation.FileTypeConstants.*; @@ -43,7 +45,16 @@ public class MimeTypeUtils { put(FileTypeConstants.SVG, Arrays.asList(SVG)); } }; - + + public String getExtensionBasedOnMimeType(String mimeType) { + return EXTENSIONS_MIME.entrySet() + .stream() + .filter(entry -> entry.getValue().contains(mimeType)) + .map(Map.Entry::getKey) + .findFirst() + .orElse(null); + } + public Boolean extensionMatchMimeType(String mimeType, String finalExtension) { finalExtension = finalExtension.toLowerCase(); diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/PresentationUrlDownloadService.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/PresentationUrlDownloadService.java index b2419636614b..6fe5e1014fba 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/PresentationUrlDownloadService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/PresentationUrlDownloadService.java @@ -300,6 +300,7 @@ public boolean savePresentation(final String meetingId, CloseableHttpAsyncClient httpclient = HttpAsyncClients.custom() .setDefaultRequestConfig(requestConfig) .build(); + try { httpclient.start(); File download = new File(filename); diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/SupportedFileTypes.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/SupportedFileTypes.java index 71f73ec893f5..4e689694518f 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/SupportedFileTypes.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/SupportedFileTypes.java @@ -62,7 +62,7 @@ public final class SupportedFileTypes { add(JPEG); add(JPG); add(PNG); } }); - + /* * Returns if the file with extension is supported. */ @@ -108,14 +108,21 @@ public static String detectMimeType(File pres) { return ""; } + public static String detectFileExtensionBasedOnMimeType(File pres) { + String mimeType = detectMimeType(pres); + return mimeTypeUtils.getExtensionBasedOnMimeType(mimeType); + } + public static Boolean isPresentationMimeTypeValid(File pres, String fileExtension) { String mimeType = detectMimeType(pres); if (mimeType.equals("")) { + log.error("Not able to detect mimeType."); return false; } if (!mimeTypeUtils.getValidMimeTypes().contains(mimeType)) { + log.error("MimeType is not valid for this meeting, [{}]", mimeType); return false; } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/UploadedPresentation.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/UploadedPresentation.java index b2ce2bdae7c4..f6a898c42b00 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/UploadedPresentation.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/UploadedPresentation.java @@ -21,6 +21,7 @@ import java.io.File; import java.util.ArrayList; +import org.apache.commons.io.FilenameUtils; public final class UploadedPresentation { private final String podId; @@ -34,6 +35,7 @@ public final class UploadedPresentation { private String fileType = "unknown"; private int numberOfPages = 0; private String conversionStatus; + private String filenameConverted; private final String baseUrl; private boolean isDownloadable = false; private boolean isRemovable = true; @@ -212,4 +214,17 @@ public ArrayList getUploadFailReason() { public boolean getIsInitialPresentation() { return isInitialPresentation; } + + public String getFilenameConverted() { + if (filenameConverted != null) { + return filenameConverted; + } else { + return ""; + } + } + + public void generateFilenameConverted(String newExtension) { + String nameWithoutExtension = FilenameUtils.removeExtension(name); + this.filenameConverted = nameWithoutExtension.concat("." + newExtension); + } } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PresentationFileProcessor.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PresentationFileProcessor.java index 46dab216cfe5..aaa9ba08a5c5 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PresentationFileProcessor.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PresentationFileProcessor.java @@ -1,12 +1,10 @@ package org.bigbluebutton.presentation.imp; import com.google.gson.Gson; +import org.apache.commons.io.FilenameUtils; import org.bigbluebutton.api.Util; import org.bigbluebutton.presentation.*; -import org.bigbluebutton.presentation.messages.DocPageConversionStarted; -import org.bigbluebutton.presentation.messages.DocPageCountExceeded; -import org.bigbluebutton.presentation.messages.DocPageCountFailed; -import org.bigbluebutton.presentation.messages.PresentationConvertMessage; +import org.bigbluebutton.presentation.messages.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -68,7 +66,15 @@ public void run() { private void processMakePresentationDownloadableMsg(UploadedPresentation pres) { try { File presentationFileDir = pres.getUploadedFile().getParentFile(); - Util.makePresentationDownloadable(presentationFileDir, pres.getId(), pres.isDownloadable()); + if (!pres.getFilenameConverted().equals("")) { + String fileExtensionConverted = FilenameUtils.getExtension(pres.getFilenameConverted()); + Util.makePresentationDownloadable(presentationFileDir, pres.getId(), pres.isDownloadable(), + fileExtensionConverted); + + } + String fileExtensionOriginal = FilenameUtils.getExtension(pres.getName()); + Util.makePresentationDownloadable(presentationFileDir, pres.getId(), pres.isDownloadable(), + fileExtensionOriginal); } catch (IOException e) { log.error("Failed to make presentation downloadable: {}", e); } @@ -76,6 +82,7 @@ private void processMakePresentationDownloadableMsg(UploadedPresentation pres) { private void processUploadedPresentation(UploadedPresentation pres) { if (SupportedFileTypes.isPdfFile(pres.getFileType())) { + pres.generateFilenameConverted("pdf"); determineNumberOfPages(pres); sendDocPageConversionStartedProgress(pres); PresentationConvertMessage msg = new PresentationConvertMessage(pres); diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/SlidesGenerationProgressNotifier.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/SlidesGenerationProgressNotifier.java index ea64b56f5d1b..310a28ecc619 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/SlidesGenerationProgressNotifier.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/SlidesGenerationProgressNotifier.java @@ -108,12 +108,11 @@ public void sendConversionCompletedMessage(UploadedPresentation pres) { log.error("GeneratedSlidesInfoHelper was not set. Could not notify interested listeners."); return; } - DocPageCompletedProgress progress = new DocPageCompletedProgress(pres.getPodId(), pres.getMeetingId(), pres.getId(), pres.getTemporaryPresentationId(), pres.getId(), pres.getName(), "notUsedYet", "notUsedYet", pres.isDownloadable(), pres.isRemovable(), ConversionMessageConstants.CONVERSION_COMPLETED_KEY, - pres.getNumberOfPages(), generateBasePresUrl(pres), pres.isCurrent(), pres.getIsInitialPresentation()); + pres.getNumberOfPages(), generateBasePresUrl(pres), pres.isCurrent(), pres.getIsInitialPresentation(), pres.getFilenameConverted()); messagingService.sendDocConversionMsg(progress); } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/messages/DocPageCompletedProgress.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/messages/DocPageCompletedProgress.java index 3c3cdb136bbd..102f9ac77049 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/messages/DocPageCompletedProgress.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/messages/DocPageCompletedProgress.java @@ -16,11 +16,14 @@ public class DocPageCompletedProgress implements IDocConversionMsg { public final String presBaseUrl; public final Boolean current; public final Boolean isInitialPresentation; + public final String filenameConverted; + public DocPageCompletedProgress(String podId, String meetingId, String presId, String temporaryPresentationId, String presInstance, String filename, String uploaderId, String authzToken, Boolean downloadable, Boolean removable, String key, - Integer numPages, String presBaseUrl, Boolean current, Boolean isInitialPresentation) { + Integer numPages, String presBaseUrl, Boolean current, + Boolean isInitialPresentation, String filenameConverted) { this.podId = podId; this.meetingId = meetingId; this.presId = presId; @@ -36,5 +39,6 @@ public DocPageCompletedProgress(String podId, String meetingId, String presId, S this.presBaseUrl = presBaseUrl; this.current = current; this.isInitialPresentation = isInitialPresentation; + this.filenameConverted = filenameConverted; } } diff --git a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/BbbWebApiGWApp.scala b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/BbbWebApiGWApp.scala index 22ec7e5245f5..663a4479afeb 100755 --- a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/BbbWebApiGWApp.scala +++ b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/BbbWebApiGWApp.scala @@ -224,7 +224,8 @@ class BbbWebApiGWApp( hideUserList = lockSettingsParams.hideUserList.booleanValue(), lockOnJoin = lockSettingsParams.lockOnJoin.booleanValue(), lockOnJoinConfigurable = lockSettingsParams.lockOnJoinConfigurable.booleanValue(), - hideViewersCursor = lockSettingsParams.hideViewersCursor.booleanValue() + hideViewersCursor = lockSettingsParams.hideViewersCursor.booleanValue(), + hideViewersAnnotation = lockSettingsParams.hideViewersAnnotation.booleanValue() ) val systemProps = SystemProps( diff --git a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/MsgBuilder.scala b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/MsgBuilder.scala index 09532942bd66..61011785431a 100755 --- a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/MsgBuilder.scala +++ b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/MsgBuilder.scala @@ -148,8 +148,9 @@ object MsgBuilder { val pages = generatePresentationPages(msg.presId, msg.numPages.intValue(), msg.presBaseUrl) val presentation = PresentationVO(msg.presId, msg.temporaryPresentationId, msg.filename, - current = msg.current.booleanValue(), pages.values.toVector, msg.downloadable.booleanValue(), msg.removable.booleanValue(), - isInitialPresentation = msg.isInitialPresentation) + current = msg.current.booleanValue(), pages.values.toVector, msg.downloadable.booleanValue(), + msg.removable.booleanValue(), + isInitialPresentation = msg.isInitialPresentation, msg.filenameConverted) val body = PresentationConversionCompletedSysPubMsgBody(podId = msg.podId, messageKey = msg.key, code = msg.key, presentation) diff --git a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/bus/ReceivedJsonMsgHdlrActor.scala b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/bus/ReceivedJsonMsgHdlrActor.scala index a7f9d8f5ed83..d91cca326ccb 100755 --- a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/bus/ReceivedJsonMsgHdlrActor.scala +++ b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/bus/ReceivedJsonMsgHdlrActor.scala @@ -86,6 +86,8 @@ class ReceivedJsonMsgHdlrActor(val msgFromAkkaAppsEventBus: MsgFromAkkaAppsEvent route[UserBroadcastCamStoppedEvtMsg](envelope, jsonNode) case UserRoleChangedEvtMsg.NAME => route[UserRoleChangedEvtMsg](envelope, jsonNode) + case UserSpeechLocaleChangedEvtMsg.NAME => + route[UserSpeechLocaleChangedEvtMsg](envelope, jsonNode) case CreateBreakoutRoomSysCmdMsg.NAME => route[CreateBreakoutRoomSysCmdMsg](envelope, jsonNode) case PresentationUploadTokenSysPubMsg.NAME => diff --git a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala index 6e4092228d7a..fb5b01bd0929 100755 --- a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala +++ b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala @@ -184,7 +184,8 @@ class OldMeetingMsgHdlrActor(val olgMsgGW: OldMessageReceivedGW) val presId = msg.body.presentationId val downloadable = msg.body.downloadable val presFilename = msg.body.presFilename - val m = new MakePresentationDownloadableMsg(meetingId, presId, presFilename, downloadable) + val downloadableExtension = msg.body.downloadableExtension; + val m = new MakePresentationDownloadableMsg(meetingId, presId, presFilename, downloadable, downloadableExtension) olgMsgGW.handle(m) } diff --git a/bbb-etherpad.placeholder.sh b/bbb-etherpad.placeholder.sh index 3ef5b11ccd1b..fefc1ce2cb6e 100755 --- a/bbb-etherpad.placeholder.sh +++ b/bbb-etherpad.placeholder.sh @@ -1,2 +1,2 @@ -git clone --branch 1.9.1 --depth 1 https://github.com/ether/etherpad-lite bbb-etherpad +git clone --branch 1.9.3 --depth 1 https://github.com/ether/etherpad-lite bbb-etherpad diff --git a/bbb-export-annotations/config/settings.json b/bbb-export-annotations/config/settings.json index d3b3354fcc3b..8352a33b808d 100644 --- a/bbb-export-annotations/config/settings.json +++ b/bbb-export-annotations/config/settings.json @@ -25,7 +25,7 @@ "notifier": { "pod_id": "DEFAULT_PRESENTATION_POD", "is_downloadable": "false", - "msgName": "NewPresAnnFileAvailableMsg" + "msgName": "NewPresFileAvailableMsg" }, "bbbWebAPI": "http://127.0.0.1:8090", "bbbPadsAPI": "http://127.0.0.1:9002", diff --git a/bbb-export-annotations/lib/utils/message-builder.js b/bbb-export-annotations/lib/utils/message-builder.js index db2740a74f88..809d1033cfa4 100644 --- a/bbb-export-annotations/lib/utils/message-builder.js +++ b/bbb-export-annotations/lib/utils/message-builder.js @@ -53,7 +53,7 @@ class PresAnnStatusMsg { } }; -class NewPresAnnFileAvailableMsg { +class NewPresFileAvailableMsg { constructor(exportJob, link) { this.message = { envelope: { @@ -70,8 +70,11 @@ class NewPresAnnFileAvailableMsg { userId: '', }, body: { - fileURI: link, + annotatedFileURI: link, + originalFileURI: '', + convertedFileURI: '', presId: exportJob.presId, + fileStateType: 'Annotated', }, }, }; @@ -84,5 +87,5 @@ class NewPresAnnFileAvailableMsg { module.exports = { PresAnnStatusMsg, - NewPresAnnFileAvailableMsg, + NewPresFileAvailableMsg, }; diff --git a/bbb-export-annotations/workers/notifier.js b/bbb-export-annotations/workers/notifier.js index 639418da6f9b..d42c8cc1e5b2 100644 --- a/bbb-export-annotations/workers/notifier.js +++ b/bbb-export-annotations/workers/notifier.js @@ -5,7 +5,7 @@ const FormData = require('form-data'); const redis = require('redis'); const axios = require('axios').default; const path = require('path'); -const {NewPresAnnFileAvailableMsg} = require('../lib/utils/message-builder'); +const {NewPresFileAvailableMsg} = require('../lib/utils/message-builder'); const {workerData} = require('worker_threads'); const [jobType, jobId, filename] = [workerData.jobType, workerData.jobId, workerData.filename]; @@ -32,7 +32,7 @@ async function notifyMeetingActor() { exportJob.parentMeetingId, exportJob.parentMeetingId, exportJob.presId, 'pdf', jobId, filename); - const notification = new NewPresAnnFileAvailableMsg(exportJob, link); + const notification = new NewPresFileAvailableMsg(exportJob, link); logger.info(`Annotated PDF available at ${link}`); await client.publish(config.redis.channels.publish, notification.build()); diff --git a/bbb-learning-dashboard/package-lock.json b/bbb-learning-dashboard/package-lock.json index 094e5cff3b8d..840c784c78ec 100644 --- a/bbb-learning-dashboard/package-lock.json +++ b/bbb-learning-dashboard/package-lock.json @@ -43,10 +43,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.16.7", - "license": "MIT", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dependencies": { - "@babel/highlight": "^7.16.7" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" @@ -104,11 +106,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.18.2", - "license": "MIT", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dependencies": { - "@babel/types": "^7.18.2", - "@jridgewell/gen-mapping": "^0.3.0", + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "engines": { @@ -116,10 +120,11 @@ } }, "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.1", - "license": "MIT", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "dependencies": { - "@jridgewell/set-array": "^1.0.0", + "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.9" }, @@ -215,8 +220,9 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.2", - "license": "MIT", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "engines": { "node": ">=6.9.0" } @@ -232,21 +238,23 @@ } }, "node_modules/@babel/helper-function-name": { - "version": "7.17.9", - "license": "MIT", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dependencies": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.16.7", - "license": "MIT", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dependencies": { - "@babel/types": "^7.16.7" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -353,18 +361,28 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.16.7", - "license": "MIT", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dependencies": { - "@babel/types": "^7.16.7" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "license": "MIT", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } @@ -402,11 +420,12 @@ } }, "node_modules/@babel/highlight": { - "version": "7.17.12", - "license": "MIT", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -414,8 +433,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.18.4", - "license": "MIT", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "bin": { "parser": "bin/babel-parser.js" }, @@ -1638,29 +1658,31 @@ } }, "node_modules/@babel/template": { - "version": "7.16.7", - "license": "MIT", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.18.2", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.18.2", - "@babel/helper-environment-visitor": "^7.18.2", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.18.0", - "@babel/types": "^7.18.2", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -1669,10 +1691,12 @@ } }, "node_modules/@babel/types": { - "version": "7.18.4", - "license": "MIT", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -2799,8 +2823,9 @@ } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.0.7", - "license": "MIT", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", "engines": { "node": ">=6.0.0" } @@ -2813,18 +2838,20 @@ } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.2", - "license": "MIT", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" } }, "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.1", - "license": "MIT", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "dependencies": { - "@jridgewell/set-array": "^1.0.0", + "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.9" }, @@ -2833,15 +2860,17 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.13", - "license": "MIT" + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.13", - "license": "MIT", + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@leichtgewicht/ip-codec": { @@ -3632,8 +3661,9 @@ } }, "node_modules/@types/estree": { - "version": "0.0.51", - "license": "MIT" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.3.tgz", + "integrity": "sha512-CS2rOaoQ/eAgAfcTfq6amKG7bsN+EMcgGY4FAFQdvSj2y1ixvOZTUA9mOtCai7E1SYu283XNw7urKK30nP3wkQ==" }, "node_modules/@types/express": { "version": "4.17.13", @@ -3863,8 +3893,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.3.7", - "license": "ISC", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -3993,8 +4024,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.3.7", - "license": "ISC", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -4066,128 +4098,145 @@ } }, "node_modules/@webassemblyjs/ast": { - "version": "1.11.1", - "license": "MIT", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.1", - "license": "MIT" + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==" }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.1", - "license": "MIT" + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==" }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.1", - "license": "MIT" + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==" }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.1", - "license": "MIT", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.1", - "license": "MIT" + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==" }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.1", - "license": "MIT", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.1", - "license": "MIT", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.1", - "license": "Apache-2.0", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.1", - "license": "MIT" + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==" }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.1", - "license": "MIT", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/helper-wasm-section": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-opt": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "@webassemblyjs/wast-printer": "1.11.1" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.1", - "license": "MIT", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.1", - "license": "MIT", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.1", - "license": "MIT", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.1", - "license": "MIT", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", "dependencies": { - "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/ast": "1.11.6", "@xtuc/long": "4.2.2" } }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" }, "node_modules/@xtuc/long": { "version": "4.2.2", - "license": "Apache-2.0" + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" }, "node_modules/abab": { "version": "2.0.6", @@ -4930,7 +4979,9 @@ "license": "BSD-2-Clause" }, "node_modules/browserslist": { - "version": "4.20.4", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", "funding": [ { "type": "opencollective", @@ -4939,15 +4990,17 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001349", - "electron-to-chromium": "^1.4.147", - "escalade": "^3.1.1", - "node-releases": "^2.0.5", - "picocolors": "^1.0.0" + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" }, "bin": { "browserslist": "cli.js" @@ -5038,7 +5091,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001350", + "version": "1.0.30001551", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001551.tgz", + "integrity": "sha512-vtBAez47BoGMMzlbYhfXrMV1kvRF2WP/lqiMuDu1Sb4EE4LKEgjopFDSRtZfdVnslNRpOqV/woE+Xgrwj6VQlg==", "funding": [ { "type": "opencollective", @@ -5047,9 +5102,12 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] }, "node_modules/case-sensitive-paths-webpack-plugin": { "version": "2.4.0", @@ -5344,24 +5402,17 @@ } }, "node_modules/core-js-compat": { - "version": "3.22.8", - "license": "MIT", + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.0.tgz", + "integrity": "sha512-0w4LcLXsVEuNkIqwjjf9rjCoPhK8uqA4tMRh4Ge26vfLtUutshn+aRJU21I9LCJlh2QQHfisNToLjw1XEJLTWw==", "dependencies": { - "browserslist": "^4.20.3", - "semver": "7.0.0" + "browserslist": "^4.22.1" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" } }, - "node_modules/core-js-compat/node_modules/semver": { - "version": "7.0.0", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/core-js-pure": { "version": "3.22.8", "hasInstallScript": true, @@ -5475,8 +5526,9 @@ } }, "node_modules/css-loader/node_modules/semver": { - "version": "7.3.7", - "license": "ISC", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -5944,8 +5996,9 @@ "license": "MIT" }, "node_modules/dns-packet": { - "version": "5.3.1", - "license": "MIT", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" }, @@ -6083,8 +6136,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.148", - "license": "ISC" + "version": "1.4.559", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.559.tgz", + "integrity": "sha512-iS7KhLYCSJbdo3rUSkhDTVuFNCV34RKs2UaB9Ecr7VlqzjjWW//0nfsFF5dtDmyXlZQaDYYtID5fjtC/6lpRug==" }, "node_modules/emittery": { "version": "0.8.1", @@ -6115,8 +6169,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.9.3", - "license": "MIT", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -6196,8 +6251,9 @@ "license": "MIT" }, "node_modules/es-module-lexer": { - "version": "0.9.3", - "license": "MIT" + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz", + "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==" }, "node_modules/es-shim-unscopables": { "version": "1.0.0", @@ -6746,8 +6802,9 @@ } }, "node_modules/eslint/node_modules/semver": { - "version": "7.3.7", - "license": "ISC", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -7314,8 +7371,9 @@ } }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": { - "version": "7.3.7", - "license": "ISC", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -9611,8 +9669,9 @@ } }, "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.3.7", - "license": "ISC", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -10697,8 +10756,15 @@ } }, "node_modules/nanoid": { - "version": "3.3.4", - "license": "MIT", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -10741,8 +10807,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.5", - "license": "MIT" + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" }, "node_modules/normalize-path": { "version": "3.0.0", @@ -11258,7 +11325,9 @@ } }, "node_modules/postcss": { - "version": "8.4.14", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "funding": [ { "type": "opencollective", @@ -11267,11 +11336,14 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -11688,8 +11760,9 @@ } }, "node_modules/postcss-loader/node_modules/semver": { - "version": "7.3.7", - "license": "ISC", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -12438,6 +12511,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, "node_modules/queue-microtask": { "version": "1.2.3", "funding": [ @@ -13123,8 +13201,9 @@ } }, "node_modules/react-scripts/node_modules/semver": { - "version": "7.3.7", - "license": "ISC", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -13603,8 +13682,9 @@ } }, "node_modules/schema-utils": { - "version": "3.1.1", - "license": "MIT", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", @@ -13633,8 +13713,9 @@ } }, "node_modules/semver": { - "version": "6.3.0", - "license": "ISC", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } @@ -13677,8 +13758,9 @@ "license": "MIT" }, "node_modules/serialize-javascript": { - "version": "6.0.0", - "license": "BSD-3-Clause", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", "dependencies": { "randombytes": "^2.1.0" } @@ -14420,11 +14502,12 @@ } }, "node_modules/terser": { - "version": "5.15.0", - "license": "BSD-2-Clause", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.22.0.tgz", + "integrity": "sha512-hHZVLgRA2z4NWcN6aS5rQDc+7Dcy58HOf2zbYwmFcQ+ua3h6eEFf5lIDKTzbWwlazPyOZsFQO8V80/IjVNExEw==", "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -14436,14 +14519,15 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.3", - "license": "MIT", + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.7", + "@jridgewell/trace-mapping": "^0.3.17", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.0", - "terser": "^5.7.2" + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" }, "engines": { "node": ">= 10.13.0" @@ -14468,8 +14552,9 @@ } }, "node_modules/terser/node_modules/acorn": { - "version": "8.7.1", - "license": "MIT", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "bin": { "acorn": "bin/acorn" }, @@ -14534,20 +14619,23 @@ } }, "node_modules/tough-cookie": { - "version": "4.0.0", - "license": "BSD-3-Clause", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", - "universalify": "^0.1.2" + "universalify": "^0.2.0", + "url-parse": "^1.5.3" }, "engines": { "node": ">=6" } }, "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.1.2", - "license": "MIT", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", "engines": { "node": ">= 4.0.0" } @@ -14753,6 +14841,35 @@ "yarn": "*" } }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "license": "BSD-2-Clause", @@ -14760,6 +14877,15 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "license": "MIT" @@ -14875,20 +15001,21 @@ } }, "node_modules/webpack": { - "version": "5.73.0", - "license": "MIT", + "version": "5.89.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", + "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", "dependencies": { "@types/eslint-scope": "^3.7.3", - "@types/estree": "^0.0.51", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "acorn": "^8.4.1", - "acorn-import-assertions": "^1.7.6", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.9.3", - "es-module-lexer": "^0.9.0", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", @@ -14897,10 +15024,10 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.1.0", + "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.3.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", "webpack-sources": "^3.2.3" }, "bin": { @@ -15131,8 +15258,9 @@ } }, "node_modules/webpack/node_modules/acorn": { - "version": "8.7.1", - "license": "MIT", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "bin": { "acorn": "bin/acorn" }, @@ -15141,8 +15269,9 @@ } }, "node_modules/webpack/node_modules/acorn-import-assertions": { - "version": "1.8.0", - "license": "MIT", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", "peerDependencies": { "acorn": "^8" } @@ -15231,8 +15360,9 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", - "license": "MIT", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "engines": { "node": ">=0.10.0" } @@ -15648,9 +15778,12 @@ } }, "@babel/code-frame": { - "version": "7.16.7", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "requires": { - "@babel/highlight": "^7.16.7" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" } }, "@babel/compat-data": { @@ -15685,17 +15818,22 @@ } }, "@babel/generator": { - "version": "7.18.2", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "requires": { - "@babel/types": "^7.18.2", - "@jridgewell/gen-mapping": "^0.3.0", + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "dependencies": { "@jridgewell/gen-mapping": { - "version": "0.3.1", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "requires": { - "@jridgewell/set-array": "^1.0.0", + "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.9" } @@ -15757,7 +15895,9 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.18.2" + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==" }, "@babel/helper-explode-assignable-expression": { "version": "7.16.7", @@ -15766,16 +15906,20 @@ } }, "@babel/helper-function-name": { - "version": "7.17.9", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "requires": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { - "version": "7.16.7", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "requires": { - "@babel/types": "^7.16.7" + "@babel/types": "^7.22.5" } }, "@babel/helper-member-expression-to-functions": { @@ -15843,13 +15987,22 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.16.7", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "requires": { - "@babel/types": "^7.16.7" + "@babel/types": "^7.22.5" } }, + "@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==" + }, "@babel/helper-validator-identifier": { - "version": "7.16.7" + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==" }, "@babel/helper-validator-option": { "version": "7.16.7" @@ -15872,15 +16025,19 @@ } }, "@babel/highlight": { - "version": "7.17.12", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.18.4" + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==" }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.17.12", @@ -16550,32 +16707,39 @@ } }, "@babel/template": { - "version": "7.16.7", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" } }, "@babel/traverse": { - "version": "7.18.2", - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.18.2", - "@babel/helper-environment-visitor": "^7.18.2", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.18.0", - "@babel/types": "^7.18.2", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.18.4", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "requires": { - "@babel/helper-validator-identifier": "^7.16.7", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, @@ -17248,22 +17412,28 @@ } }, "@jridgewell/resolve-uri": { - "version": "3.0.7" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==" }, "@jridgewell/set-array": { "version": "1.1.1" }, "@jridgewell/source-map": { - "version": "0.3.2", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", "requires": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" }, "dependencies": { "@jridgewell/gen-mapping": { - "version": "0.3.1", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "requires": { - "@jridgewell/set-array": "^1.0.0", + "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.9" } @@ -17271,13 +17441,17 @@ } }, "@jridgewell/sourcemap-codec": { - "version": "1.4.13" + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "@jridgewell/trace-mapping": { - "version": "0.3.13", + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "@leichtgewicht/ip-codec": { @@ -17674,7 +17848,9 @@ } }, "@types/estree": { - "version": "0.0.51" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.3.tgz", + "integrity": "sha512-CS2rOaoQ/eAgAfcTfq6amKG7bsN+EMcgGY4FAFQdvSj2y1ixvOZTUA9mOtCai7E1SYu283XNw7urKK30nP3wkQ==" }, "@types/express": { "version": "4.17.13", @@ -17850,7 +18026,9 @@ "version": "5.2.0" }, "semver": { - "version": "7.3.7", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "requires": { "lru-cache": "^6.0.0" } @@ -17903,7 +18081,9 @@ }, "dependencies": { "semver": { - "version": "7.3.7", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "requires": { "lru-cache": "^6.0.0" } @@ -17942,111 +18122,145 @@ } }, "@webassemblyjs/ast": { - "version": "1.11.1", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", "requires": { - "@webassemblyjs/helper-numbers": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" } }, "@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.1" + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==" }, "@webassemblyjs/helper-api-error": { - "version": "1.11.1" + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==" }, "@webassemblyjs/helper-buffer": { - "version": "1.11.1" + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==" }, "@webassemblyjs/helper-numbers": { - "version": "1.11.1", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", "requires": { - "@webassemblyjs/floating-point-hex-parser": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", "@xtuc/long": "4.2.2" } }, "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.1" + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==" }, "@webassemblyjs/helper-wasm-section": { - "version": "1.11.1", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" } }, "@webassemblyjs/ieee754": { - "version": "1.11.1", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", "requires": { "@xtuc/ieee754": "^1.2.0" } }, "@webassemblyjs/leb128": { - "version": "1.11.1", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", "requires": { "@xtuc/long": "4.2.2" } }, "@webassemblyjs/utf8": { - "version": "1.11.1" + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==" }, "@webassemblyjs/wasm-edit": { - "version": "1.11.1", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/helper-wasm-section": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-opt": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "@webassemblyjs/wast-printer": "1.11.1" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" } }, "@webassemblyjs/wasm-gen": { - "version": "1.11.1", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" } }, "@webassemblyjs/wasm-opt": { - "version": "1.11.1", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" } }, "@webassemblyjs/wasm-parser": { - "version": "1.11.1", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" } }, "@webassemblyjs/wast-printer": { - "version": "1.11.1", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", "requires": { - "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/ast": "1.11.6", "@xtuc/long": "4.2.2" } }, "@xtuc/ieee754": { - "version": "1.2.0" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" }, "@xtuc/long": { - "version": "4.2.2" + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" }, "abab": { "version": "2.0.6" @@ -18509,13 +18723,14 @@ "version": "1.0.0" }, "browserslist": { - "version": "4.20.4", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", "requires": { - "caniuse-lite": "^1.0.30001349", - "electron-to-chromium": "^1.4.147", - "escalade": "^3.1.1", - "node-releases": "^2.0.5", - "picocolors": "^1.0.0" + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" } }, "bser": { @@ -18566,7 +18781,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001350" + "version": "1.0.30001551", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001551.tgz", + "integrity": "sha512-vtBAez47BoGMMzlbYhfXrMV1kvRF2WP/lqiMuDu1Sb4EE4LKEgjopFDSRtZfdVnslNRpOqV/woE+Xgrwj6VQlg==" }, "case-sensitive-paths-webpack-plugin": { "version": "2.4.0" @@ -18742,15 +18959,11 @@ "version": "3.22.8" }, "core-js-compat": { - "version": "3.22.8", + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.0.tgz", + "integrity": "sha512-0w4LcLXsVEuNkIqwjjf9rjCoPhK8uqA4tMRh4Ge26vfLtUutshn+aRJU21I9LCJlh2QQHfisNToLjw1XEJLTWw==", "requires": { - "browserslist": "^4.20.3", - "semver": "7.0.0" - }, - "dependencies": { - "semver": { - "version": "7.0.0" - } + "browserslist": "^4.22.1" } }, "core-js-pure": { @@ -18810,7 +19023,9 @@ }, "dependencies": { "semver": { - "version": "7.3.7", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "requires": { "lru-cache": "^6.0.0" } @@ -19079,7 +19294,9 @@ "version": "1.0.0" }, "dns-packet": { - "version": "5.3.1", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", "requires": { "@leichtgewicht/ip-codec": "^2.0.1" } @@ -19165,7 +19382,9 @@ } }, "electron-to-chromium": { - "version": "1.4.148" + "version": "1.4.559", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.559.tgz", + "integrity": "sha512-iS7KhLYCSJbdo3rUSkhDTVuFNCV34RKs2UaB9Ecr7VlqzjjWW//0nfsFF5dtDmyXlZQaDYYtID5fjtC/6lpRug==" }, "emittery": { "version": "0.8.1" @@ -19180,7 +19399,9 @@ "version": "1.0.2" }, "enhanced-resolve": { - "version": "5.9.3", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", "requires": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -19239,7 +19460,9 @@ "version": "1.0.0" }, "es-module-lexer": { - "version": "0.9.3" + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz", + "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==" }, "es-shim-unscopables": { "version": "1.0.0", @@ -19389,7 +19612,9 @@ "version": "4.0.0" }, "semver": { - "version": "7.3.7", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "requires": { "lru-cache": "^6.0.0" } @@ -19928,7 +20153,9 @@ } }, "semver": { - "version": "7.3.7", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "requires": { "lru-cache": "^6.0.0" } @@ -21291,7 +21518,9 @@ "version": "4.0.0" }, "semver": { - "version": "7.3.7", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "requires": { "lru-cache": "^6.0.0" } @@ -21955,7 +22184,9 @@ } }, "nanoid": { - "version": "3.3.4" + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==" }, "natural-compare": { "version": "1.4.0" @@ -21980,7 +22211,9 @@ "version": "0.4.0" }, "node-releases": { - "version": "2.0.5" + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" }, "normalize-path": { "version": "3.0.0" @@ -22273,9 +22506,11 @@ } }, "postcss": { - "version": "8.4.14", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "requires": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } @@ -22453,7 +22688,9 @@ }, "dependencies": { "semver": { - "version": "7.3.7", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "requires": { "lru-cache": "^6.0.0" } @@ -22851,6 +23088,11 @@ "side-channel": "^1.0.4" } }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, "queue-microtask": { "version": "1.2.3" }, @@ -23268,7 +23510,9 @@ } }, "semver": { - "version": "7.3.7", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "requires": { "lru-cache": "^6.0.0" } @@ -23533,7 +23777,9 @@ } }, "schema-utils": { - "version": "3.1.1", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "requires": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", @@ -23550,7 +23796,9 @@ } }, "semver": { - "version": "6.3.0" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" }, "send": { "version": "0.18.0", @@ -23587,7 +23835,9 @@ } }, "serialize-javascript": { - "version": "6.0.0", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", "requires": { "randombytes": "^2.1.0" } @@ -24063,16 +24313,20 @@ } }, "terser": { - "version": "5.15.0", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.22.0.tgz", + "integrity": "sha512-hHZVLgRA2z4NWcN6aS5rQDc+7Dcy58HOf2zbYwmFcQ+ua3h6eEFf5lIDKTzbWwlazPyOZsFQO8V80/IjVNExEw==", "requires": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "dependencies": { "acorn": { - "version": "8.7.1" + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==" }, "commander": { "version": "2.20.3" @@ -24080,13 +24334,15 @@ } }, "terser-webpack-plugin": { - "version": "5.3.3", + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", "requires": { - "@jridgewell/trace-mapping": "^0.3.7", + "@jridgewell/trace-mapping": "^0.3.17", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.0", - "terser": "^5.7.2" + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" } }, "test-exclude": { @@ -24122,15 +24378,20 @@ "version": "1.0.1" }, "tough-cookie": { - "version": "4.0.0", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "requires": { "psl": "^1.1.33", "punycode": "^2.1.1", - "universalify": "^0.1.2" + "universalify": "^0.2.0", + "url-parse": "^1.5.3" }, "dependencies": { "universalify": { - "version": "0.1.2" + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==" } } }, @@ -24251,12 +24512,30 @@ "upath": { "version": "1.2.0" }, + "update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, "uri-js": { "version": "4.4.1", "requires": { "punycode": "^2.1.0" } }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "util-deprecate": { "version": "1.0.2" }, @@ -24332,19 +24611,21 @@ "version": "6.1.0" }, "webpack": { - "version": "5.73.0", + "version": "5.89.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", + "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", "requires": { "@types/eslint-scope": "^3.7.3", - "@types/estree": "^0.0.51", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "acorn": "^8.4.1", - "acorn-import-assertions": "^1.7.6", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.9.3", - "es-module-lexer": "^0.9.0", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", @@ -24353,18 +24634,22 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.1.0", + "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.3.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", "webpack-sources": "^3.2.3" }, "dependencies": { "acorn": { - "version": "8.7.1" + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==" }, "acorn-import-assertions": { - "version": "1.8.0", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", "requires": {} } } @@ -24550,7 +24835,9 @@ } }, "word-wrap": { - "version": "1.2.3" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==" }, "workbox-background-sync": { "version": "6.5.3", diff --git a/bbb-libreoffice/docker/Dockerfile b/bbb-libreoffice/docker/Dockerfile index ffc6697a76ab..da1ddcb685bd 100644 --- a/bbb-libreoffice/docker/Dockerfile +++ b/bbb-libreoffice/docker/Dockerfile @@ -1,3 +1,2 @@ -FROM amazoncorretto:17-alpine +FROM bigbluebutton/bbb-libreoffice:7.5.7-2023-10-19-110211 -RUN apk add fontconfig libreoffice diff --git a/bbb-playback.placeholder.sh b/bbb-playback.placeholder.sh index 73f1eb09f7a6..8ccae7be75c3 100755 --- a/bbb-playback.placeholder.sh +++ b/bbb-playback.placeholder.sh @@ -1 +1 @@ -git clone --branch v5.0.0 --depth 1 https://github.com/bigbluebutton/bbb-playback bbb-playback +git clone --branch v5.0.1 --depth 1 https://github.com/bigbluebutton/bbb-playback bbb-playback diff --git a/bbb-presentation-video.placeholder.sh b/bbb-presentation-video.placeholder.sh index 4d5d35056f99..8285524966f2 100755 --- a/bbb-presentation-video.placeholder.sh +++ b/bbb-presentation-video.placeholder.sh @@ -1,6 +1,6 @@ #!/bin/sh set -ex -RELEASE=4.0.3 +RELEASE=4.0.4 cat < + diff --git a/bbb-webrtc-recorder.placeholder.sh b/bbb-webrtc-recorder.placeholder.sh new file mode 100755 index 000000000000..56cc7be9d298 --- /dev/null +++ b/bbb-webrtc-recorder.placeholder.sh @@ -0,0 +1 @@ +git clone --branch v0.5.1 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-recorder bbb-webrtc-recorder diff --git a/bbb-webrtc-sfu.placeholder.sh b/bbb-webrtc-sfu.placeholder.sh index e3baf72bd308..e5cca0710927 100755 --- a/bbb-webrtc-sfu.placeholder.sh +++ b/bbb-webrtc-sfu.placeholder.sh @@ -1 +1 @@ -git clone --branch v2.9.15 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu +git clone --branch v2.12.0-beta.1 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu diff --git a/bigbluebutton-config/bigbluebutton-release b/bigbluebutton-config/bigbluebutton-release index 14ca5b38b12a..47b2fe59a361 100644 --- a/bigbluebutton-config/bigbluebutton-release +++ b/bigbluebutton-config/bigbluebutton-release @@ -1 +1 @@ -BIGBLUEBUTTON_RELEASE=2.6.14 +BIGBLUEBUTTON_RELEASE=2.7.2 diff --git a/bigbluebutton-config/bin/bbb-conf b/bigbluebutton-config/bin/bbb-conf index 75bea915dfcf..7c562e4eccb6 100755 --- a/bigbluebutton-config/bin/bbb-conf +++ b/bigbluebutton-config/bin/bbb-conf @@ -154,6 +154,14 @@ get_bbb_web_config_value() { RECORD_CONFIG=/usr/local/bigbluebutton/core/scripts/bigbluebutton.yml +WEBRTC_RECORDER_DEFAULT_CONFIG=/etc/bbb-webrtc-recorder/bbb-webrtc-recorder.yml +WEBRTC_RECORDER_ETC_CONFIG=/etc/bigbluebutton/bbb-webrtc-recorder.yml +if [ -f $WEBRTC_RECORDER_ETC_CONFIG ]; then + WEBRTC_RECORDER_CONFIG=$(yq m -x $WEBRTC_RECORDER_DEFAULT_CONFIG $WEBRTC_RECORDER_ETC_CONFIG) +else + WEBRTC_RECORDER_CONFIG=$(yq r $WEBRTC_RECORDER_DEFAULT_CONFIG) +fi + HTML5_DEFAULT_CONFIG=/usr/share/meteor/bundle/programs/server/assets/app/config/settings.yml HTML5_ETC_CONFIG=/etc/bigbluebutton/bbb-html5.yml if [ -f $HTML5_ETC_CONFIG ]; then @@ -407,16 +415,7 @@ display_bigbluebutton_status () { fi if [ -f /usr/lib/systemd/system/bbb-html5.service ]; then - units="$units mongod bbb-html5 bbb-webrtc-sfu kurento-media-server" - - for i in `seq 8888 8890`; do - # check if multi-kurento setup is configured - if [ -f /usr/lib/systemd/system/kurento-media-server-${i}.service ]; then - if systemctl is-enabled kurento-media-server-${i}.service > /dev/null; then - units="$units kurento-media-server-${i}" - fi - fi - done + units="$units mongod bbb-html5" source /usr/share/meteor/bundle/bbb-html5-with-roles.conf @@ -433,6 +432,27 @@ display_bigbluebutton_status () { done fi + if [ -f /usr/lib/systemd/system/bbb-webrtc-sfu.service ]; then + units="$units bbb-webrtc-sfu" + fi + + if [ -f /usr/lib/systemd/system/bbb-webrtc-recorder.service ]; then + units="$units bbb-webrtc-recorder" + fi + + if [ -f /usr/lib/systemd/system/kurento-media-server.service ]; then + units="$units kurento-media-server" + fi + + for i in `seq 8888 8890`; do + # check if multi-kurento setup is configured + if [ -f /usr/lib/systemd/system/kurento-media-server-${i}.service ]; then + if systemctl is-enabled kurento-media-server-${i}.service > /dev/null; then + units="$units kurento-media-server-${i}" + fi + fi + done + if [ -f /usr/share/etherpad-lite/settings.json ]; then units="$units etherpad" fi @@ -469,11 +489,15 @@ display_bigbluebutton_status () { units="$units bbb-rap-starter" fi + if [ -f /usr/lib/systemd/system/bbb-transcription-controller.service ]; then + units="$units bbb-transcription-controller" + fi + if systemctl list-units --full -all | grep -q $TOMCAT_USER.service; then TOMCAT_SERVICE=$TOMCAT_USER fi - line='——————————————————————►' + line='—————————————————————————————►' for unit in $units; do status=$(systemctl is-active "$unit") if [ "$status" = "active" ]; then @@ -706,6 +730,9 @@ if [[ $PORT_RANGE ]]; then yq w -i $WEBRTC_SFU_ETC_CONFIG mediasoup.worker.rtcMinPort $START_PORT yq w -i $WEBRTC_SFU_ETC_CONFIG mediasoup.worker.rtcMaxPort $END_PORT + yq w -i $WEBRTC_RECORDER_DEFAULT_CONFIG webrtc.rtcMinPort $START_PORT + yq w -i $WEBRTC_RECORDER_DEFAULT_CONFIG webrtc.rtcMaxPort $END_PORT + echo echo "BigBlueButton's UDP port range is now $START_PORT-$END_PORT" echo @@ -1397,6 +1424,8 @@ if [ $CHECK ]; then echo " kurento: $(awk -F '=' '{if (! ($0 ~ /^;/) && $0 ~ /minPort/) print $2}' /etc/kurento/modules/kurento/BaseRtpEndpoint.conf.ini)-$(awk -F '=' '{if (! ($0 ~ /^;/) && $0 ~ /maxPort/) print $2}' /etc/kurento/modules/kurento/BaseRtpEndpoint.conf.ini)" echo " bbb-webrtc-sfu: $(echo "$WEBRTC_SFU_CONFIG" | yq r - mediasoup.worker.rtcMinPort)-$(echo "$WEBRTC_SFU_CONFIG" | yq r - mediasoup.worker.rtcMaxPort)" + echo " bbb-webrtc-recorder: $(echo "$WEBRTC_RECORDER_CONFIG" | yq r - webrtc.rtcMinPort)-$(echo "$WEBRTC_RECORDER_CONFIG" | yq r - webrtc.rtcMaxPort)" + # if [ -f ${LTI_DIR}/WEB-INF/classes/lti-config.properties ]; then @@ -1431,10 +1460,20 @@ if [ $CHECK ]; then echo " kurento.ip: $(echo "$WEBRTC_SFU_CONFIG" | yq r - kurento[0].ip)" echo " kurento.url: $(echo "$WEBRTC_SFU_CONFIG" | yq r - kurento[0].url)" echo " freeswitch.sip_ip: $(echo "$WEBRTC_SFU_CONFIG" | yq r - freeswitch.sip_ip)" + echo " recordingAdapter: $(echo "$WEBRTC_SFU_CONFIG" | yq r - recordingAdapter)" echo " recordScreenSharing: $(echo "$WEBRTC_SFU_CONFIG" | yq r - recordScreenSharing)" echo " recordWebcams: $(echo "$WEBRTC_SFU_CONFIG" | yq r - recordWebcams)" echo " codec_video_main: $(echo "$WEBRTC_SFU_CONFIG" | yq r - conference-media-specs.codec_video_main)" echo " codec_video_content: $(echo "$WEBRTC_SFU_CONFIG" | yq r - conference-media-specs.codec_video_content)" + + fi + + if [ -n "$WEBRTC_RECORDER_CONFIG" ]; then + echo + echo "/etc/bbb-webrtc-recorder/bbb-webrtc-recorder.yml (bbb-webrtc-recorder)" + echo "/etc/bigbluebutton/bbb-webrtc-recorder.yml (bbb-webrtc-recorder - override)" + echo " debug: $(echo "$WEBRTC_RECORDER_CONFIG" | yq r - debug)" + echo " recorder.directory: $(echo "$WEBRTC_RECORDER_CONFIG" | yq r - recorder.directory)" fi if [ -n "$HTML5_CONFIG" ]; then @@ -1689,7 +1728,9 @@ if [ -n "$HOST" ]; then sudo yq w -i /usr/local/bigbluebutton/bbb-webrtc-sfu/config/default.yml freeswitch.esl_password "$ESL_PASSWORD" sudo xmlstarlet edit --inplace --update 'configuration/settings//param[@name="password"]/@value' --value $ESL_PASSWORD /opt/freeswitch/etc/freeswitch/autoload_configs/event_socket.conf.xml - + if [ -f /usr/local/bigbluebutton/bbb-transcription-controller/config/default.yml ]; then + sudo yq w -i /usr/local/bigbluebutton/bbb-transcription-controller/config/default.yml freeswitch.esl_password "$ESL_PASSWORD" + fi echo "Restarting BigBlueButton $BIGBLUEBUTTON_RELEASE ..." stop_bigbluebutton diff --git a/bigbluebutton-config/cron.daily/bigbluebutton b/bigbluebutton-config/cron.daily/bigbluebutton index 92df7f2528af..3a0b41f06388 100755 --- a/bigbluebutton-config/cron.daily/bigbluebutton +++ b/bigbluebutton-config/cron.daily/bigbluebutton @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License along # with BigBlueButton; if not, see . -# +# test -x /var/bigbluebutton || exit 0 @@ -44,6 +44,7 @@ find /var/bigbluebutton/ -maxdepth 1 -type d -name "*-[0-9]*" -mtime +$history - # kurento_dir=/var/kurento/ mediasoup_dir=/var/mediasoup/ +bbb_webrtc_recorder_dir=/var/lib/bbb-webrtc-recorder/ remove_stale_sfu_raw_files() { for app in recordings screenshare; do @@ -57,6 +58,7 @@ remove_stale_sfu_raw_files() { remove_stale_sfu_raw_files "$kurento_dir" remove_stale_sfu_raw_files "$mediasoup_dir" +remove_stale_sfu_raw_files "$bbb_webrtc_recorder_dir" # # Delete FreeSWITCH wav/opus recordings older than N days diff --git a/bigbluebutton-html5/client/main.html b/bigbluebutton-html5/client/main.html index 73d3d9335a4f..ee60144b4869 100755 --- a/bigbluebutton-html5/client/main.html +++ b/bigbluebutton-html5/client/main.html @@ -157,4 +157,5 @@ +
diff --git a/bigbluebutton-html5/client/main.jsx b/bigbluebutton-html5/client/main.jsx index 7df66f70d547..0988403386b8 100755 --- a/bigbluebutton-html5/client/main.jsx +++ b/bigbluebutton-html5/client/main.jsx @@ -45,7 +45,7 @@ import('/imports/api/audio/client/bridge/bridge-whitelist').catch(() => { const { disableWebsocketFallback } = Meteor.settings.public.app; if (disableWebsocketFallback) { - Meteor.connection._stream._sockjsProtocolsWhitelist = function () { return ['websocket']; } + Meteor.connection._stream._sockjsProtocolsWhitelist = function () { return ['websocket']; }; Meteor.disconnect(); Meteor.reconnect(); @@ -80,7 +80,7 @@ Meteor.startup(() => { // TODO make this a Promise render( - + <> @@ -93,7 +93,7 @@ Meteor.startup(() => { - + , document.getElementById('app'), ); diff --git a/bigbluebutton-html5/client/stylesheets/modals.css b/bigbluebutton-html5/client/stylesheets/modals.css index dbd6263f8de1..7c072ec471d1 100755 --- a/bigbluebutton-html5/client/stylesheets/modals.css +++ b/bigbluebutton-html5/client/stylesheets/modals.css @@ -38,3 +38,39 @@ border: none; background-color: transparent; } + +/* Prevents that an element within app shows over a modal */ +#app { + position: inherit; + z-index: 1000 !important; +} + +.modal-low { + z-index: 1001; +} + +.modal-medium { + z-index: 1002; +} + +.modal-high { + z-index: 1003; +} + +/* Within a same priority, hide all but first (FIFO) */ +.modal-low ~ .modal-low, +.modal-medium ~ .modal-medium, +.modal-high ~ .modal-high { + display: none; +} + +/* Hide all low priority modals when a medium or high priority modals are displayed */ +#modals-container:has(.modal-medium) .modal-low, +#modals-container:has(.modal-high) .modal-low { + display: none; +} + +/* Hide all medium priority modals when a high priority modal is displayed */ +#modals-container:has(.modal-high) .modal-medium { + display: none; +} diff --git a/bigbluebutton-html5/imports/api/annotations/addAnnotation.js b/bigbluebutton-html5/imports/api/annotations/addAnnotation.js index 003973012caf..f3cdb9700354 100755 --- a/bigbluebutton-html5/imports/api/annotations/addAnnotation.js +++ b/bigbluebutton-html5/imports/api/annotations/addAnnotation.js @@ -1,5 +1,5 @@ import { check } from 'meteor/check'; -import _ from 'lodash'; +import { defaultsDeep } from '/imports/utils/array-utils'; async function addAnnotation(meetingId, whiteboardId, userId, annotation, Annotations) { check(meetingId, String); @@ -19,7 +19,7 @@ async function addAnnotation(meetingId, whiteboardId, userId, annotation, Annota const oldAnnotation = await Annotations.findOneAsync(selector); if (oldAnnotation) { - annotationInfo = _.merge(oldAnnotation.annotationInfo, annotationInfo); + annotationInfo = defaultsDeep(annotationInfo, oldAnnotation.annotationInfo); } const modifier = { diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods/sendAnnotationHelper.js b/bigbluebutton-html5/imports/api/annotations/server/methods/sendAnnotationHelper.js index 728a66cb9506..3b39236edd38 100755 --- a/bigbluebutton-html5/imports/api/annotations/server/methods/sendAnnotationHelper.js +++ b/bigbluebutton-html5/imports/api/annotations/server/methods/sendAnnotationHelper.js @@ -1,4 +1,3 @@ -import _ from 'lodash'; import RedisPubSub from '/imports/startup/server/redis'; import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; @@ -14,7 +13,9 @@ export default function sendAnnotationHelper(annotations, meetingId, requesterUs // TODO see if really necessary, don't know if it's possible // to have annotations from different pages // group annotations by same whiteboardId - _.each(_.groupBy(annotations, "wbId"), (whiteboardAnnotations) => { + const groupedAnnotations = annotations.reduce((r, v, i, a, k = v.wbId) => ((r[k] || (r[k] = [])).push(v), r), {}) //groupBy wbId + + Object.entries(groupedAnnotations).forEach(([_, whiteboardAnnotations]) => { const whiteboardId = whiteboardAnnotations[0].wbId; check(whiteboardId, String); diff --git a/bigbluebutton-html5/imports/api/audio-captions/server/methods/updateTranscript.js b/bigbluebutton-html5/imports/api/audio-captions/server/methods/updateTranscript.js index 3b1b7bf68cf9..a00802a17e26 100644 --- a/bigbluebutton-html5/imports/api/audio-captions/server/methods/updateTranscript.js +++ b/bigbluebutton-html5/imports/api/audio-captions/server/methods/updateTranscript.js @@ -3,7 +3,7 @@ import RedisPubSub from '/imports/startup/server/redis'; import { extractCredentials } from '/imports/api/common/server/helpers'; import Logger from '/imports/startup/server/logger'; -export default function updateTranscript(transcriptId, start, end, text, transcript, locale) { +export default function updateTranscript(transcriptId, start, end, text, transcript, locale, isFinal) { try { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; @@ -19,6 +19,7 @@ export default function updateTranscript(transcriptId, start, end, text, transcr check(text, String); check(transcript, String); check(locale, String); + check(isFinal, Boolean); // Ignore irrelevant updates if (start !== -1 && end !== -1) { @@ -29,6 +30,7 @@ export default function updateTranscript(transcriptId, start, end, text, transcr text, transcript, locale, + result: isFinal, }; RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/sfu-audio-bridge.js b/bigbluebutton-html5/imports/api/audio/client/bridge/sfu-audio-bridge.js index 0c02be637188..e26fec57c750 100755 --- a/bigbluebutton-html5/imports/api/audio/client/bridge/sfu-audio-bridge.js +++ b/bigbluebutton-html5/imports/api/audio/client/bridge/sfu-audio-bridge.js @@ -8,6 +8,7 @@ import { getMappedFallbackStun, } from '/imports/utils/fetchStunTurnServers'; import getFromMeetingSettings from '/imports/ui/services/meeting-settings'; +import getFromUserSettings from '/imports/ui/services/users-settings'; import browserInfo from '/imports/utils/browserInfo'; import { getAudioSessionNumber, @@ -24,9 +25,12 @@ const TRACE_LOGS = Meteor.settings.public.kurento.traceLogs; const GATHERING_TIMEOUT = Meteor.settings.public.kurento.gatheringTimeout; const MEDIA = Meteor.settings.public.media; const DEFAULT_FULLAUDIO_MEDIA_SERVER = MEDIA.audio.fullAudioMediaServer; +const RETRY_THROUGH_RELAY = MEDIA.audio.retryThroughRelay || false; const LISTEN_ONLY_OFFERING = MEDIA.listenOnlyOffering; +const FULLAUDIO_OFFERING = MEDIA.fullAudioOffering; +const TRANSPARENT_LISTEN_ONLY = MEDIA.transparentListenOnly; const MEDIA_TAG = MEDIA.mediaTag.replace(/#/g, ''); -const RECONNECT_TIMEOUT_MS = MEDIA.listenOnlyCallTimeout || 15000; +const CONNECTION_TIMEOUT_MS = MEDIA.listenOnlyCallTimeout || 15000; const { audio: NETWORK_PRIORITY } = MEDIA.networkPriorities || {}; const SENDRECV_ROLE = 'sendrecv'; const RECV_ROLE = 'recv'; @@ -44,6 +48,9 @@ const errorCodeMap = { 1307: 1007, }; +// Error codes that are prone to a retry according to RETRY_THROUGH_RELAY +const RETRYABLE_ERRORS = [1007, 1010]; + const mapErrorCode = (error) => { const { errorCode } = error; const mappedErrorCode = errorCodeMap[errorCode]; @@ -67,7 +74,18 @@ const getMediaServerAdapter = (listenOnly = false) => { ); }; +const isTransparentListenOnlyEnabled = () => getFromUserSettings( + 'bbb_transparent_listen_only', + TRANSPARENT_LISTEN_ONLY, +); + export default class SFUAudioBridge extends BaseAudioBridge { + static getOfferingRole(isListenOnly) { + return isListenOnly + ? LISTEN_ONLY_OFFERING + : (!isTransparentListenOnlyEnabled() && FULLAUDIO_OFFERING); + } + constructor(userData) { super(); this.userId = userData.userId; @@ -76,8 +94,9 @@ export default class SFUAudioBridge extends BaseAudioBridge { this.broker = null; this.reconnecting = false; this.iceServers = []; - this.inEchoTest = false; this.bridgeName = BRIDGE_NAME; + + this.handleTermination = this.handleTermination.bind(this); } get inputStream() { @@ -111,73 +130,104 @@ export default class SFUAudioBridge extends BaseAudioBridge { return doGUM(constraints, true); } - handleTermination() { - return this.callback({ status: this.baseCallStates.ended, bridge: this.bridgeName }); + setConnectionTimeout() { + if (this.connectionTimeout) this.clearConnectionTimeout(); + + this.connectionTimeout = setTimeout(() => { + const error = new Error(`ICE negotiation timeout after ${CONNECTION_TIMEOUT_MS / 1000}s`); + error.errorCode = 1010; + // Duplicating key-vals because I can'decide settle on an error pattern - prlanzarin again + error.errorCause = error.message; + error.errorMessage = error.message; + this.handleBrokerFailure(error); + }, CONNECTION_TIMEOUT_MS); } - clearReconnectionTimeout() { - this.reconnecting = false; - if (this.reconnectionTimeout) { - clearTimeout(this.reconnectionTimeout); - this.reconnectionTimeout = null; + clearConnectionTimeout() { + if (this.connectionTimeout) { + clearTimeout(this.connectionTimeout); + this.connectionTimeout = null; } } - reconnect() { + dispatchAutoplayHandlingEvent(mediaElement) { + const tagFailedEvent = new CustomEvent('audioPlayFailed', { + detail: { mediaElement }, + }); + window.dispatchEvent(tagFailedEvent); + this.callback({ status: this.baseCallStates.autoplayBlocked, bridge: this.bridgeName }); + } + + reconnect(options = {}) { + // If broker has already started, fire the reconnecting callback so the user + // knows what's going on + if (this.broker.started) { + this.callback({ status: this.baseCallStates.reconnecting, bridge: this.bridgeName }); + } else { + // Otherwise: override termination handler so the ended callback doesn't get + // triggered - this is a retry attempt and the user shouldn't be notified + // yet. + this.broker.onended = () => {}; + } + this.broker.stop(); - this.callback({ status: this.baseCallStates.reconnecting, bridge: this.bridgeName }); this.reconnecting = true; - // Set up a reconnectionTimeout in case the server is unresponsive - // for some reason. If it gets triggered, end the session and stop - // trying to reconnect - this.reconnectionTimeout = setTimeout(() => { - this.callback({ - status: this.baseCallStates.failed, - error: 1010, - bridgeError: 'Reconnection timeout', - bridge: this.bridgeName, + this._startBroker({ isListenOnly: this.isListenOnly, ...options }) + .catch((error) => { + // Error handling is a no-op because it will be "handled" in handleBrokerFailure + logger.debug({ + logCode: 'sfuaudio_reconnect_failed', + extraInfo: { + errorMessage: error.errorMessage, + reconnecting: this.reconnecting, + bridge: this.bridgeName, + role: this.role, + }, + }, 'SFU audio reconnect failed'); }); - this.broker.stop(); - this.clearReconnectionTimeout(); - }, RECONNECT_TIMEOUT_MS); - - this.joinAudio({ isListenOnly: this.isListenOnly }, this.callback).then( - () => this.clearReconnectionTimeout(), - ).catch((error) => { - // Error handling is a no-op because it will be "handled" in handleBrokerFailure - logger.debug({ - logCode: 'sfuaudio_reconnect_failed', - extraInfo: { - errorMessage: error.errorMessage, - reconnecting: this.reconnecting, - bridge: this.bridgeName, - role: this.role, - }, - }, 'SFU audio reconnect failed'); - }); } handleBrokerFailure(error) { return new Promise((resolve, reject) => { + this.clearConnectionTimeout(); mapErrorCode(error); const { errorMessage, errorCause, errorCode } = error; - if (this.broker.started && !this.reconnecting) { - logger.error({ - logCode: 'sfuaudio_error_try_to_reconnect', - extraInfo: { - errorMessage, - errorCode, - errorCause, - bridge: this.bridgeName, - role: this.role, - }, - }, 'SFU audio failed, try to reconnect'); - this.reconnect(); - return resolve(); + if (!this.reconnecting) { + if (this.broker.started) { + logger.error({ + logCode: 'sfuaudio_error_try_to_reconnect', + extraInfo: { + errorMessage, + errorCode, + errorCause, + bridge: this.bridgeName, + role: this.role, + }, + }, 'SFU audio failed, try to reconnect'); + this.reconnect(); + return resolve(); + } + + if (RETRYABLE_ERRORS.includes(errorCode) && RETRY_THROUGH_RELAY) { + logger.error({ + logCode: 'sfuaudio_error_retry_through_relay', + extraInfo: { + errorMessage, + errorCode, + errorCause, + bridge: this.bridgeName, + role: this.role, + }, + }, 'SFU audio failed to connect, retry through relay'); + this.reconnect({ forceRelay: true }); + return resolve(); + } } + // Already tried reconnecting once OR the user handn't succesfully - // connected firsthand. Just finish the session and reject with error + // connected firsthand and retrying isn't an option. Finish the session + // and reject with the error logger.error({ logCode: 'sfuaudio_error', extraInfo: { @@ -189,7 +239,7 @@ export default class SFUAudioBridge extends BaseAudioBridge { role: this.role, }, }, 'SFU audio failed'); - this.clearReconnectionTimeout(); + this.clearConnectionTimeout(); this.broker.stop(); this.callback({ status: this.baseCallStates.failed, @@ -201,23 +251,23 @@ export default class SFUAudioBridge extends BaseAudioBridge { }); } - dispatchAutoplayHandlingEvent(mediaElement) { - const tagFailedEvent = new CustomEvent('audioPlayFailed', { - detail: { mediaElement }, - }); - window.dispatchEvent(tagFailedEvent); - this.callback({ status: this.baseCallStates.autoplayBlocked, bridge: this.bridgeName }); + handleTermination() { + this.clearConnectionTimeout(); + return this.callback({ status: this.baseCallStates.ended, bridge: this.bridgeName }); } handleStart() { const stream = this.broker.webRtcPeer.getRemoteStream(); const mediaElement = document.getElementById(MEDIA_TAG); - return loadAndPlayMediaStream(stream, mediaElement, false).then(() => this - .callback({ + return loadAndPlayMediaStream(stream, mediaElement, false).then(() => { + this.callback({ status: this.baseCallStates.started, bridge: this.bridgeName, - })).catch((error) => { + }); + this.clearConnectionTimeout(); + this.reconnecting = false; + }).catch((error) => { // NotAllowedError equals autoplay issues, fire autoplay handling event. // This will be handled in audio-manager. if (error.name === 'NotAllowedError') { @@ -230,6 +280,11 @@ export default class SFUAudioBridge extends BaseAudioBridge { }, }, 'SFU audio media play failed due to autoplay error'); this.dispatchAutoplayHandlingEvent(mediaElement); + // For connection purposes, this worked - the autoplay thing is a client + // side soft issue to be handled at the UI/UX level, not WebRTC/negotiation + // So: clear the connection timer + this.clearConnectionTimeout(); + this.reconnecting = false; } else { const normalizedError = { errorCode: 1004, @@ -247,9 +302,32 @@ export default class SFUAudioBridge extends BaseAudioBridge { } async _startBroker(options) { + try { + this.iceServers = await fetchWebRTCMappedStunTurnServers(this.sessionToken); + } catch (error) { + logger.error({ logCode: 'sfuaudio_stun-turn_fetch_failed' }, + 'SFU audio bridge failed to fetch STUN/TURN info, using default servers'); + this.iceServers = getMappedFallbackStun(); + } + return new Promise((resolve, reject) => { + const { + isListenOnly, + extension, + inputStream, + forceRelay: _forceRelay = false, + } = options; + + const handleInitError = (_error) => { + mapErrorCode(_error); + if (RETRYABLE_ERRORS.includes(_error?.errorCode) + || !RETRY_THROUGH_RELAY + || this.reconnecting) { + reject(_error); + } + }; + try { - const { isListenOnly, extension, inputStream } = options; this.inEchoTest = !!extension; this.isListenOnly = isListenOnly; @@ -259,14 +337,15 @@ export default class SFUAudioBridge extends BaseAudioBridge { iceServers: this.iceServers, mediaServer: getMediaServerAdapter(isListenOnly), constraints: getAudioConstraints({ deviceId: this.inputDeviceId }), - forceRelay: shouldForceRelay(), + forceRelay: _forceRelay || shouldForceRelay(), stream: (inputStream && inputStream.active) ? inputStream : undefined, - offering: isListenOnly ? LISTEN_ONLY_OFFERING : true, + offering: SFUAudioBridge.getOfferingRole(this.isListenOnly), signalCandidates: SIGNAL_CANDIDATES, traceLogs: TRACE_LOGS, networkPriority: NETWORK_PRIORITY, mediaStreamFactory: this.mediaStreamFactory, gatheringTimeout: GATHERING_TIMEOUT, + transparentListenOnly: isTransparentListenOnlyEnabled(), }; this.broker = new AudioBroker( @@ -283,25 +362,19 @@ export default class SFUAudioBridge extends BaseAudioBridge { this.handleStart().then(resolve).catch(reject); }; - this.broker.joinAudio().catch(reject); + // Set up a connectionTimeout in case the server or network are botching + // negotiation or conn checks. + this.setConnectionTimeout(); + this.broker.joinAudio().catch(handleInitError); } catch (error) { - logger.warn({ logCode: 'sfuaudio_bridge_broker_init_fail' }, - 'Problem when initializing SFU broker for fullaudio bridge'); - reject(error); + handleInitError(error); } }); } async joinAudio(options, callback) { this.callback = callback; - - try { - this.iceServers = await fetchWebRTCMappedStunTurnServers(this.sessionToken); - } catch (error) { - logger.error({ logCode: 'sfuaudio_stun-turn_fetch_failed' }, - 'SFU audio bridge failed to fetch STUN/TURN info, using default servers'); - this.iceServers = getMappedFallbackStun(); - } + this.reconnecting = false; return this._startBroker(options); } @@ -390,7 +463,8 @@ export default class SFUAudioBridge extends BaseAudioBridge { exitAudio() { const mediaElement = document.getElementById(MEDIA_TAG); - this.clearReconnectionTimeout(); + this.clearConnectionTimeout(); + this.reconnecting = false; if (this.broker) { this.broker.stop(); diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js index 910439276458..e7f013c9b76a 100755 --- a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js +++ b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js @@ -12,6 +12,7 @@ import { filterValidIceCandidates, analyzeSdp, logSelectedCandidate, + forceDisableStereo, } from '/imports/utils/sdpUtils'; import { Tracker } from 'meteor/tracker'; import VoiceCallStates from '/imports/api/voice-call-states'; @@ -25,6 +26,7 @@ import { filterSupportedConstraints, doGUM, } from '/imports/api/audio/client/bridge/service'; +import SpeechService from '/imports/ui/components/audio/captions/speech/service'; const MEDIA = Meteor.settings.public.media; const MEDIA_TAG = MEDIA.mediaTag; @@ -708,12 +710,25 @@ class SIPSession { const target = SIP.UserAgent.makeURI(`sip:${callExtension}@${hostname}`); const matchConstraints = getAudioConstraints({ deviceId: this.inputDeviceId }); + const sessionDescriptionHandlerModifiers = []; const iceModifiers = [ filterValidIceCandidates.bind(this, this.validIceCandidates), ]; if (!SIPJS_ALLOW_MDNS) iceModifiers.push(stripMDnsCandidates); + // The current Vosk provider does not support stereo when transcribing + // microphone streams, so we need to make sure it is forcefully disabled + // via SDP munging. Having it disabled on server side FS _does not suffice_ + // because the stereo parameter is client-mandated (ie replicated in the + // answer) + if (SpeechService.stereoUnsupported()) { + logger.debug({ + logCode: 'sipjs_transcription_disable_stereo', + }, 'Transcription provider does not support stereo, forcing stereo=0'); + sessionDescriptionHandlerModifiers.push(forceDisableStereo); + } + const inviterOptions = { sessionDescriptionHandlerOptions: { constraints: { @@ -724,6 +739,7 @@ class SIPSession { }, iceGatheringTimeout: ICE_GATHERING_TIMEOUT, }, + sessionDescriptionHandlerModifiers, sessionDescriptionHandlerModifiersPostICEGathering: iceModifiers, delegate: { onSessionDescriptionHandler: diff --git a/bigbluebutton-html5/imports/api/breakouts/server/handlers/breakoutList.js b/bigbluebutton-html5/imports/api/breakouts/server/handlers/breakoutList.js index c0fb7c2c57e0..9ff5d5cd46a5 100644 --- a/bigbluebutton-html5/imports/api/breakouts/server/handlers/breakoutList.js +++ b/bigbluebutton-html5/imports/api/breakouts/server/handlers/breakoutList.js @@ -11,6 +11,7 @@ export default async function handleBreakoutRoomsList({ body }, meetingId) { const { meetingId: parentMeetingId, rooms, + sendInviteToModerators, } = body; // set firstly the last seq, then client will know when receive all @@ -39,6 +40,7 @@ export default async function handleBreakoutRoomsList({ body }, meetingId) { joinedUsers: [], timeRemaining: DEFAULT_TIME_REMAINING, parentMeetingId, + sendInviteToModerators, ...flat(breakoutWithoutUrls), ...urls, }, diff --git a/bigbluebutton-html5/imports/api/breakouts/server/methods/createBreakout.js b/bigbluebutton-html5/imports/api/breakouts/server/methods/createBreakout.js index 635c73d2cbd8..2c3c80f9c6fa 100644 --- a/bigbluebutton-html5/imports/api/breakouts/server/methods/createBreakout.js +++ b/bigbluebutton-html5/imports/api/breakouts/server/methods/createBreakout.js @@ -4,7 +4,7 @@ import Logger from '/imports/startup/server/logger'; import { extractCredentials } from '/imports/api/common/server/helpers'; import { check } from 'meteor/check'; -export default function createBreakoutRoom(rooms, durationInMinutes, record = false, captureNotes = false, captureSlides = false) { +export default function createBreakoutRoom(rooms, durationInMinutes, record = false, captureNotes = false, captureSlides = false, sendInviteToModerators = false) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const BREAKOUT_LIM = Meteor.settings.public.app.breakouts.breakoutRoomLimit; @@ -29,6 +29,7 @@ export default function createBreakoutRoom(rooms, durationInMinutes, record = fa durationInMinutes, rooms, meetingId, + sendInviteToModerators, }; RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); diff --git a/bigbluebutton-html5/imports/api/breakouts/server/publishers.js b/bigbluebutton-html5/imports/api/breakouts/server/publishers.js index 7f16342dded9..85605129b723 100755 --- a/bigbluebutton-html5/imports/api/breakouts/server/publishers.js +++ b/bigbluebutton-html5/imports/api/breakouts/server/publishers.js @@ -35,6 +35,7 @@ async function breakouts() { timeRemaining: 1, captureNotes: 1, captureSlides: 1, + sendInviteToModerators: 1, }, }; diff --git a/bigbluebutton-html5/imports/api/cursor/server/handlers/cursorUpdate.js b/bigbluebutton-html5/imports/api/cursor/server/handlers/cursorUpdate.js index 066ee7bf3c78..e9377f98d15f 100755 --- a/bigbluebutton-html5/imports/api/cursor/server/handlers/cursorUpdate.js +++ b/bigbluebutton-html5/imports/api/cursor/server/handlers/cursorUpdate.js @@ -1,13 +1,13 @@ import { check } from 'meteor/check'; import CursorStreamer from '/imports/api/cursor/server/streamer'; import Logger from '/imports/startup/server/logger'; -import _ from 'lodash'; +import { throttle } from '/imports/utils/throttle'; const CURSOR_PROCCESS_INTERVAL = 30; const cursorQueue = {}; -const proccess = _.throttle(() => { +const proccess = throttle(() => { try { Object.keys(cursorQueue).forEach((meetingId) => { try { diff --git a/bigbluebutton-html5/imports/api/cursor/server/modifiers/updateCursor.js b/bigbluebutton-html5/imports/api/cursor/server/modifiers/updateCursor.js deleted file mode 100644 index 562ec58f0f92..000000000000 --- a/bigbluebutton-html5/imports/api/cursor/server/modifiers/updateCursor.js +++ /dev/null @@ -1,38 +0,0 @@ -import Logger from '/imports/startup/server/logger'; -import Cursor from '/imports/api/cursor'; -import { check } from 'meteor/check'; - -export default function updateCursor(meetingId, whiteboardId, userId, x = -1, y = -1) { - check(meetingId, String); - check(userId, String); - check(x, Number); - check(y, Number); - - const selector = { - meetingId, - whiteboardId, - userId, - }; - - const modifier = { - $set: { - meetingId, - whiteboardId, - userId, - x, - y, - }, - }; - - try { - const { insertedId } = Cursor.upsert(selector, modifier); - - if (insertedId) { - Logger.info(`Initialized cursor meeting=${meetingId}`); - } else { - Logger.debug('Updated cursor ', { meetingId }); - } - } catch (err) { - Logger.error(`Upserting cursor to collection: ${err}`); - } -} diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/handlers/groupChatMsgBroadcast.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/handlers/groupChatMsgBroadcast.js index 4af907d2bc90..6c0a0dd64e8b 100644 --- a/bigbluebutton-html5/imports/api/group-chat-msg/server/handlers/groupChatMsgBroadcast.js +++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/handlers/groupChatMsgBroadcast.js @@ -1,5 +1,5 @@ import { check } from 'meteor/check'; -import _ from 'lodash'; +import { throttle } from '/imports/utils/throttle'; import addGroupChatMsg from '../modifiers/addGroupChatMsg'; import addBulkGroupChatMsgs from '../modifiers/addBulkGroupChatMsgs'; @@ -7,7 +7,7 @@ const { bufferChatInsertsMs } = Meteor.settings.public.chat; const msgBuffer = []; -const bulkFn = _.throttle(addBulkGroupChatMsgs, bufferChatInsertsMs); +const bulkFn = throttle(addBulkGroupChatMsgs, bufferChatInsertsMs); export default async function handleGroupChatMsgBroadcast({ body }, meetingId) { const { chatId, msg } = body; diff --git a/bigbluebutton-html5/imports/api/local-settings/server/methods/userChangedLocalSettings.js b/bigbluebutton-html5/imports/api/local-settings/server/methods/userChangedLocalSettings.js index 67fd9898db1d..c9186a91c284 100644 --- a/bigbluebutton-html5/imports/api/local-settings/server/methods/userChangedLocalSettings.js +++ b/bigbluebutton-html5/imports/api/local-settings/server/methods/userChangedLocalSettings.js @@ -1,9 +1,9 @@ -import _ from 'lodash'; import { check } from 'meteor/check'; import LocalSettings from '/imports/api/local-settings'; import setChangedLocalSettings from '../modifiers/setChangedLocalSettings'; import { extractCredentials } from '/imports/api/common/server/helpers'; import Logger from '/imports/startup/server/logger'; +import { isEqual } from 'radash'; export default async function userChangedLocalSettings(settings) { try { @@ -21,7 +21,7 @@ export default async function userChangedLocalSettings(settings) { fields: { settings: 1 }, }); - if (!userLocalSettings || !_.isEqual(userLocalSettings.settings, settings)) { + if (!userLocalSettings || !isEqual(userLocalSettings.settings, settings)) { await setChangedLocalSettings(meetingId, requesterUserId, settings); } } catch (err) { diff --git a/bigbluebutton-html5/imports/api/meetings/server/methods/changeLayout.js b/bigbluebutton-html5/imports/api/meetings/server/methods/changeLayout.js index 99903dd46f7d..93ce667eef2e 100644 --- a/bigbluebutton-html5/imports/api/meetings/server/methods/changeLayout.js +++ b/bigbluebutton-html5/imports/api/meetings/server/methods/changeLayout.js @@ -42,7 +42,7 @@ export default async function changeLayout(payload) { pushLayout: Boolean, presentationIsOpen: Boolean, isResizing: Boolean, - cameraPosition: String, + cameraPosition: Match.Maybe(String), focusedCamera: String, presentationVideoRate: Number, }); diff --git a/bigbluebutton-html5/imports/api/meetings/server/methods/setPushLayout.js b/bigbluebutton-html5/imports/api/meetings/server/methods/setPushLayout.js index ea5ee4197254..dc27f8ba3be5 100644 --- a/bigbluebutton-html5/imports/api/meetings/server/methods/setPushLayout.js +++ b/bigbluebutton-html5/imports/api/meetings/server/methods/setPushLayout.js @@ -15,7 +15,7 @@ export default function setPushLayout(payload) { check(requesterUserId, String); check(payload, { - pushLayout: Boolean, + pushLayout: Match.Maybe(Boolean), }); RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); diff --git a/bigbluebutton-html5/imports/api/meetings/server/methods/toggleLockSettings.js b/bigbluebutton-html5/imports/api/meetings/server/methods/toggleLockSettings.js index ab807b54aad4..60be14fe689d 100755 --- a/bigbluebutton-html5/imports/api/meetings/server/methods/toggleLockSettings.js +++ b/bigbluebutton-html5/imports/api/meetings/server/methods/toggleLockSettings.js @@ -24,6 +24,7 @@ export default function toggleLockSettings(lockSettingsProps) { lockOnJoin: Boolean, lockOnJoinConfigurable: Boolean, hideViewersCursor: Boolean, + hideViewersAnnotation: Boolean, setBy: Match.Maybe(String), }); @@ -37,6 +38,7 @@ export default function toggleLockSettings(lockSettingsProps) { lockOnJoin, lockOnJoinConfigurable, hideViewersCursor, + hideViewersAnnotation, } = lockSettingsProps; const payload = { @@ -49,6 +51,7 @@ export default function toggleLockSettings(lockSettingsProps) { lockOnJoin, lockOnJoinConfigurable, hideViewersCursor, + hideViewersAnnotation, setBy: requesterUserId, }; diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js index 8da43223a558..f9c6f62d3965 100755 --- a/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js +++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js @@ -11,10 +11,12 @@ import Meetings, { } from '/imports/api/meetings'; import Logger from '/imports/startup/server/logger'; import { initPads } from '/imports/api/pads/server/helpers'; +import createTimer from '/imports/api/timer/server/methods/createTimer'; import { initCaptions } from '/imports/api/captions/server/helpers'; import { addAnnotationsStreamer } from '/imports/api/annotations/server/streamer'; import { addCursorStreamer } from '/imports/api/cursor/server/streamer'; import { addExternalVideoStreamer } from '/imports/api/external-videos/server/streamer'; +import addUserReactionsObserver from '/imports/api/user-reaction/server/helpers'; import { LAYOUT_TYPE } from '/imports/ui/components/layout/enums'; const addExternalVideo = async (meetingId) => { @@ -147,6 +149,7 @@ export default async function addMeeting(meeting) { lockOnJoin: Boolean, lockOnJoinConfigurable: Boolean, hideViewersCursor: Boolean, + hideViewersAnnotation: Boolean, }, systemProps: { html5InstanceId: Number, @@ -262,12 +265,17 @@ export default async function addMeeting(meeting) { if (insertedId) { Logger.info(`Added meeting id=${meetingId}`); + // Init Timer collection + createTimer(meetingId); if (newMeeting.meetingProp.disabledFeatures.indexOf('sharedNotes') === -1) { initPads(meetingId); } if (newMeeting.meetingProp.disabledFeatures.indexOf('captions') === -1) { await initCaptions(meetingId); } + if (newMeeting.meetingProp.disabledFeatures.indexOf('reactions') === -1) { + await addUserReactionsObserver(meetingId); + } } else if (numberAffected) { Logger.info(`Upserted meeting id=${meetingId}`); } diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/changeLockSettings.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/changeLockSettings.js index fa4132d49564..1b1e084b4a8d 100755 --- a/bigbluebutton-html5/imports/api/meetings/server/modifiers/changeLockSettings.js +++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/changeLockSettings.js @@ -14,6 +14,7 @@ export default async function changeLockSettings(meetingId, payload) { lockOnJoin: Boolean, lockOnJoinConfigurable: Boolean, hideViewersCursor: Boolean, + hideViewersAnnotation: Boolean, setBy: Match.Maybe(String), }); @@ -27,6 +28,7 @@ export default async function changeLockSettings(meetingId, payload) { lockOnJoin, lockOnJoinConfigurable, hideViewersCursor, + hideViewersAnnotation, setBy, } = payload; @@ -46,14 +48,14 @@ export default async function changeLockSettings(meetingId, payload) { lockOnJoin, lockOnJoinConfigurable, hideViewersCursor, + hideViewersAnnotation, setBy, }, }, }; try { - const { numberAffected } = Meetings.upsertAsync(selector, modifier); - + const { numberAffected } = await Meetings.upsertAsync(selector, modifier); if (numberAffected) { Logger.info(`Changed meeting={${meetingId}} updated lock settings`); } else { diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/meetingHasEnded.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/meetingHasEnded.js index e851e3036d2f..0ecbff86866d 100755 --- a/bigbluebutton-html5/imports/api/meetings/server/modifiers/meetingHasEnded.js +++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/meetingHasEnded.js @@ -20,6 +20,7 @@ import clearVoiceUsers from '/imports/api/voice-users/server/modifiers/clearVoic import clearUserInfo from '/imports/api/users-infos/server/modifiers/clearUserInfo'; import clearConnectionStatus from '/imports/api/connection-status/server/modifiers/clearConnectionStatus'; import clearScreenshare from '/imports/api/screenshare/server/modifiers/clearScreenshare'; +import clearTimer from '/imports/api/timer/server/modifiers/clearTimer'; import clearAudioCaptions from '/imports/api/audio-captions/server/modifiers/clearAudioCaptions'; import clearMeetingTimeRemaining from '/imports/api/meetings/server/modifiers/clearMeetingTimeRemaining'; import clearLocalSettings from '/imports/api/local-settings/server/modifiers/clearLocalSettings'; @@ -29,6 +30,7 @@ import clearVoiceCallStates from '/imports/api/voice-call-states/server/modifier import clearVideoStreams from '/imports/api/video-streams/server/modifiers/clearVideoStreams'; import clearAuthTokenValidation from '/imports/api/auth-token-validation/server/modifiers/clearAuthTokenValidation'; import clearUsersPersistentData from '/imports/api/users-persistent-data/server/modifiers/clearUsersPersistentData'; +import clearReactions from '/imports/api/user-reaction/server/modifiers/clearReactions'; import clearWhiteboardMultiUser from '/imports/api/whiteboard-multi-user/server/modifiers/clearWhiteboardMultiUser'; import Metrics from '/imports/startup/server/metrics'; @@ -56,6 +58,7 @@ export default async function meetingHasEnded(meetingId) { clearVoiceUsers(meetingId), clearUserInfo(meetingId), clearConnectionStatus(meetingId), + clearTimer(meetingId), clearAudioCaptions(meetingId), clearLocalSettings(meetingId), clearMeetingTimeRemaining(meetingId), @@ -67,6 +70,7 @@ export default async function meetingHasEnded(meetingId) { clearWhiteboardMultiUser(meetingId), clearScreenshare(meetingId), clearUsersPersistentData(meetingId), + clearReactions(meetingId), ]); await Metrics.removeMeeting(meetingId); return Logger.info(`Cleared Meetings with id ${meetingId}`); diff --git a/bigbluebutton-html5/imports/api/presentations/server/eventHandlers.js b/bigbluebutton-html5/imports/api/presentations/server/eventHandlers.js index f85485e30203..b146c8af0207 100644 --- a/bigbluebutton-html5/imports/api/presentations/server/eventHandlers.js +++ b/bigbluebutton-html5/imports/api/presentations/server/eventHandlers.js @@ -18,5 +18,5 @@ RedisPubSub.on('PresentationConversionCompletedEvtMsg', handlePresentationAdded) RedisPubSub.on('RemovePresentationEvtMsg', handlePresentationRemove); RedisPubSub.on('SetCurrentPresentationEvtMsg', handlePresentationCurrentSet); RedisPubSub.on('SetPresentationDownloadableEvtMsg', handlePresentationDownloadableSet); -RedisPubSub.on('NewPresAnnFileAvailableEvtMsg', handlePresentationExport); +RedisPubSub.on('NewPresFileAvailableEvtMsg', handlePresentationExport); RedisPubSub.on('PresAnnStatusEvtMsg', handlePresentationExportToastUpdate); diff --git a/bigbluebutton-html5/imports/api/presentations/server/handlers/presentationDownloadableSet.js b/bigbluebutton-html5/imports/api/presentations/server/handlers/presentationDownloadableSet.js index a8ca39d8a8ac..67884b9e24ed 100644 --- a/bigbluebutton-html5/imports/api/presentations/server/handlers/presentationDownloadableSet.js +++ b/bigbluebutton-html5/imports/api/presentations/server/handlers/presentationDownloadableSet.js @@ -4,13 +4,17 @@ import setPresentationDownloadable from '../modifiers/setPresentationDownloadabl export default async function handlePresentationDownloadableSet({ body }, meetingId) { check(body, Object); - const { presentationId, podId, downloadable } = body; + const { + presentationId, podId, downloadable, downloadableExtension, + } = body; check(meetingId, String); check(presentationId, String); check(podId, String); check(downloadable, Boolean); + check(downloadableExtension, String); - const result = await setPresentationDownloadable(meetingId, podId, presentationId, downloadable); + const result = await setPresentationDownloadable(meetingId, podId, presentationId, downloadable, + downloadableExtension); return result; } diff --git a/bigbluebutton-html5/imports/api/presentations/server/handlers/presentationExport.js b/bigbluebutton-html5/imports/api/presentations/server/handlers/presentationExport.js index fd9a6ed29160..985276c0bc12 100644 --- a/bigbluebutton-html5/imports/api/presentations/server/handlers/presentationExport.js +++ b/bigbluebutton-html5/imports/api/presentations/server/handlers/presentationExport.js @@ -1,16 +1,42 @@ import { check } from 'meteor/check'; import sendExportedPresentationChatMsg from '/imports/api/presentations/server/handlers/sendExportedPresentationChatMsg'; import setPresentationExporting from '/imports/api/presentations/server/modifiers/setPresentationExporting'; +import setOriginalUriDownload from '/imports/api/presentations/server/modifiers/setOriginalUriDownload'; export default async function handlePresentationExport({ body }, meetingId) { check(body, Object); check(meetingId, String); - const { fileURI, presId } = body; + const { + annotatedFileURI, + originalFileURI, + convertedFileURI, + presId, + fileStateType, + } = body; - check(fileURI, String); + check(annotatedFileURI, String); + check(originalFileURI, String); + check(convertedFileURI, String); check(presId, String); + check(fileStateType, String); - await sendExportedPresentationChatMsg(meetingId, presId, fileURI); + if (fileStateType === 'Original' || fileStateType === 'Converted') { + if (fileStateType === 'Converted') { + await setOriginalUriDownload( + meetingId, + presId, + convertedFileURI, + ); + } else { + await setOriginalUriDownload( + meetingId, + presId, + originalFileURI, + ); + } + } else { + await sendExportedPresentationChatMsg(meetingId, presId, annotatedFileURI, fileStateType); + } await setPresentationExporting(meetingId, presId, { status: 'EXPORTED' }); } diff --git a/bigbluebutton-html5/imports/api/presentations/server/handlers/sendExportedPresentationChatMsg.js b/bigbluebutton-html5/imports/api/presentations/server/handlers/sendExportedPresentationChatMsg.js index 8297b34201e5..e3ff491bd370 100644 --- a/bigbluebutton-html5/imports/api/presentations/server/handlers/sendExportedPresentationChatMsg.js +++ b/bigbluebutton-html5/imports/api/presentations/server/handlers/sendExportedPresentationChatMsg.js @@ -3,12 +3,13 @@ import Presentations from '/imports/api/presentations'; const DEFAULT_FILENAME = 'annotated_slides.pdf'; -export default async function sendExportedPresentationChatMsg(meetingId, presentationId, fileURI) { +export default async function sendExportedPresentationChatMsg(meetingId, + presentationId, fileURI, fileStateType) { const CHAT_CONFIG = Meteor.settings.public.chat; const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id; const PUBLIC_CHAT_SYSTEM_ID = CHAT_CONFIG.system_userid; - const CHAT_EXPORTED_PRESENTATION_MESSAGE = CHAT_CONFIG.system_messages_keys - .chat_exported_presentation; + const CHAT_EXPORTED_PRESENTATION_MESSAGE = CHAT_CONFIG + .system_messages_keys.chat_exported_presentation; const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system; const pres = await Presentations.findOneAsync({ meetingId, id: presentationId }); @@ -17,6 +18,7 @@ export default async function sendExportedPresentationChatMsg(meetingId, present type: 'presentation', fileURI, filename: pres?.name || DEFAULT_FILENAME, + fileStateType, }; const payload = { diff --git a/bigbluebutton-html5/imports/api/presentations/server/methods.js b/bigbluebutton-html5/imports/api/presentations/server/methods.js index 803c4175415a..a99dbf405296 100644 --- a/bigbluebutton-html5/imports/api/presentations/server/methods.js +++ b/bigbluebutton-html5/imports/api/presentations/server/methods.js @@ -3,12 +3,12 @@ import removePresentation from './methods/removePresentation'; import setPresentationRenderedInToast from './methods/setPresentationRenderedInToast'; import setPresentation from './methods/setPresentation'; import setPresentationDownloadable from './methods/setPresentationDownloadable'; -import exportPresentationToChat from './methods/exportPresentationToChat'; +import exportPresentation from './methods/exportPresentation'; Meteor.methods({ removePresentation, setPresentation, setPresentationDownloadable, - exportPresentationToChat, + exportPresentation, setPresentationRenderedInToast, }); diff --git a/bigbluebutton-html5/imports/api/presentations/server/methods/exportPresentationToChat.js b/bigbluebutton-html5/imports/api/presentations/server/methods/exportPresentation.js similarity index 86% rename from bigbluebutton-html5/imports/api/presentations/server/methods/exportPresentationToChat.js rename to bigbluebutton-html5/imports/api/presentations/server/methods/exportPresentation.js index 00fe93f3b8f2..70d5d458be7c 100644 --- a/bigbluebutton-html5/imports/api/presentations/server/methods/exportPresentationToChat.js +++ b/bigbluebutton-html5/imports/api/presentations/server/methods/exportPresentation.js @@ -7,10 +7,10 @@ import Presentations from '/imports/api/presentations'; const EXPORTING_THRESHOLD_PER_SLIDE = 2500; -export default async function exportPresentationToChat(presentationId) { +export default async function exportPresentation(presentationId, fileStateType) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; - const EVENT_NAME = 'MakePresentationWithAnnotationDownloadReqMsg'; + const EVENT_NAME = 'MakePresentationDownloadReqMsg'; try { const { meetingId, requesterUserId } = extractCredentials(this.userId); @@ -22,6 +22,7 @@ export default async function exportPresentationToChat(presentationId) { const payload = { presId: presentationId, allPages: true, + fileStateType, pages: [], }; @@ -30,7 +31,8 @@ export default async function exportPresentationToChat(presentationId) { const selector = { meetingId, id: presentationId }; const cursor = Presentations.find(selector); - const numPages = await cursor.fetchAsync()[0]?.pages?.length ?? 1; + const pres = await Presentations.findOneAsync(selector); + const numPages = pres?.pages?.length ?? 1; const threshold = EXPORTING_THRESHOLD_PER_SLIDE * numPages; const observer = cursor.observe({ @@ -54,6 +56,6 @@ export default async function exportPresentationToChat(presentationId) { RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); } catch (err) { - Logger.error(`Exception while invoking method exportPresentationToChat ${err.stack}`); + Logger.error(`Exception while invoking method exportPresentation ${err.stack}`); } } diff --git a/bigbluebutton-html5/imports/api/presentations/server/methods/setPresentationDownloadable.js b/bigbluebutton-html5/imports/api/presentations/server/methods/setPresentationDownloadable.js index 4ccf1e063a49..1ad147ca9ca5 100644 --- a/bigbluebutton-html5/imports/api/presentations/server/methods/setPresentationDownloadable.js +++ b/bigbluebutton-html5/imports/api/presentations/server/methods/setPresentationDownloadable.js @@ -3,7 +3,7 @@ import { check } from 'meteor/check'; import { extractCredentials } from '/imports/api/common/server/helpers'; import Logger from '/imports/startup/server/logger'; -export default function setPresentationDownloadable(presentationId, downloadable) { +export default function setPresentationDownloadable(presentationId, downloadable, fileStateType) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'SetPresentationDownloadablePubMsg'; @@ -15,11 +15,13 @@ export default function setPresentationDownloadable(presentationId, downloadable check(requesterUserId, String); check(downloadable, Match.Maybe(Boolean)); check(presentationId, String); + check(fileStateType, Match.Maybe(String)); const payload = { presentationId, podId: 'DEFAULT_PRESENTATION_POD', downloadable, + fileStateType, }; RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); diff --git a/bigbluebutton-html5/imports/api/presentations/server/modifiers/addPresentation.js b/bigbluebutton-html5/imports/api/presentations/server/modifiers/addPresentation.js index 87f6eedd94ca..06819c9c291b 100755 --- a/bigbluebutton-html5/imports/api/presentations/server/modifiers/addPresentation.js +++ b/bigbluebutton-html5/imports/api/presentations/server/modifiers/addPresentation.js @@ -52,6 +52,7 @@ export default async function addPresentation(meetingId, podId, presentation) { downloadable: Boolean, removable: Boolean, isInitialPresentation: Boolean, + filenameConverted: String, }); const selector = { @@ -71,7 +72,7 @@ export default async function addPresentation(meetingId, podId, presentation) { }; try { - const { insertedId } = await Presentations.upsertAsync(selector, modifier); + await Presentations.upsertAsync(selector, modifier); await addSlides(meetingId, podId, presentation.id, presentation.pages); diff --git a/bigbluebutton-html5/imports/api/presentations/server/modifiers/setOriginalUriDownload.js b/bigbluebutton-html5/imports/api/presentations/server/modifiers/setOriginalUriDownload.js new file mode 100644 index 000000000000..d00f1ddb45e7 --- /dev/null +++ b/bigbluebutton-html5/imports/api/presentations/server/modifiers/setOriginalUriDownload.js @@ -0,0 +1,30 @@ +import { check } from 'meteor/check'; +import Presentations from '/imports/api/presentations'; +import Logger from '/imports/startup/server/logger'; + +export default async function setOriginalUriDownload(meetingId, presentationId, fileURI) { + check(meetingId, String); + check(presentationId, String); + check(fileURI, String); + + const selector = { + meetingId, + id: presentationId, + }; + + const modifier = { + $set: { + originalFileURI: fileURI, + }, + }; + + try { + const { numberAffected } = await Presentations.upsertAsync(selector, modifier); + + if (numberAffected) { + Logger.info(`Set URI for file ${presentationId} in meeting ${meetingId} URI=${fileURI}`); + } + } catch (err) { + Logger.error(`Could not set URI for file ${presentationId} in meeting ${meetingId} ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/presentations/server/modifiers/setPresentationDownloadable.js b/bigbluebutton-html5/imports/api/presentations/server/modifiers/setPresentationDownloadable.js index dd15bd782c4e..a7623fc0fd1b 100644 --- a/bigbluebutton-html5/imports/api/presentations/server/modifiers/setPresentationDownloadable.js +++ b/bigbluebutton-html5/imports/api/presentations/server/modifiers/setPresentationDownloadable.js @@ -3,11 +3,12 @@ import Presentations from '/imports/api/presentations'; import Logger from '/imports/startup/server/logger'; export default async function setPresentationDownloadable(meetingId, podId, - presentationId, downloadable) { + presentationId, downloadable, extensionToBeDownloadable) { check(meetingId, String); check(presentationId, String); check(podId, String); check(downloadable, Boolean); + check(extensionToBeDownloadable, String); const selector = { meetingId, @@ -15,9 +16,14 @@ export default async function setPresentationDownloadable(meetingId, podId, id: presentationId, }; + let downloadableExtension = extensionToBeDownloadable; + if (!downloadable) { + downloadableExtension = ''; + } const modifier = { $set: { downloadable, + downloadableExtension, }, }; diff --git a/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js index fdc9e1cd438f..71d1aaa32d54 100755 --- a/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js +++ b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js @@ -2,7 +2,7 @@ import Auth from '/imports/ui/services/auth'; import logger from '/imports/startup/client/logger'; import BridgeService from './service'; import ScreenshareBroker from '/imports/ui/services/bbb-webrtc-sfu/screenshare-broker'; -import { setSharingScreen, screenShareEndAlert } from '/imports/ui/components/screenshare/service'; +import { setIsSharing, screenShareEndAlert, setOutputDeviceId } from '/imports/ui/components/screenshare/service'; import { SCREENSHARING_ERRORS } from './errors'; import { shouldForceRelay } from '/imports/ui/services/bbb-webrtc-sfu/utils'; import MediaStreamUtils from '/imports/utils/media-stream-utils'; @@ -54,6 +54,7 @@ export default class KurentoScreenshareBridge { this.reconnectionTimeout; this.restartIntervalMs = BridgeService.BASE_MEDIA_TIMEOUT; this.startedOnce = false; + this.outputDeviceId = null; } get gdmStream() { @@ -200,6 +201,11 @@ export default class KurentoScreenshareBridge { if (mediaElement && this.broker && this.broker.webRtcPeer) { const stream = this.broker.webRtcPeer.getRemoteStream(); + + if (this.hasAudio && this.outputDeviceId && typeof this.outputDeviceId === 'string') { + setOutputDeviceId(this.outputDeviceId); + } + BridgeService.screenshareLoadAndPlayMediaStream(stream, mediaElement, !this.broker.hasAudio); } @@ -245,14 +251,18 @@ export default class KurentoScreenshareBridge { return error; } - async view(hasAudio = false) { - this.hasAudio = hasAudio; + async view(options = { + hasAudio: false, + outputDeviceId: null, + }) { + this.hasAudio = options.hasAudio; + this.outputDeviceId = options.outputDeviceId; this.role = RECV_ROLE; const iceServers = await BridgeService.getIceServers(Auth.sessionToken); - const options = { + const brokerOptions = { iceServers, userName: Auth.fullname, - hasAudio, + hasAudio: options.hasAudio, offering: OFFERING, mediaServer: BridgeService.getMediaServerAdapter(), signalCandidates: SIGNAL_CANDIDATES, @@ -267,7 +277,7 @@ export default class KurentoScreenshareBridge { Auth.userID, Auth.meetingID, this.role, - options, + brokerOptions, ); this.broker.onstart = this.handleViewerStart.bind(this); @@ -292,7 +302,7 @@ export default class KurentoScreenshareBridge { screenShareEndAlert(); } - share(stream, onFailure) { + share(stream, onFailure, contentType) { return new Promise(async (resolve, reject) => { this.onerror = onFailure; this.connectionAttempts += 1; @@ -322,6 +332,7 @@ export default class KurentoScreenshareBridge { userName: Auth.fullname, stream, hasAudio: this.hasAudio, + contentType: contentType, bitrate: BridgeService.BASE_BITRATE, offering: true, mediaServer: BridgeService.getMediaServerAdapter(), @@ -365,7 +376,7 @@ export default class KurentoScreenshareBridge { // component tracker to be extra sure we won't have any client-side state // inconsistency - prlanzarin if (this.broker && this.broker.role === SEND_ROLE && !this.reconnecting) { - setSharingScreen(false); + setIsSharing(false); } this.broker = null; } @@ -388,5 +399,7 @@ export default class KurentoScreenshareBridge { MediaStreamUtils.stopMediaStreamTracks(this.gdmStream); this.gdmStream = null; } + + this.outputDeviceId = null; } } diff --git a/bigbluebutton-html5/imports/api/timer/index.js b/bigbluebutton-html5/imports/api/timer/index.js new file mode 100644 index 000000000000..c43d8e33c477 --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/index.js @@ -0,0 +1,9 @@ +import { Meteor } from 'meteor/meteor'; + +const Timer = new Mongo.Collection('timer'); + +if (Meteor.isServer) { + Timer.createIndex({ meetingId: 1 }); +} + +export default Timer; diff --git a/bigbluebutton-html5/imports/api/timer/server/eventHandlers.js b/bigbluebutton-html5/imports/api/timer/server/eventHandlers.js new file mode 100644 index 000000000000..48d442ffa65f --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/eventHandlers.js @@ -0,0 +1,20 @@ +import RedisPubSub from '/imports/startup/server/redis'; +import handleTimerActivated from './handlers/timerActivated'; +import handleTimerDeactivated from './handlers/timerDeactivated'; +import handleTimerStarted from './handlers/timerStarted'; +import handleTimerStopped from './handlers/timerStopped'; +import handleTimerSwitched from './handlers/timerSwitched'; +import handleTimerSet from './handlers/timerSet'; +import handleTimerReset from './handlers/timerReset'; +import handleTimerEnded from './handlers/timerEnded'; +import handleTrackSet from './handlers/trackSet'; + +RedisPubSub.on('ActivateTimerRespMsg', handleTimerActivated); +RedisPubSub.on('DeactivateTimerRespMsg', handleTimerDeactivated); +RedisPubSub.on('StartTimerRespMsg', handleTimerStarted); +RedisPubSub.on('StopTimerRespMsg', handleTimerStopped); +RedisPubSub.on('SwitchTimerRespMsg', handleTimerSwitched); +RedisPubSub.on('SetTimerRespMsg', handleTimerSet); +RedisPubSub.on('ResetTimerRespMsg', handleTimerReset); +RedisPubSub.on('TimerEndedEvtMsg', handleTimerEnded); +RedisPubSub.on('SetTrackRespMsg', handleTrackSet); diff --git a/bigbluebutton-html5/imports/api/timer/server/handlers/timerActivated.js b/bigbluebutton-html5/imports/api/timer/server/handlers/timerActivated.js new file mode 100644 index 000000000000..fdeaccbafe36 --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/handlers/timerActivated.js @@ -0,0 +1,14 @@ +import { check } from 'meteor/check'; +import updateTimer from '/imports/api/timer/server/modifiers/updateTimer'; + +export default function handleTimerActivated({ body }, meetingId) { + const { userId } = body; + check(meetingId, String); + check(userId, String); + + updateTimer({ + action: 'activate', + meetingId, + requesterUserId: userId, + }); +} diff --git a/bigbluebutton-html5/imports/api/timer/server/handlers/timerDeactivated.js b/bigbluebutton-html5/imports/api/timer/server/handlers/timerDeactivated.js new file mode 100644 index 000000000000..61a7ae45d209 --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/handlers/timerDeactivated.js @@ -0,0 +1,14 @@ +import { check } from 'meteor/check'; +import updateTimer from '/imports/api/timer/server/modifiers/updateTimer'; + +export default function handleTimerDeactivated({ body }, meetingId) { + const { userId } = body; + check(meetingId, String); + check(userId, String); + + updateTimer({ + action: 'deactivate', + meetingId, + requesterUserId: userId, + }); +} diff --git a/bigbluebutton-html5/imports/api/timer/server/handlers/timerEnded.js b/bigbluebutton-html5/imports/api/timer/server/handlers/timerEnded.js new file mode 100644 index 000000000000..410b3eb561c5 --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/handlers/timerEnded.js @@ -0,0 +1,13 @@ +import { check } from 'meteor/check'; +import updateTimer from '/imports/api/timer/server/modifiers/updateTimer'; + +export default function handleTimerEnded({ body }, meetingId) { + check(meetingId, String); + check(body, Object); + + updateTimer({ + action: 'reset', + meetingId, + requesterUserId: 'nodeJSapp', + }); +} diff --git a/bigbluebutton-html5/imports/api/timer/server/handlers/timerReset.js b/bigbluebutton-html5/imports/api/timer/server/handlers/timerReset.js new file mode 100644 index 000000000000..f44b69f2043b --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/handlers/timerReset.js @@ -0,0 +1,14 @@ +import { check } from 'meteor/check'; +import updateTimer from '/imports/api/timer/server/modifiers/updateTimer'; + +export default function handleTimerReset({ body }, meetingId) { + const { userId } = body; + check(meetingId, String); + check(userId, String); + + updateTimer({ + action: 'reset', + meetingId, + requesterUserId: userId, + }); +} diff --git a/bigbluebutton-html5/imports/api/timer/server/handlers/timerSet.js b/bigbluebutton-html5/imports/api/timer/server/handlers/timerSet.js new file mode 100644 index 000000000000..0ae328276969 --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/handlers/timerSet.js @@ -0,0 +1,17 @@ +import { check } from 'meteor/check'; +import updateTimer from '/imports/api/timer/server/modifiers/updateTimer'; + +export default function handleTimerSet({ body }, meetingId) { + const { userId, time } = body; + + check(meetingId, String); + check(userId, String); + check(time, Number); + + updateTimer({ + action: 'set', + meetingId, + requesterUserId: userId, + time, + }); +} diff --git a/bigbluebutton-html5/imports/api/timer/server/handlers/timerStarted.js b/bigbluebutton-html5/imports/api/timer/server/handlers/timerStarted.js new file mode 100644 index 000000000000..2400f46ad1a8 --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/handlers/timerStarted.js @@ -0,0 +1,14 @@ +import { check } from 'meteor/check'; +import updateTimer from '/imports/api/timer/server/modifiers/updateTimer'; + +export default function handleTimerStarted({ body }, meetingId) { + const { userId } = body; + check(meetingId, String); + check(userId, String); + + updateTimer({ + action: 'start', + meetingId, + requesterUserId: userId, + }); +} diff --git a/bigbluebutton-html5/imports/api/timer/server/handlers/timerStopped.js b/bigbluebutton-html5/imports/api/timer/server/handlers/timerStopped.js new file mode 100644 index 000000000000..cb4023e5bfed --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/handlers/timerStopped.js @@ -0,0 +1,17 @@ +import { check } from 'meteor/check'; +import updateTimer from '/imports/api/timer/server/modifiers/updateTimer'; + +export default function handleTimerStopped({ body }, meetingId) { + const { userId, accumulated } = body; + + check(meetingId, String); + check(userId, String); + check(accumulated, Number); + + updateTimer({ + action: 'stop', + meetingId, + requesterUserId: userId, + accumulated, + }); +} diff --git a/bigbluebutton-html5/imports/api/timer/server/handlers/timerSwitched.js b/bigbluebutton-html5/imports/api/timer/server/handlers/timerSwitched.js new file mode 100644 index 000000000000..38dc03d26f9e --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/handlers/timerSwitched.js @@ -0,0 +1,17 @@ +import { check } from 'meteor/check'; +import updateTimer from '/imports/api/timer/server/modifiers/updateTimer'; + +export default function handleTimerSwitched({ body }, meetingId) { + const { userId, stopwatch } = body; + + check(meetingId, String); + check(userId, String); + check(stopwatch, Boolean); + + updateTimer({ + action: 'switch', + meetingId, + requesterUserId: userId, + stopwatch, + }); +} diff --git a/bigbluebutton-html5/imports/api/timer/server/handlers/trackSet.js b/bigbluebutton-html5/imports/api/timer/server/handlers/trackSet.js new file mode 100644 index 000000000000..570cffb3c6d0 --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/handlers/trackSet.js @@ -0,0 +1,18 @@ +import { check } from 'meteor/check'; +import updateTimer from '/imports/api/timer/server/modifiers/updateTimer'; + +export default function handleTrackSet({ body }, meetingId) { + const { userId, track } = body; + + check(meetingId, String); + check(userId, String); + check(track, String); + + updateTimer({ + action: 'track', + meetingId, + requesterUserId: userId, + stopwatch: false, + track, + }); +} diff --git a/bigbluebutton-html5/imports/api/timer/server/helpers.js b/bigbluebutton-html5/imports/api/timer/server/helpers.js new file mode 100644 index 000000000000..88494d6ae114 --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/helpers.js @@ -0,0 +1,40 @@ +import { Meteor } from 'meteor/meteor'; + +const TIMER_CONFIG = Meteor.settings.public.timer; + +const MILLI_IN_MINUTE = 60000; + +const TRACKS = [ + 'noTrack', + 'track1', + 'track2', + 'track3', +]; + +const isEnabled = () => TIMER_CONFIG.enabled; + +const getDefaultTime = () => TIMER_CONFIG.time * MILLI_IN_MINUTE; + +const getInitialState = () => { + const time = getDefaultTime(); + check(time, Number); + + return { + stopwatch: true, + running: false, + time, + accumulated: 0, + timestamp: 0, + track: TRACKS[0], + }; +}; + +const isTrackValid = (track) => TRACKS.includes(track); + +export { + TRACKS, + isEnabled, + getDefaultTime, + getInitialState, + isTrackValid, +}; diff --git a/bigbluebutton-html5/imports/api/timer/server/index.js b/bigbluebutton-html5/imports/api/timer/server/index.js new file mode 100644 index 000000000000..f57b4620df10 --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/index.js @@ -0,0 +1,3 @@ +import './methods'; +import './eventHandlers'; +import './publishers'; diff --git a/bigbluebutton-html5/imports/api/timer/server/methods.js b/bigbluebutton-html5/imports/api/timer/server/methods.js new file mode 100644 index 000000000000..85be11faa2c0 --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/methods.js @@ -0,0 +1,24 @@ +import { Meteor } from 'meteor/meteor'; +import activateTimer from './methods/activateTimer'; +import deactivateTimer from './methods/deactivateTimer'; +import resetTimer from './methods/resetTimer'; +import startTimer from './methods/startTimer'; +import stopTimer from './methods/stopTimer'; +import switchTimer from './methods/switchTimer'; +import setTimer from './methods/setTimer'; +import getServerTime from './methods/getServerTime'; +import setTrack from './methods/setTrack'; +import timerEnded from './methods/endTimer'; + +Meteor.methods({ + activateTimer, + deactivateTimer, + resetTimer, + startTimer, + stopTimer, + switchTimer, + setTimer, + getServerTime, + setTrack, + timerEnded, +}); diff --git a/bigbluebutton-html5/imports/api/timer/server/methods/activateTimer.js b/bigbluebutton-html5/imports/api/timer/server/methods/activateTimer.js new file mode 100644 index 000000000000..41cb168caa82 --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/methods/activateTimer.js @@ -0,0 +1,23 @@ +import { check } from 'meteor/check'; +import RedisPubSub from '/imports/startup/server/redis'; +import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; +import { getInitialState } from '../helpers'; + +export default function activateTimer() { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'ActivateTimerReqMsg'; + + try { + const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + + const payload = getInitialState(); + + RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); + } catch (err) { + Logger.error(`Activating timer: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/timer/server/methods/createTimer.js b/bigbluebutton-html5/imports/api/timer/server/methods/createTimer.js new file mode 100644 index 000000000000..262aedcfd220 --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/methods/createTimer.js @@ -0,0 +1,29 @@ +import { check } from 'meteor/check'; +import { getInitialState, isEnabled } from '/imports/api/timer/server/helpers'; +import addTimer from '/imports/api/timer/server/modifiers/addTimer'; +import RedisPubSub from '/imports/startup/server/redis'; +import Logger from '/imports/startup/server/logger'; + +// This method should only be used by the server +export default function createTimer(meetingId) { + check(meetingId, String); + + // Avoid timer creation if this feature is disabled + if (!isEnabled()) { + Logger.warn(`Timers are disabled for ${meetingId}`); + return; + } + + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'CreateTimerPubMsg'; + + try { + addTimer(meetingId); + const payload = getInitialState(); + + RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, 'nodeJsApp', payload); + } catch (err) { + Logger.error(`Activating timer: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/timer/server/methods/deactivateTimer.js b/bigbluebutton-html5/imports/api/timer/server/methods/deactivateTimer.js new file mode 100644 index 000000000000..5cbd4a337264 --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/methods/deactivateTimer.js @@ -0,0 +1,20 @@ +import { check } from 'meteor/check'; +import RedisPubSub from '/imports/startup/server/redis'; +import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; + +export default function deactivateTimer() { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'DeactivateTimerReqMsg'; + + try { + const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + + RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, {}); + } catch (err) { + Logger.error(`Deactivating timer: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/timer/server/methods/endTimer.js b/bigbluebutton-html5/imports/api/timer/server/methods/endTimer.js new file mode 100644 index 000000000000..835f2425ff18 --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/methods/endTimer.js @@ -0,0 +1,33 @@ +import { check } from 'meteor/check'; +import RedisPubSub from '/imports/startup/server/redis'; +import Logger from '/imports/startup/server/logger'; +import updateTimer from '/imports/api/timer/server/modifiers/updateTimer'; +import { extractCredentials } from '/imports/api/common/server/helpers'; + +export default function timerEnded() { + const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + + updateTimer({ + action: 'ended', + meetingId, + requesterUserId, + }); +} + +// This method should only be used by the server +export function sysEndTimer(meetingId) { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'TimerEndedPubMsg'; + const USER_ID = 'nodeJSapp'; + + try { + check(meetingId, String); + + RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, USER_ID, {}); + } catch (err) { + Logger.error(`Ending timer: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/timer/server/methods/getServerTime.js b/bigbluebutton-html5/imports/api/timer/server/methods/getServerTime.js new file mode 100644 index 000000000000..6ff652f16c6e --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/methods/getServerTime.js @@ -0,0 +1,5 @@ +export default function getServerTime() { + if (this.userId) return Date.now(); + + return 0; +} diff --git a/bigbluebutton-html5/imports/api/timer/server/methods/resetTimer.js b/bigbluebutton-html5/imports/api/timer/server/methods/resetTimer.js new file mode 100644 index 000000000000..4d534f2cee4a --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/methods/resetTimer.js @@ -0,0 +1,20 @@ +import { check } from 'meteor/check'; +import RedisPubSub from '/imports/startup/server/redis'; +import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; + +export default function resetTimer() { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'ResetTimerReqMsg'; + + try { + const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + + RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, {}); + } catch (err) { + Logger.error(`Resetting timer: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/timer/server/methods/setTimer.js b/bigbluebutton-html5/imports/api/timer/server/methods/setTimer.js new file mode 100644 index 000000000000..b17e7574f3c8 --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/methods/setTimer.js @@ -0,0 +1,25 @@ +import { check } from 'meteor/check'; +import RedisPubSub from '/imports/startup/server/redis'; +import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; + +export default function setTimer(time) { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'SetTimerReqMsg'; + + try { + const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + check(time, Number); + + const payload = { + time, + }; + + RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); + } catch (err) { + Logger.error(`Setting timer: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/timer/server/methods/setTrack.js b/bigbluebutton-html5/imports/api/timer/server/methods/setTrack.js new file mode 100644 index 000000000000..a63262db0f31 --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/methods/setTrack.js @@ -0,0 +1,30 @@ +import { check } from 'meteor/check'; +import RedisPubSub from '/imports/startup/server/redis'; +import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; +import { isTrackValid } from '/imports/api/timer/server/helpers'; + +export default function setTrack(track) { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'SetTrackReqMsg'; + + try { + const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + check(track, String); + + if (isTrackValid(track)) { + const payload = { + track, + }; + + RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); + } else { + Logger.warn(`User=${requesterUserId} tried to set invalid track '${track}' in meeting=${meetingId}`); + } + } catch (err) { + Logger.error(`Setting track: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/timer/server/methods/startTimer.js b/bigbluebutton-html5/imports/api/timer/server/methods/startTimer.js new file mode 100644 index 000000000000..cb372a3002d0 --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/methods/startTimer.js @@ -0,0 +1,20 @@ +import { check } from 'meteor/check'; +import RedisPubSub from '/imports/startup/server/redis'; +import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; + +export default function startTimer() { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'StartTimerReqMsg'; + + try { + const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + + RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, {}); + } catch (err) { + Logger.error(`Starting timer: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/timer/server/methods/stopTimer.js b/bigbluebutton-html5/imports/api/timer/server/methods/stopTimer.js new file mode 100644 index 000000000000..3b769f653fba --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/methods/stopTimer.js @@ -0,0 +1,92 @@ +import { check } from 'meteor/check'; +import Timer from '/imports/api/timer'; +import RedisPubSub from '/imports/startup/server/redis'; +import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; + +export default function stopTimer() { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'StopTimerReqMsg'; + + try { + const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + + const now = Date.now(); + const timer = Timer.findOne( + { meetingId }, + { + fields: + { + stopwatch: 1, + time: 1, + accumulated: 1, + timestamp: 1, + }, + }, + ); + + if (timer) { + const { + timestamp, + } = timer; + + const accumulated = timer.accumulated + (now - timestamp); + + const payload = { + accumulated, + }; + + RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); + } else { + Logger.warn(`Could not stop timer for meeting=${meetingId}, timer not found`); + } + } catch (err) { + Logger.error(`Stopping timer: ${err}`); + } +} + +// This method should only be used by the server +export function sysStopTimer(meetingId) { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'StopTimerReqMsg'; + const USER_ID = 'nodeJSapp'; + + try { + check(meetingId, String); + const now = Date.now(); + const timer = Timer.findOne( + { meetingId }, + { + fields: + { + stopwatch: 1, + time: 1, + accumulated: 1, + timestamp: 1, + }, + }, + ); + + if (timer) { + const { + timestamp, + } = timer; + + const accumulated = timer.accumulated + (now - timestamp); + + const payload = { + accumulated, + }; + + RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, USER_ID, payload); + } else { + Logger.warn(`Could not stop timer for meeting=${meetingId}, timer not found`); + } + } catch (err) { + Logger.error(`Stopping timer: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/timer/server/methods/switchTimer.js b/bigbluebutton-html5/imports/api/timer/server/methods/switchTimer.js new file mode 100644 index 000000000000..0dfbaa0d21df --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/methods/switchTimer.js @@ -0,0 +1,25 @@ +import { check } from 'meteor/check'; +import RedisPubSub from '/imports/startup/server/redis'; +import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; + +export default function switchTimer(stopwatch) { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'SwitchTimerReqMsg'; + + try { + const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(meetingId, String); + check(requesterUserId, String); + check(stopwatch, Boolean); + + const payload = { + stopwatch, + }; + + RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); + } catch (err) { + Logger.error(`Switching timer: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/timer/server/modifiers/addTimer.js b/bigbluebutton-html5/imports/api/timer/server/modifiers/addTimer.js new file mode 100644 index 000000000000..2ab47072e968 --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/modifiers/addTimer.js @@ -0,0 +1,34 @@ +import { check } from 'meteor/check'; +import Timer from '/imports/api/timer'; +import Logger from '/imports/startup/server/logger'; +import { getInitialState } from '/imports/api/timer/server/helpers'; + +// This method should only be used by the server +export default function addTimer(meetingId) { + check(meetingId, String); + + const selector = { + meetingId, + }; + + const modifier = { + meetingId, + ...getInitialState(), + active: false, + ended: 0, + }; + + const cb = (err, numChanged) => { + if (err) { + return Logger.error(`Adding timer to the collection: ${err}`); + } + + if (numChanged) { + return Logger.debug(`Added timer meeting=${meetingId}`); + } + + return Logger.debug(`Upserted timer meeting=${meetingId}`); + }; + + return Timer.upsert(selector, modifier, cb); +} diff --git a/bigbluebutton-html5/imports/api/timer/server/modifiers/clearTimer.js b/bigbluebutton-html5/imports/api/timer/server/modifiers/clearTimer.js new file mode 100644 index 000000000000..49e595c18e63 --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/modifiers/clearTimer.js @@ -0,0 +1,14 @@ +import Timer from '/imports/api/timer'; +import Logger from '/imports/startup/server/logger'; + +export default function clearTimer(meetingId) { + if (meetingId) { + return Timer.remove({ meetingId }, () => { + Logger.info(`Cleared Timer (${meetingId})`); + }); + } + + return Timer.remove({}, () => { + Logger.info('Cleared Timer (all)'); + }); +} diff --git a/bigbluebutton-html5/imports/api/timer/server/modifiers/endTimer.js b/bigbluebutton-html5/imports/api/timer/server/modifiers/endTimer.js new file mode 100644 index 000000000000..bcedfdd1c9fe --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/modifiers/endTimer.js @@ -0,0 +1,18 @@ +import { check } from 'meteor/check'; +import RedisPubSub from '/imports/startup/server/redis'; +import Logger from '/imports/startup/server/logger'; + +export default function endTimer(meetingId) { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'TimerEndedPubMsg'; + const USER_ID = 'nodeJSapp'; + + try { + check(meetingId, String); + + RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, USER_ID, {}); + } catch (err) { + Logger.error(`Ending timer: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/timer/server/modifiers/stopTimer.js b/bigbluebutton-html5/imports/api/timer/server/modifiers/stopTimer.js new file mode 100644 index 000000000000..1ed539906d70 --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/modifiers/stopTimer.js @@ -0,0 +1,46 @@ +import { check } from 'meteor/check'; +import Timer from '/imports/api/timer'; +import RedisPubSub from '/imports/startup/server/redis'; +import Logger from '/imports/startup/server/logger'; + +export default function stopTimer(meetingId) { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'StopTimerReqMsg'; + const USER_ID = 'nodeJSapp'; + + try { + check(meetingId, String); + const now = Date.now(); + const timer = Timer.findOne( + { meetingId }, + { + fields: + { + stopwatch: 1, + time: 1, + accumulated: 1, + timestamp: 1, + }, + }, + ); + + if (timer) { + const { + timestamp, + } = timer; + + const accumulated = timer.accumulated + (now - timestamp); + + const payload = { + accumulated, + }; + + RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, USER_ID, payload); + } else { + Logger.warn(`Could not stop timer for meeting=${meetingId}, timer not found`); + } + } catch (err) { + Logger.error(`Stopping timer: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/timer/server/modifiers/updateTimer.js b/bigbluebutton-html5/imports/api/timer/server/modifiers/updateTimer.js new file mode 100644 index 000000000000..1bf3b1f2e641 --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/modifiers/updateTimer.js @@ -0,0 +1,188 @@ +import { check } from 'meteor/check'; +import Timer from '/imports/api/timer'; +import Logger from '/imports/startup/server/logger'; +import Users from '/imports/api/users'; +import { TRACKS, getInitialState } from '/imports/api/timer/server/helpers'; +import { sysStopTimer } from '../methods/stopTimer'; +import { sysEndTimer } from '../methods/endTimer'; + +const getActivateModifier = () => ({ + $set: { + active: true, + ...getInitialState(), + ended: 0, + }, +}); + +const getDeactivateModifier = () => ({ + $set: { + active: false, + running: false, + ended: 0, + }, +}); + +const getResetModifier = () => ({ + $set: { + accumulated: 0, + timestamp: Date.now(), + ended: 0, + }, +}); + +const handleTimerEndedNotifications = (fields, meetingId, handle) => { + const meetingUsers = Users.find({ meetingId }).count(); + + if (fields.running === false) { + handle.stop(); + } + + if (fields.ended >= meetingUsers) { + sysStopTimer(meetingId); + sysEndTimer(meetingId); + } +}; + +const setTimerEndObserver = (meetingId) => { + const { stopwatch } = Timer.findOne({ meetingId }); + + if (stopwatch === false) { + const meetingTimer = Timer.find( + { meetingId }, + { fields: { ended: 1, running: 1 } }, + ); + const handle = meetingTimer.observeChanges({ + changed: (id, fields) => { + handleTimerEndedNotifications(fields, meetingId, handle); + }, + }); + } +}; + +const getStartModifier = () => ({ + $set: { + running: true, + timestamp: Date.now(), + ended: 0, + }, +}); + +const getStopModifier = (accumulated) => ({ + $set: { + running: false, + accumulated, + timestamp: 0, + ended: 0, + }, +}); + +const getSwitchModifier = (stopwatch) => ({ + $set: { + stopwatch, + running: false, + accumulated: 0, + timestamp: 0, + track: TRACKS[0], + ended: 0, + }, +}); + +const getSetModifier = (time) => ({ + $set: { + running: false, + accumulated: 0, + timestamp: 0, + time, + }, +}); + +const getTrackModifier = (track) => ({ + $set: { + track, + }, +}); + +const getEndedModifier = () => ({ + $inc: { + ended: 1, + }, +}); + +const logTimer = (meetingId, requesterUserId, action, stopwatch, time, track) => { + if (action === 'switch') { + Logger.info(`Timer: meetingId=${meetingId} requesterUserId=${requesterUserId} action=${action} stopwatch=${stopwatch} `); + } else if (action === 'set' && time !== 0) { + Logger.info(`Timer: meetingId=${meetingId} requesterUserId=${requesterUserId} action=${action} ${time}ms`); + } else if (action === 'track') { + Logger.info(`Timer: meetingId=${meetingId} requesterUserId=${requesterUserId} action=${action} changed to ${track}`); + } else { + Logger.info(`Timer: meetingId=${meetingId} requesterUserId=${requesterUserId} action=${action}`); + } +}; + +export default function updateTimer({ + action, + meetingId, + requesterUserId, + time = 0, + stopwatch = true, + accumulated = 0, + track = TRACKS[0], +}) { + check(action, String); + check(meetingId, String); + check(requesterUserId, String); + check(time, Number); + check(stopwatch, Boolean); + check(accumulated, Number); + check(track, String); + + const selector = { + meetingId, + }; + + let modifier; + + switch (action) { + case 'activate': + modifier = getActivateModifier(); + break; + case 'deactivate': + modifier = getDeactivateModifier(); + break; + case 'reset': + modifier = getResetModifier(); + break; + case 'start': + setTimerEndObserver(meetingId); + modifier = getStartModifier(); + break; + case 'stop': + modifier = getStopModifier(accumulated); + break; + case 'switch': + modifier = getSwitchModifier(stopwatch); + break; + case 'set': + modifier = getSetModifier(time); + break; + case 'track': + modifier = getTrackModifier(track); + break; + case 'ended': + modifier = getEndedModifier(); + break; + default: + Logger.error(`Unhandled timer action=${action}`); + } + + try { + const { numberAffected } = Timer.upsert(selector, modifier); + + if (numberAffected) { + logTimer(meetingId, requesterUserId, action, stopwatch, time, track); + } + } catch (err) { + Logger.error(`Updating timer: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/timer/server/publishers.js b/bigbluebutton-html5/imports/api/timer/server/publishers.js new file mode 100644 index 000000000000..c9535d418cba --- /dev/null +++ b/bigbluebutton-html5/imports/api/timer/server/publishers.js @@ -0,0 +1,22 @@ +import { Meteor } from 'meteor/meteor'; +import Logger from '/imports/startup/server/logger'; +import Timer from '/imports/api/timer'; +import { extractCredentials } from '/imports/api/common/server/helpers'; + +function timer() { + if (!this.userId) { + return Timer.find({ meetingId: '' }); + } + const { meetingId, requesterUserId } = extractCredentials(this.userId); + + Logger.info(`Publishing timer for ${meetingId} ${requesterUserId}`); + + return Timer.find({ meetingId }); +} + +function publish(...args) { + const boundTimer = timer.bind(this); + return boundTimer(...args); +} + +Meteor.publish('timer', publish); diff --git a/bigbluebutton-html5/imports/api/user-reaction/index.js b/bigbluebutton-html5/imports/api/user-reaction/index.js new file mode 100644 index 000000000000..eb5ffcbde2d3 --- /dev/null +++ b/bigbluebutton-html5/imports/api/user-reaction/index.js @@ -0,0 +1,14 @@ +import { Meteor } from 'meteor/meteor'; + +const expireSeconds = Meteor.settings.public.userReaction.expire; +const UserReaction = new Mongo.Collection('user-reaction'); + +if (Meteor.isServer) { + // TTL indexes are special single-field indexes to automatically remove documents + // from a collection after a certain amount of time. + // A single-field with only a date is necessary to this special single-field index, because + // compound indexes do not support TTL. + UserReaction._ensureIndex({ creationDate: 1 }, { expireAfterSeconds: expireSeconds }); +} + +export default UserReaction; diff --git a/bigbluebutton-html5/imports/api/user-reaction/server/eventHandlers.js b/bigbluebutton-html5/imports/api/user-reaction/server/eventHandlers.js new file mode 100644 index 000000000000..31437b0d7d14 --- /dev/null +++ b/bigbluebutton-html5/imports/api/user-reaction/server/eventHandlers.js @@ -0,0 +1,6 @@ +import RedisPubSub from '/imports/startup/server/redis'; +import handleSetUserReaction from './handlers/setUserReaction'; +import handleClearUsersReaction from './handlers/clearUsersReaction'; + +RedisPubSub.on('UserReactionEmojiChangedEvtMsg', handleSetUserReaction); +RedisPubSub.on('ClearedAllUsersReactionEvtMsg', handleClearUsersReaction); diff --git a/bigbluebutton-html5/imports/api/user-reaction/server/handlers/clearUsersReaction.js b/bigbluebutton-html5/imports/api/user-reaction/server/handlers/clearUsersReaction.js new file mode 100644 index 000000000000..b9113912b036 --- /dev/null +++ b/bigbluebutton-html5/imports/api/user-reaction/server/handlers/clearUsersReaction.js @@ -0,0 +1,7 @@ +import { check } from 'meteor/check'; +import clearReactions from '../modifiers/clearReactions'; + +export default function handleClearUsersReaction({ body }, meetingId) { + check(meetingId, String); + clearReactions(meetingId); +} diff --git a/bigbluebutton-html5/imports/api/user-reaction/server/handlers/setUserReaction.js b/bigbluebutton-html5/imports/api/user-reaction/server/handlers/setUserReaction.js new file mode 100644 index 000000000000..4b8e28629160 --- /dev/null +++ b/bigbluebutton-html5/imports/api/user-reaction/server/handlers/setUserReaction.js @@ -0,0 +1,7 @@ +import addUserReaction from '../modifiers/addUserReaction'; + +export default function handleSetUserReaction({ body }, meetingId) { + const { userId, reactionEmoji } = body; + + addUserReaction(meetingId, userId, reactionEmoji); +} diff --git a/bigbluebutton-html5/imports/api/user-reaction/server/helpers.js b/bigbluebutton-html5/imports/api/user-reaction/server/helpers.js new file mode 100644 index 000000000000..1efe976b0105 --- /dev/null +++ b/bigbluebutton-html5/imports/api/user-reaction/server/helpers.js @@ -0,0 +1,44 @@ +import RedisPubSub from '/imports/startup/server/redis'; +import UserReactions from '/imports/api/user-reaction'; +import Logger from '/imports/startup/server/logger'; + +const expireSeconds = Meteor.settings.public.userReaction.expire; +const expireMilliseconds = expireSeconds * 1000; + +const notifyExpiredReaction = (meetingId, userId) => { + try { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'UserReactionTimeExpiredCmdMsg'; + const NODE_USER = 'nodeJSapp'; + const emoji = 'none'; + + check(meetingId, String); + + const payload = { + userId, + }; + + Logger.verbose('User emoji status updated due to expiration time', { + emoji, NODE_USER, meetingId, + }); + + RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, NODE_USER, payload); + } catch (err) { + Logger.error(`Exception while invoking method resetUserReaction ${err.stack}`); + } +}; + +const addUserReactionsObserver = (meetingId) => { + const meetingUserReactions = UserReactions.find({ meetingId }); + return meetingUserReactions.observe({ + removed(document) { + const isExpirationTriggeredRemoval = (Date.now() - Date.parse(document.creationDate)) >= expireMilliseconds; + if (isExpirationTriggeredRemoval) { + notifyExpiredReaction(meetingId, document.userId); + } + }, + }); +}; + +export default addUserReactionsObserver; diff --git a/bigbluebutton-html5/imports/api/user-reaction/server/index.js b/bigbluebutton-html5/imports/api/user-reaction/server/index.js new file mode 100644 index 000000000000..92451ac76bf2 --- /dev/null +++ b/bigbluebutton-html5/imports/api/user-reaction/server/index.js @@ -0,0 +1,3 @@ +import './eventHandlers'; +import './methods'; +import './publishers'; diff --git a/bigbluebutton-html5/imports/api/user-reaction/server/methods.js b/bigbluebutton-html5/imports/api/user-reaction/server/methods.js new file mode 100644 index 000000000000..0e69eb8fc288 --- /dev/null +++ b/bigbluebutton-html5/imports/api/user-reaction/server/methods.js @@ -0,0 +1,8 @@ +import { Meteor } from 'meteor/meteor'; +import setUserReaction from './methods/setUserReaction'; +import clearAllUsersReaction from './methods/clearAllUsersReaction'; + +Meteor.methods({ + setUserReaction, + clearAllUsersReaction, +}); diff --git a/bigbluebutton-html5/imports/api/user-reaction/server/methods/clearAllUsersReaction.js b/bigbluebutton-html5/imports/api/user-reaction/server/methods/clearAllUsersReaction.js new file mode 100644 index 000000000000..77b7e960aa26 --- /dev/null +++ b/bigbluebutton-html5/imports/api/user-reaction/server/methods/clearAllUsersReaction.js @@ -0,0 +1,30 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import RedisPubSub from '/imports/startup/server/redis'; +import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; + +export default function clearAllUsersReaction() { + try { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'ClearAllUsersReactionCmdMsg'; + + const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterUserId, String); + + const payload = { + userId: requesterUserId, + }; + + Logger.verbose('Sending clear all users reactions', { + requesterUserId, meetingId, + }); + + RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); + } catch (err) { + Logger.error(`Exception while invoking method clearAllUsersReaction ${err.stack}`); + } +} diff --git a/bigbluebutton-html5/imports/api/user-reaction/server/methods/setUserReaction.js b/bigbluebutton-html5/imports/api/user-reaction/server/methods/setUserReaction.js new file mode 100644 index 000000000000..2277ac61508a --- /dev/null +++ b/bigbluebutton-html5/imports/api/user-reaction/server/methods/setUserReaction.js @@ -0,0 +1,32 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import RedisPubSub from '/imports/startup/server/redis'; +import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; + +export default function setUserReaction(reactionEmoji, userId = undefined) { + try { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'ChangeUserReactionEmojiReqMsg'; + + const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterUserId, String); + check(reactionEmoji, String); + + const payload = { + reactionEmoji, + userId: userId || requesterUserId, + }; + + Logger.verbose('User reactionEmoji status updated', { + reactionEmoji, requesterUserId, meetingId, + }); + + RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); + } catch (err) { + Logger.error(`Exception while invoking method setUserReaction ${err.stack}`); + } +} diff --git a/bigbluebutton-html5/imports/api/user-reaction/server/modifiers/addUserReaction.js b/bigbluebutton-html5/imports/api/user-reaction/server/modifiers/addUserReaction.js new file mode 100644 index 000000000000..53d46c37a676 --- /dev/null +++ b/bigbluebutton-html5/imports/api/user-reaction/server/modifiers/addUserReaction.js @@ -0,0 +1,34 @@ +import UserReaction from '/imports/api/user-reaction'; +import Logger from '/imports/startup/server/logger'; +import { check } from 'meteor/check'; + +export default function addUserReaction(meetingId, userId, reaction) { + check(meetingId, String); + check(userId, String); + check(reaction, String); + + const selector = { + creationDate: new Date(), + meetingId, + userId, + }; + + const modifier = { + $set: { + meetingId, + userId, + reaction, + }, + }; + + try { + UserReaction.remove({ meetingId, userId }); + const { numberAffected } = UserReaction.upsert(selector, modifier); + + if (numberAffected) { + Logger.verbose(`Added user reaction meetingId=${meetingId} userId=${userId}`); + } + } catch (err) { + Logger.error(`Adding user reaction: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/user-reaction/server/modifiers/clearReactions.js b/bigbluebutton-html5/imports/api/user-reaction/server/modifiers/clearReactions.js new file mode 100644 index 000000000000..4696b48bb5ac --- /dev/null +++ b/bigbluebutton-html5/imports/api/user-reaction/server/modifiers/clearReactions.js @@ -0,0 +1,26 @@ +import UserReaction from '/imports/api/user-reaction'; +import Logger from '/imports/startup/server/logger'; + +export default function clearReactions(meetingId) { + const selector = {}; + + if (meetingId) { + selector.meetingId = meetingId; + } + + try { + const numberAffected = UserReaction.remove(selector); + + if (numberAffected) { + if (meetingId) { + Logger.info(`Removed UserReaction (${meetingId})`); + } else { + Logger.info('Removed UserReaction (all)'); + } + } else { + Logger.warn('Removing UserReaction nonaffected'); + } + } catch (err) { + Logger.error(`Removing UserReaction: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/user-reaction/server/publishers.js b/bigbluebutton-html5/imports/api/user-reaction/server/publishers.js new file mode 100644 index 000000000000..82658437c37d --- /dev/null +++ b/bigbluebutton-html5/imports/api/user-reaction/server/publishers.js @@ -0,0 +1,27 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import Logger from '/imports/startup/server/logger'; +import UserReaction from '/imports/api/user-reaction'; +import { extractCredentials } from '/imports/api/common/server/helpers'; + +function userReaction() { + if (!this.userId) { + return UserReaction.find({ meetingId: '' }); + } + + const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterUserId, String); + + Logger.info(`Publishing user reaction for ${meetingId} ${requesterUserId}`); + + return UserReaction.find({ meetingId }); +} + +function publish(...args) { + const boundUserReaction = userReaction.bind(this); + return boundUserReaction(...args); +} + +Meteor.publish('user-reaction', publish); diff --git a/bigbluebutton-html5/imports/api/users-persistent-data/server/modifiers/addUserPersistentData.js b/bigbluebutton-html5/imports/api/users-persistent-data/server/modifiers/addUserPersistentData.js index 71288d6ae551..4e833027fc81 100644 --- a/bigbluebutton-html5/imports/api/users-persistent-data/server/modifiers/addUserPersistentData.js +++ b/bigbluebutton-html5/imports/api/users-persistent-data/server/modifiers/addUserPersistentData.js @@ -23,6 +23,9 @@ export default async function addUserPersistentData(user) { waitingForAcceptance: Match.Maybe(Boolean), guestStatus: String, emoji: String, + reactionEmoji: String, + raiseHand: Boolean, + away: Boolean, presenter: Boolean, locked: Boolean, avatar: String, diff --git a/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js b/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js index 2cca790a9831..68e9682bb709 100644 --- a/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js +++ b/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js @@ -34,6 +34,8 @@ const currentParameters = [ 'bbb_listen_only_mode', 'bbb_skip_check_audio', 'bbb_skip_check_audio_on_first_join', + 'bbb_fullaudio_bridge', + 'bbb_transparent_listen_only', // BRANDING 'bbb_display_branding_area', // SHORTCUTS diff --git a/bigbluebutton-html5/imports/api/users/server/eventHandlers.js b/bigbluebutton-html5/imports/api/users/server/eventHandlers.js index 7891bd0d0e22..405261fed470 100644 --- a/bigbluebutton-html5/imports/api/users/server/eventHandlers.js +++ b/bigbluebutton-html5/imports/api/users/server/eventHandlers.js @@ -8,15 +8,23 @@ import handleEmojiStatus from './handlers/emojiStatus'; import handleChangeRole from './handlers/changeRole'; import handleUserPinChanged from './handlers/userPinChanged'; import handleUserInactivityInspect from './handlers/userInactivityInspect'; -import handleChangeMobileFlag from '/imports/api/users/server/handlers/changeMobileFlag'; +import handleChangeMobileFlag from './handlers/changeMobileFlag'; +import handleChangeRaiseHand from './handlers/changeRaiseHand'; +import handleAway from './handlers/changeAway'; +import handleClearUsersEmoji from './handlers/clearUsersEmoji'; +import handleUserSpeechLocaleChanged from './handlers/userSpeechLocaleChanged'; RedisPubSub.on('PresenterAssignedEvtMsg', handlePresenterAssigned); RedisPubSub.on('UserJoinedMeetingEvtMsg', handleUserJoined); RedisPubSub.on('UserLeftMeetingEvtMsg', handleRemoveUser); RedisPubSub.on('ValidateAuthTokenRespMsg', handleValidateAuthToken); RedisPubSub.on('UserEmojiChangedEvtMsg', handleEmojiStatus); +RedisPubSub.on('ClearedAllUsersEmojiEvtMsg', handleClearUsersEmoji); +RedisPubSub.on('UserRaiseHandChangedEvtMsg', handleChangeRaiseHand); +RedisPubSub.on('UserAwayChangedEvtMsg', handleAway); RedisPubSub.on('UserRoleChangedEvtMsg', handleChangeRole); RedisPubSub.on('UserMobileFlagChangedEvtMsg', handleChangeMobileFlag); RedisPubSub.on('UserLeftFlagUpdatedEvtMsg', handleUserLeftFlagUpdated); RedisPubSub.on('UserPinStateChangedEvtMsg', handleUserPinChanged); RedisPubSub.on('UserInactivityInspectMsg', handleUserInactivityInspect); +RedisPubSub.on('UserSpeechLocaleChangedEvtMsg', handleUserSpeechLocaleChanged); diff --git a/bigbluebutton-html5/imports/api/users/server/handlers/changeAway.js b/bigbluebutton-html5/imports/api/users/server/handlers/changeAway.js new file mode 100644 index 000000000000..eff0b91e3454 --- /dev/null +++ b/bigbluebutton-html5/imports/api/users/server/handlers/changeAway.js @@ -0,0 +1,11 @@ +import { check } from 'meteor/check'; +import changeAway from '/imports/api/users/server/modifiers/changeAway'; + +export default async function handleAway(payload, meetingId) { + check(payload.body, Object); + check(meetingId, String); + + const { userId: requesterUserId, away } = payload.body; + + await changeAway(meetingId, requesterUserId, away); +} diff --git a/bigbluebutton-html5/imports/api/users/server/handlers/changeRaiseHand.js b/bigbluebutton-html5/imports/api/users/server/handlers/changeRaiseHand.js new file mode 100644 index 000000000000..6cef5bee76cd --- /dev/null +++ b/bigbluebutton-html5/imports/api/users/server/handlers/changeRaiseHand.js @@ -0,0 +1,11 @@ +import { check } from 'meteor/check'; +import changeRaiseHand from '/imports/api/users/server/modifiers/changeRaiseHand'; + +export default async function handleChangeRaiseHand(payload, meetingId) { + check(payload.body, Object); + check(meetingId, String); + + const { userId: requesterUserId, raiseHand } = payload.body; + + await changeRaiseHand(meetingId, requesterUserId, raiseHand); +} diff --git a/bigbluebutton-html5/imports/api/users/server/handlers/clearUsersEmoji.js b/bigbluebutton-html5/imports/api/users/server/handlers/clearUsersEmoji.js new file mode 100644 index 000000000000..3f9ef63cb930 --- /dev/null +++ b/bigbluebutton-html5/imports/api/users/server/handlers/clearUsersEmoji.js @@ -0,0 +1,7 @@ +import { check } from 'meteor/check'; +import clearUsersEmoji from '../modifiers/clearUsersEmoji'; + +export default function handleClearUsersEmoji({ body }, meetingId) { + check(meetingId, String); + clearUsersEmoji(meetingId); +} diff --git a/bigbluebutton-html5/imports/api/users/server/handlers/reactionEmoji.js b/bigbluebutton-html5/imports/api/users/server/handlers/reactionEmoji.js new file mode 100644 index 000000000000..bff5d4d26cff --- /dev/null +++ b/bigbluebutton-html5/imports/api/users/server/handlers/reactionEmoji.js @@ -0,0 +1,32 @@ +import Logger from '/imports/startup/server/logger'; +import { check } from 'meteor/check'; +import Users from '/imports/api/users'; + +export default async function handleReactionEmoji({ body }, meetingId) { + const { userId, reactionEmoji } = body; +aaa + check(userId, String); + check(reactionEmoji, String); + + const selector = { + meetingId, + userId, + }; + + const modifier = { + $set: { + reactionEmojiTime: (new Date()).getTime(), + reactionEmoji, + }, + }; + + try { + const numberAffected = await Users.updateAsync(selector, modifier); + + if (numberAffected) { + Logger.info(`Assigned user rectionEmoji ${reactionEmoji} id=${userId} meeting=${meetingId}`); + } + } catch (err) { + Logger.error(`Assigning user reactionEmoji: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/users/server/handlers/userSpeechLocaleChanged.js b/bigbluebutton-html5/imports/api/users/server/handlers/userSpeechLocaleChanged.js new file mode 100644 index 000000000000..205cfee3b919 --- /dev/null +++ b/bigbluebutton-html5/imports/api/users/server/handlers/userSpeechLocaleChanged.js @@ -0,0 +1,13 @@ +import { check } from 'meteor/check'; +import updateSpeechLocale from '../modifiers/updateSpeechLocale'; + +export default function handleUserSpeechLocaleChanged({ body, header }, meetingId) { + const { locale } = body; + const { userId } = header; + + check(meetingId, String); + check(userId, String); + check(locale, String); + + return updateSpeechLocale(meetingId, userId, locale); +} diff --git a/bigbluebutton-html5/imports/api/users/server/methods.js b/bigbluebutton-html5/imports/api/users/server/methods.js index 3d64c05d56f7..c67a644f1cd1 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods.js +++ b/bigbluebutton-html5/imports/api/users/server/methods.js @@ -1,8 +1,10 @@ import { Meteor } from 'meteor/meteor'; import validateAuthToken from './methods/validateAuthToken'; -import setEmojiStatus from './methods/setEmojiStatus'; import setSpeechLocale from './methods/setSpeechLocale'; import setMobileUser from './methods/setMobileUser'; +import setEmojiStatus from './methods/setEmojiStatus'; +import changeAway from './methods/changeAway'; +import changeRaiseHand from './methods/changeRaiseHand'; import assignPresenter from './methods/assignPresenter'; import changeRole from './methods/changeRole'; import removeUser from './methods/removeUser'; @@ -13,11 +15,15 @@ import userLeftMeeting from './methods/userLeftMeeting'; import changePin from './methods/changePin'; import setRandomUser from './methods/setRandomUser'; import setExitReason from './methods/setExitReason'; +import clearAllUsersEmoji from './methods/clearAllUsersEmoji'; Meteor.methods({ - setEmojiStatus, setSpeechLocale, setMobileUser, + setEmojiStatus, + clearAllUsersEmoji, + changeAway, + changeRaiseHand, assignPresenter, changeRole, removeUser, diff --git a/bigbluebutton-html5/imports/api/users/server/methods/changeAway.js b/bigbluebutton-html5/imports/api/users/server/methods/changeAway.js new file mode 100644 index 000000000000..929dfad52bc3 --- /dev/null +++ b/bigbluebutton-html5/imports/api/users/server/methods/changeAway.js @@ -0,0 +1,31 @@ +import { check } from 'meteor/check'; +import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; +import RedisPubSub from '/imports/startup/server/redis'; + +export default async function changeAway(away) { + try { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'ChangeUserAwayReqMsg'; + + const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterUserId, String); + check(away, Boolean); + + const payload = { + userId: requesterUserId, + away, + }; + + Logger.verbose('Updated away status for user', { + meetingId, requesterUserId, away, + }); + + RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); + } catch (err) { + Logger.error(`Exception while invoking method changeAway ${err.stack}`); + } +} diff --git a/bigbluebutton-html5/imports/api/users/server/methods/changeRaiseHand.js b/bigbluebutton-html5/imports/api/users/server/methods/changeRaiseHand.js new file mode 100644 index 000000000000..77d7d8b756c8 --- /dev/null +++ b/bigbluebutton-html5/imports/api/users/server/methods/changeRaiseHand.js @@ -0,0 +1,31 @@ +import { check } from 'meteor/check'; +import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; +import RedisPubSub from '/imports/startup/server/redis'; + +export default async function changeRaiseHand(raiseHand, userId = undefined) { + try { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'ChangeUserRaiseHandReqMsg'; + + const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterUserId, String); + check(raiseHand, Boolean); + + const payload = { + userId: userId || requesterUserId, + raiseHand, + }; + + Logger.verbose('Updated raiseHand status for user', { + meetingId, requesterUserId, raiseHand, + }); + + RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); + } catch (err) { + Logger.error(`Exception while invoking method changeRaiseHand ${err.stack}`); + } +} diff --git a/bigbluebutton-html5/imports/api/users/server/methods/clearAllUsersEmoji.js b/bigbluebutton-html5/imports/api/users/server/methods/clearAllUsersEmoji.js new file mode 100644 index 000000000000..916f31db6f78 --- /dev/null +++ b/bigbluebutton-html5/imports/api/users/server/methods/clearAllUsersEmoji.js @@ -0,0 +1,30 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import RedisPubSub from '/imports/startup/server/redis'; +import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; + +export default function clearAllUsersEmoji() { + try { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'ClearAllUsersEmojiCmdMsg'; + + const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterUserId, String); + + const payload = { + userId: requesterUserId, + }; + + Logger.verbose('Sending clear all users emojis', { + requesterUserId, meetingId, + }); + + RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); + } catch (err) { + Logger.error(`Exception while invoking method setUserReaction ${err.stack}`); + } +} diff --git a/bigbluebutton-html5/imports/api/users/server/methods/sendAwayStatusChatMsg.js b/bigbluebutton-html5/imports/api/users/server/methods/sendAwayStatusChatMsg.js new file mode 100644 index 000000000000..d2c082d74b76 --- /dev/null +++ b/bigbluebutton-html5/imports/api/users/server/methods/sendAwayStatusChatMsg.js @@ -0,0 +1,72 @@ +import { Meteor } from 'meteor/meteor'; + +import Meetings from '/imports/api/meetings'; +import Users from '/imports/api/users'; +import addSystemMsg from '/imports/api/group-chat-msg/server/modifiers/addSystemMsg'; + +const ROLE_VIEWER = Meteor.settings.public.user.role_viewer; +const CHAT_CONFIG = Meteor.settings.public.chat; +const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id; +const CHAT_USER_STATUS_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_status_message; +const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system; + +export default function sendAwayStatusChatMsg(meetingId, userId, newAwayStatus) { + const user = Users.findOne( + { meetingId, userId }, + { + fields: { + name: 1, + role: 1, + locked: 1, + away: 1, + }, + }, + ); + + if (!user) return null; + + // Check for viewer permissions + if (user.role === ROLE_VIEWER && user.locked) { + const meeting = Meetings.findOne( + { meetingId }, + { fields: { 'lockSettingsProps.disablePublicChat': 1 } }, + ); + + if (!meeting) return null; + + // Return if viewer has his public chat disabled + const { lockSettingsProps } = meeting; + if (lockSettingsProps && lockSettingsProps.disablePublicChat) { + return null; + } + } + + // Send message if previous emoji or actual emoji is 'away' + let status; + if (user.away && !newAwayStatus) { + status = 'notAway'; + } else if (!user.away && newAwayStatus) { + status = 'away'; + } else { + return null; + } + + const extra = { + type: 'status', + status, + }; + + const payload = { + id: `${SYSTEM_CHAT_TYPE}-${CHAT_USER_STATUS_MESSAGE}`, + timestamp: Date.now(), + correlationId: `${userId}-${Date.now()}`, + sender: { + id: userId, + name: user.name, + }, + message: '', + extra, + }; + + return addSystemMsg(meetingId, PUBLIC_GROUP_CHAT_ID, payload); +} diff --git a/bigbluebutton-html5/imports/api/users/server/methods/setSpeechLocale.js b/bigbluebutton-html5/imports/api/users/server/methods/setSpeechLocale.js index 6945cdc0f60d..3bc0625221c1 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods/setSpeechLocale.js +++ b/bigbluebutton-html5/imports/api/users/server/methods/setSpeechLocale.js @@ -1,20 +1,30 @@ import { check } from 'meteor/check'; import Logger from '/imports/startup/server/logger'; -import updateSpeechLocale from '../modifiers/updateSpeechLocale'; +import RedisPubSub from '/imports/startup/server/redis'; import { extractCredentials } from '/imports/api/common/server/helpers'; const LANGUAGES = Meteor.settings.public.app.audioCaptions.language.available; -export default async function setSpeechLocale(locale) { +export default function setSpeechLocale(locale, provider) { try { const { meetingId, requesterUserId } = extractCredentials(this.userId); + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'SetUserSpeechLocaleReqMsg'; + check(meetingId, String); check(requesterUserId, String); check(locale, String); + check(provider, String); + + const payload = { + locale, + provider: provider !== 'webspeech' ? provider : '', + }; if (LANGUAGES.includes(locale) || locale === '') { - await updateSpeechLocale(meetingId, requesterUserId, locale); + RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); } } catch (err) { Logger.error(`Exception while invoking method setSpeechLocale ${err.stack}`); diff --git a/bigbluebutton-html5/imports/api/users/server/modifiers/addDialInUser.js b/bigbluebutton-html5/imports/api/users/server/modifiers/addDialInUser.js index 9cb82b76e22d..a760c989ac8b 100644 --- a/bigbluebutton-html5/imports/api/users/server/modifiers/addDialInUser.js +++ b/bigbluebutton-html5/imports/api/users/server/modifiers/addDialInUser.js @@ -20,6 +20,9 @@ export default async function addDialInUser(meetingId, voiceUser) { waitingForAcceptance: false, guestStatus: 'ALLOW', emoji: 'none', + reactionEmoji: 'none', + raiseHand: false, + away: false, presenter: false, locked: false, // TODO avatar: '', diff --git a/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js b/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js index b8a476989bb1..6945110ce233 100755 --- a/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js +++ b/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js @@ -24,6 +24,9 @@ export default async function addUser(meetingId, userData) { waitingForAcceptance: Match.Maybe(Boolean), guestStatus: String, emoji: String, + reactionEmoji: String, + raiseHand: Boolean, + away: Boolean, presenter: Boolean, locked: Boolean, avatar: String, diff --git a/bigbluebutton-html5/imports/api/users/server/modifiers/changeAway.js b/bigbluebutton-html5/imports/api/users/server/modifiers/changeAway.js new file mode 100644 index 000000000000..c5ec077c1bc4 --- /dev/null +++ b/bigbluebutton-html5/imports/api/users/server/modifiers/changeAway.js @@ -0,0 +1,30 @@ +import Logger from '/imports/startup/server/logger'; +import Users from '/imports/api/users'; +import sendAwayStatusChatMsg from '../methods/sendAwayStatusChatMsg'; + +export default async function changeAway(meetingId, userId, away) { + const selector = { + meetingId, + userId, + }; + + const modifier = { + $set: { + away, + awayTime: away ? (new Date()).getTime() : 0, + }, + }; + + try { + // must be called before modifying the users collection, because it + // needs to be consulted in order to know the previous emoji + sendAwayStatusChatMsg(meetingId, userId, away); + + const numberAffected = await Users.updateAsync(selector, modifier); + if (numberAffected) { + Logger.info(`Assigned away=${away} user id=${userId} meeting=${meetingId}`); + } + } catch (err) { + Logger.error(`Assigning away user: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/users/server/modifiers/changeRaiseHand.js b/bigbluebutton-html5/imports/api/users/server/modifiers/changeRaiseHand.js new file mode 100644 index 000000000000..543e1d7e344d --- /dev/null +++ b/bigbluebutton-html5/imports/api/users/server/modifiers/changeRaiseHand.js @@ -0,0 +1,26 @@ +import Logger from '/imports/startup/server/logger'; +import Users from '/imports/api/users'; + +export default async function changeRaiseHand(meetingId, userId, raiseHand) { + const selector = { + meetingId, + userId, + }; + + const modifier = { + $set: { + raiseHand, + raiseHandTime: raiseHand ? (new Date()).getTime() : 0, + }, + }; + + try { + const numberAffected = await Users.updateAsync(selector, modifier); + + if (numberAffected) { + Logger.info(`Assigned raiseHand=${raiseHand} user id=${userId} meeting=${meetingId}`); + } + } catch (err) { + Logger.error(`Assigning raiseHand user: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/users/server/modifiers/clearUsersEmoji.js b/bigbluebutton-html5/imports/api/users/server/modifiers/clearUsersEmoji.js new file mode 100644 index 000000000000..ed9ad03eb7f5 --- /dev/null +++ b/bigbluebutton-html5/imports/api/users/server/modifiers/clearUsersEmoji.js @@ -0,0 +1,35 @@ +import Users from '/imports/api/users'; +import Logger from '/imports/startup/server/logger'; + +export default function clearUsersEmoji(meetingId) { + const selector = {}; + + if (meetingId) { + selector.meetingId = meetingId; + } + + try { + const numberAffected = Users.update(selector, { + $set: { + emojiTime: (new Date()).getTime(), + emoji: 'none', + awayTime: 0, + away: false, + raiseHandTime: 0, + raiseHand: false, + }, + }, { multi: true }); + + if (numberAffected) { + if (meetingId) { + Logger.info(`Removed users emoji status (${meetingId})`); + } else { + Logger.info('Removed users emoji status (all)'); + } + } else { + Logger.warn('Removing users emoji status nonaffected'); + } + } catch (err) { + Logger.error(`Removing users emoji stautus: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/voice-users/server/publishers.js b/bigbluebutton-html5/imports/api/voice-users/server/publishers.js index 36a2b13b2ff3..942cf229e0f6 100644 --- a/bigbluebutton-html5/imports/api/voice-users/server/publishers.js +++ b/bigbluebutton-html5/imports/api/voice-users/server/publishers.js @@ -2,7 +2,6 @@ import VoiceUsers from '/imports/api/voice-users'; import { Meteor } from 'meteor/meteor'; import Logger from '/imports/startup/server/logger'; import AuthTokenValidation, { ValidationStates } from '/imports/api/auth-token-validation'; -import ejectUserFromVoice from './methods/ejectUserFromVoice'; async function voiceUser() { const tokenValidation = await AuthTokenValidation @@ -14,22 +13,8 @@ async function voiceUser() { } const { meetingId, userId: requesterUserId } = tokenValidation; - - const onCloseConnection = Meteor.bindEnvironment(async () => { - try { - // I used user because voiceUser is the function's name - const User = await VoiceUsers.findOneAsync({ meetingId, requesterUserId }); - if (User) { - await ejectUserFromVoice(requesterUserId); - } - } catch (e) { - Logger.error(`Exception while executing ejectUserFromVoice for ${requesterUserId}: ${e}`); - } - }); - Logger.debug('Publishing Voice User', { meetingId, requesterUserId }); - this._session.socket.on('close', _.debounce(onCloseConnection, 100)); return VoiceUsers.find({ meetingId }); } diff --git a/bigbluebutton-html5/imports/startup/client/intl.jsx b/bigbluebutton-html5/imports/startup/client/intl.jsx index ad8087c5b9d6..37eb639a7ba7 100644 --- a/bigbluebutton-html5/imports/startup/client/intl.jsx +++ b/bigbluebutton-html5/imports/startup/client/intl.jsx @@ -5,11 +5,11 @@ import { IntlProvider } from 'react-intl'; import Settings from '/imports/ui/services/settings'; import LoadingScreen from '/imports/ui/components/common/loading-screen/component'; import getFromUserSettings from '/imports/ui/services/users-settings'; -import _ from 'lodash'; import { Session } from 'meteor/session'; import Logger from '/imports/startup/client/logger'; import { formatLocaleCode } from '/imports/utils/string-utils'; import Intl from '/imports/ui/services/locale'; +import { isEmpty } from 'radash'; const propTypes = { locale: PropTypes.string, @@ -58,7 +58,7 @@ class IntlStartup extends Component { if (overrideLocaleFromPassedParameter !== prevProps.overrideLocaleFromPassedParameter) { this.fetchLocalizedMessages(overrideLocaleFromPassedParameter); } else { - const shouldFetch = (!fetching && _.isEmpty(messages)) || ((locale !== prevProps.locale) && (normalizedLocale && (locale !== normalizedLocale))); + const shouldFetch = (!fetching && isEmpty(messages)) || ((locale !== prevProps.locale) && (normalizedLocale && (locale !== normalizedLocale))); if (shouldFetch) this.fetchLocalizedMessages(locale); } } diff --git a/bigbluebutton-html5/imports/startup/server/settings.js b/bigbluebutton-html5/imports/startup/server/settings.js index 3189369502b1..bdab48a5bc92 100644 --- a/bigbluebutton-html5/imports/startup/server/settings.js +++ b/bigbluebutton-html5/imports/startup/server/settings.js @@ -2,19 +2,19 @@ import { Meteor } from 'meteor/meteor'; import fs from 'fs'; import YAML from 'yaml'; -import _ from 'lodash'; +import { defaultsDeep } from '/imports/utils/array-utils'; const DEFAULT_SETTINGS_FILE_PATH = process.env.BBB_HTML5_SETTINGS || 'assets/app/config/settings.yml'; const LOCAL_SETTINGS_FILE_PATH = process.env.BBB_HTML5_LOCAL_SETTINGS || '/etc/bigbluebutton/bbb-html5.yml'; try { if (fs.existsSync(DEFAULT_SETTINGS_FILE_PATH)) { - const SETTINGS = YAML.parse(fs.readFileSync(DEFAULT_SETTINGS_FILE_PATH, 'utf-8')); + let SETTINGS = YAML.parse(fs.readFileSync(DEFAULT_SETTINGS_FILE_PATH, 'utf-8')); if (fs.existsSync(LOCAL_SETTINGS_FILE_PATH)) { console.log('Local configuration found! Merging with default configuration...'); const LOCAL_CONFIG = YAML.parse(fs.readFileSync(LOCAL_SETTINGS_FILE_PATH, 'utf-8')); - _.mergeWith(SETTINGS, LOCAL_CONFIG, (a, b) => (_.isArray(b) ? b : undefined)); + SETTINGS = defaultsDeep(LOCAL_CONFIG, SETTINGS); } else console.log('Local Configuration not found! Loading default configuration...'); Meteor.settings = SETTINGS; diff --git a/bigbluebutton-html5/imports/ui/components/about/component.jsx b/bigbluebutton-html5/imports/ui/components/about/component.jsx index 935344eeacab..a240891d9232 100644 --- a/bigbluebutton-html5/imports/ui/components/about/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/about/component.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { defineMessages, injectIntl } from 'react-intl'; -import Modal from '/imports/ui/components/common/modal/simple/component'; +import ModalSimple from '/imports/ui/components/common/modal/simple/component'; const intlMessages = defineMessages({ title: { @@ -38,7 +38,8 @@ const intlMessages = defineMessages({ }, }); -const AboutComponent = ({ intl, settings }) => { +const AboutComponent = (props) => { + const { intl, settings, isOpen, onRequestClose, priority, } = props; const { html5ClientBuild, copyright, @@ -54,20 +55,25 @@ const AboutComponent = ({ intl, settings }) => { ); return ( - {`${intl.formatMessage(intlMessages.copyright)} ${copyright}`}
{`${intl.formatMessage(intlMessages.version)} ${html5ClientBuild}`} {displayBbbServerVersion ? showLabelVersion() : null} -
+ ); }; diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx index 4ea3354d0ca9..6f93ec201e5f 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx @@ -1,34 +1,38 @@ -import _ from 'lodash'; import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { defineMessages } from 'react-intl'; -import { withModalMounter } from '/imports/ui/components/common/modal/service'; import withShortcutHelper from '/imports/ui/components/shortcut-help/service'; import ExternalVideoModal from '/imports/ui/components/external-video-player/modal/container'; import RandomUserSelectContainer from '/imports/ui/components/common/modal/random-user/container'; import LayoutModalContainer from '/imports/ui/components/layout/modal/container'; import BBBMenu from '/imports/ui/components/common/menu/component'; import Styled from './styles'; +import TimerService from '/imports/ui/components/timer/service'; import { colorPrimary } from '/imports/ui/stylesheets/styled-components/palette'; import { PANELS, ACTIONS, LAYOUT_TYPE } from '../../layout/enums'; -import { isPresentationEnabled } from '/imports/ui/services/features'; -import {isLayoutsEnabled} from '/imports/ui/services/features'; +import { uniqueId } from '/imports/utils/string-utils'; +import { isPresentationEnabled, isLayoutsEnabled } from '/imports/ui/services/features'; +import VideoPreviewContainer from '/imports/ui/components/video-preview/container'; +import { screenshareHasEnded } from '/imports/ui/components/screenshare/service'; const propTypes = { amIPresenter: PropTypes.bool.isRequired, intl: PropTypes.shape({ formatMessage: PropTypes.func.isRequired, }).isRequired, - mountModal: PropTypes.func.isRequired, amIModerator: PropTypes.bool.isRequired, shortcuts: PropTypes.string, handleTakePresenter: PropTypes.func.isRequired, + isTimerActive: PropTypes.bool.isRequired, + isTimerEnabled: PropTypes.bool.isRequired, allowExternalVideo: PropTypes.bool.isRequired, stopExternalVideoShare: PropTypes.func.isRequired, isMobile: PropTypes.bool.isRequired, setMeetingLayout: PropTypes.func.isRequired, setPushLayout: PropTypes.func.isRequired, showPushLayout: PropTypes.bool.isRequired, + isTimerFeatureEnabled: PropTypes.bool.isRequired, + isCameraAsContentEnabled: PropTypes.bool.isRequired, }; const defaultProps = { @@ -41,6 +45,14 @@ const intlMessages = defineMessages({ id: 'app.actionsBar.actionsDropdown.actionsLabel', description: 'Actions button label', }, + activateTimerStopwatchLabel: { + id: 'app.actionsBar.actionsDropdown.activateTimerStopwatchLabel', + description: 'Activate timer/stopwatch label', + }, + deactivateTimerStopwatchLabel: { + id: 'app.actionsBar.actionsDropdown.deactivateTimerStopwatchLabel', + description: 'Deactivate timer/stopwatch label', + }, presentationLabel: { id: 'app.actionsBar.actionsDropdown.presentationLabel', description: 'Upload a presentation option label', @@ -89,14 +101,18 @@ const intlMessages = defineMessages({ id: 'app.actionsBar.actionsDropdown.selectRandUserDesc', description: 'Description for select random user option', }, - propagateLayoutLabel: { - id: 'app.actionsBar.actionsDropdown.propagateLayoutLabel', - description: 'Label for propagate layout button', - }, layoutModal: { id: 'app.actionsBar.actionsDropdown.layoutModal', description: 'Label for layouts selection button', }, + shareCameraAsContent: { + id: 'app.actionsBar.actionsDropdown.shareCameraAsContent', + description: 'Label for share camera as content', + }, + unshareCameraAsContent: { + id: 'app.actionsBar.actionsDropdown.unshareCameraAsContent', + description: 'Label for unshare camera as content', + }, }); const handlePresentationClick = () => Session.set('showUploadPresentationView', true); @@ -105,26 +121,48 @@ class ActionsDropdown extends PureComponent { constructor(props) { super(props); - this.presentationItemId = _.uniqueId('action-item-'); - this.pollId = _.uniqueId('action-item-'); - this.takePresenterId = _.uniqueId('action-item-'); - this.selectUserRandId = _.uniqueId('action-item-'); + this.presentationItemId = uniqueId('action-item-'); + this.pollId = uniqueId('action-item-'); + this.takePresenterId = uniqueId('action-item-'); + this.timerId = uniqueId('action-item-'); + this.selectUserRandId = uniqueId('action-item-'); + this.state = { + isExternalVideoModalOpen: false, + isRandomUserSelectModalOpen: false, + isLayoutModalOpen: false, + isCameraAsContentModalOpen: false, + }; this.handleExternalVideoClick = this.handleExternalVideoClick.bind(this); this.makePresentationItems = this.makePresentationItems.bind(this); + this.setExternalVideoModalIsOpen = this.setExternalVideoModalIsOpen.bind(this); + this.setRandomUserSelectModalIsOpen = this.setRandomUserSelectModalIsOpen.bind(this); + this.setLayoutModalIsOpen = this.setLayoutModalIsOpen.bind(this); + this.setCameraAsContentModalIsOpen = this.setCameraAsContentModalIsOpen.bind(this); + this.setPropsToPassModal = this.setPropsToPassModal.bind(this); + this.setForceOpen = this.setForceOpen.bind(this); + this.handleTimerClick = this.handleTimerClick.bind(this); } componentDidUpdate(prevProps) { const { amIPresenter: wasPresenter } = prevProps; - const { amIPresenter: isPresenter, mountModal } = this.props; + const { amIPresenter: isPresenter } = this.props; if (wasPresenter && !isPresenter) { - mountModal(null); + this.setExternalVideoModalIsOpen(false); } } handleExternalVideoClick() { - const { mountModal } = this.props; - mountModal(); + this.setExternalVideoModalIsOpen(true); + } + + handleTimerClick() { + const { isTimerActive, layoutContextDispatch } = this.props; + if (!isTimerActive) { + TimerService.activateTimer(layoutContextDispatch); + } else { + TimerService.deactivateTimer(); + } } getAvailableActions() { @@ -137,41 +175,40 @@ class ActionsDropdown extends PureComponent { isPollingEnabled, isSelectRandomUserEnabled, stopExternalVideoShare, - mountModal, + isTimerActive, + isTimerEnabled, layoutContextDispatch, setMeetingLayout, setPushLayout, showPushLayout, amIModerator, + isMobile, + hasCameraAsContent, + isCameraAsContentEnabled, + isTimerFeatureEnabled, } = this.props; - const { - pollBtnLabel, - presentationLabel, - takePresenter, - } = intlMessages; + const { pollBtnLabel, presentationLabel, takePresenter } = intlMessages; - const { - formatMessage, - } = intl; + const { formatMessage } = intl; const actions = []; if (amIPresenter && isPresentationEnabled()) { actions.push({ - icon: "upload", - dataTest: "managePresentations", + icon: 'upload', + dataTest: 'managePresentations', label: formatMessage(presentationLabel), key: this.presentationItemId, onClick: handlePresentationClick, dividerTop: this.props?.presentations?.length > 1 ? true : false, - }) + }); } if (amIPresenter && isPollingEnabled) { actions.push({ - icon: "polling", - dataTest: "polling", + icon: 'polling', + dataTest: 'polling', label: formatMessage(pollBtnLabel), key: this.pollId, onClick: () => { @@ -188,12 +225,12 @@ class ActionsDropdown extends PureComponent { }); Session.set('forcePollOpen', true); }, - }) + }); } if (!amIPresenter && amIModerator) { actions.push({ - icon: "presentation", + icon: 'presentation', label: formatMessage(takePresenter), key: this.takePresenterId, onClick: () => handleTakePresenter(), @@ -202,45 +239,64 @@ class ActionsDropdown extends PureComponent { if (amIPresenter && allowExternalVideo) { actions.push({ - icon: !isSharingVideo ? "external-video" : "external-video_off", - label: !isSharingVideo ? intl.formatMessage(intlMessages.startExternalVideoLabel) + icon: !isSharingVideo ? 'external-video' : 'external-video_off', + label: !isSharingVideo + ? intl.formatMessage(intlMessages.startExternalVideoLabel) : intl.formatMessage(intlMessages.stopExternalVideoLabel), - key: "external-video", + key: 'external-video', onClick: isSharingVideo ? stopExternalVideoShare : this.handleExternalVideoClick, - dataTest: "shareExternalVideo", - }) + dataTest: 'shareExternalVideo', + }); } if (amIPresenter && isSelectRandomUserEnabled) { actions.push({ - icon: "user", + icon: 'user', label: intl.formatMessage(intlMessages.selectRandUserLabel), key: this.selectUserRandId, - onClick: () => mountModal(), - dataTest: "selectRandomUser", - }) + onClick: () => this.setRandomUserSelectModalIsOpen(true), + dataTest: 'selectRandomUser', + }); } - if (amIPresenter && showPushLayout && isLayoutsEnabled()) { + if (amIModerator && isTimerEnabled && isTimerFeatureEnabled) { actions.push({ - icon: 'send', - label: intl.formatMessage(intlMessages.propagateLayoutLabel), - key: 'propagate layout', - onClick: amIPresenter ? setMeetingLayout : setPushLayout, - dataTest: 'propagateLayout', + icon: 'time', + label: isTimerActive + ? intl.formatMessage(intlMessages.deactivateTimerStopwatchLabel) + : intl.formatMessage(intlMessages.activateTimerStopwatchLabel), + key: this.timerId, + onClick: () => this.handleTimerClick(), }); } - if (isLayoutsEnabled()){ + if (isLayoutsEnabled()) { actions.push({ - icon: 'send', + icon: 'manage_layout', label: intl.formatMessage(intlMessages.layoutModal), key: 'layoutModal', - onClick: () => mountModal(), - dataTest: 'layoutModal', + onClick: () => this.setLayoutModalIsOpen(true), + dataTest: 'manageLayoutBtn', + }); + } + + if (isCameraAsContentEnabled && amIPresenter) { + actions.push({ + icon: hasCameraAsContent ? 'video_off' : 'video', + label: hasCameraAsContent + ? intl.formatMessage(intlMessages.unshareCameraAsContent) + : intl.formatMessage(intlMessages.shareCameraAsContent), + key: 'camera as content', + onClick: hasCameraAsContent + ? screenshareHasEnded + : () => { + screenshareHasEnded(); + this.setCameraAsContentModalIsOpen(true); + }, + dataTest: 'shareCameraAsContent', }); } - + return actions; } @@ -259,7 +315,7 @@ class ActionsDropdown extends PureComponent { const { podId } = podIds[0]; const presentationItemElements = presentations - .sort((a, b) => (a.name.localeCompare(b.name))) + .sort((a, b) => a.name.localeCompare(b.name)) .map((p) => { const customStyles = { color: colorPrimary }; @@ -279,10 +335,45 @@ class ActionsDropdown extends PureComponent { } ); }); - return presentationItemElements; } + setExternalVideoModalIsOpen(value) { + this.setState({ isExternalVideoModalOpen: value }); + } + + setRandomUserSelectModalIsOpen(value) { + this.setState({ isRandomUserSelectModalOpen: value }); + } + + setLayoutModalIsOpen(value) { + this.setState({ isLayoutModalOpen: value }); + } + + setCameraAsContentModalIsOpen(value) { + this.setState({ isCameraAsContentModalOpen: value }); + } + + setPropsToPassModal(value) { + this.setState({ propsToPassModal: value }); + } + setForceOpen(value) { + this.setState({ forceOpen: value }); + } + + renderModal(isOpen, setIsOpen, priority, Component) { + return isOpen ? ( + setIsOpen(false), + priority, + setIsOpen, + isOpen, + }} + /> + ) : null; + } + render() { const { intl, @@ -292,50 +383,102 @@ class ActionsDropdown extends PureComponent { isDropdownOpen, isMobile, isRTL, + isSelectRandomUserEnabled, + propsToPassModal, } = this.props; + const { + isExternalVideoModalOpen, + isRandomUserSelectModalOpen, + isLayoutModalOpen, + isCameraAsContentModalOpen, + } = this.state; + const availableActions = this.getAvailableActions(); const availablePresentations = this.makePresentationItems(); const children = availablePresentations.length > 1 && amIPresenter - ? availablePresentations.concat(availableActions) : availableActions; + ? availablePresentations.concat(availableActions) + : availableActions; const customStyles = { top: '-1rem' }; - if (availableActions.length === 0 - || !isMeteorConnected) { + if (availableActions.length === 0 || !isMeteorConnected) { return null; } return ( - null} - /> - } - actions={children} - opts={{ - id: "actions-dropdown-menu", - keepMounted: true, - transitionDuration: 0, - elevation: 3, - getContentAnchorEl: null, - fullwidth: "true", - anchorOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' }, - transformOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' }, - }} - /> + <> + null} + /> + )} + actions={children} + opts={{ + id: 'actions-dropdown-menu', + keepMounted: true, + transitionDuration: 0, + elevation: 3, + getcontentanchorel: null, + fullwidth: 'true', + anchorOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' }, + transformOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' }, + }} + /> + {this.renderModal( + isExternalVideoModalOpen, + this.setExternalVideoModalIsOpen, + 'low', + ExternalVideoModal, + )} + {amIPresenter && isSelectRandomUserEnabled + ? this.renderModal( + isRandomUserSelectModalOpen, + this.setRandomUserSelectModalIsOpen, + 'low', + RandomUserSelectContainer, + ) + : null} + {this.renderModal( + isLayoutModalOpen, + this.setLayoutModalIsOpen, + 'low', + LayoutModalContainer, + )} + {this.renderModal( + isCameraAsContentModalOpen, + this.setCameraAsContentModalIsOpen, + 'low', + () => ( + { + this.setPropsToPassModal({}); + this.setForceOpen(false); + }, + priority: 'low', + setIsOpen: this.setCameraAsContentModalIsOpen, + isOpen: isCameraAsContentModalOpen, + }} + {...propsToPassModal} + /> + ) + )} + ); } } @@ -343,4 +486,4 @@ class ActionsDropdown extends PureComponent { ActionsDropdown.propTypes = propTypes; ActionsDropdown.defaultProps = defaultProps; -export default withShortcutHelper(withModalMounter(ActionsDropdown), 'openActions'); +export default withShortcutHelper(ActionsDropdown, 'openActions'); diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/container.jsx index 798289ed246c..e1bb4eb235a4 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/container.jsx @@ -6,6 +6,7 @@ import PresentationPodService from '/imports/ui/components/presentation-pod/serv import ActionsDropdown from './component'; import { layoutSelectInput, layoutDispatch, layoutSelect } from '../../layout/context'; import { SMALL_VIEWPORT_BREAKPOINT } from '../../layout/enums'; +import { isCameraAsContentEnabled, isTimerFeatureEnabled } from '/imports/ui/services/features'; const ActionsDropdownContainer = (props) => { const sidebarContent = layoutSelectInput((i) => i.sidebarContent); @@ -16,24 +17,27 @@ const ActionsDropdownContainer = (props) => { const isRTL = layoutSelect((i) => i.isRTL); return ( - ); }; export default withTracker(() => { const presentations = Presentations.find({ 'conversion.done': true }).fetch(); - return ({ + return { presentations, + isTimerFeatureEnabled: isTimerFeatureEnabled(), isDropdownOpen: Session.get('dropdownOpen'), setPresentation: PresentationUploaderService.setPresentation, podIds: PresentationPodService.getPresentationPodIds(), - }); + isCameraAsContentEnabled: isCameraAsContentEnabled(), + }; })(ActionsDropdownContainer); diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx index d1b9b216e11a..55f98269b5cc 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx @@ -4,7 +4,9 @@ import deviceInfo from '/imports/utils/deviceInfo'; import Styled from './styles'; import ActionsDropdown from './actions-dropdown/container'; import AudioCaptionsButtonContainer from '/imports/ui/components/audio/captions/button/container'; +import CaptionsReaderMenuContainer from '/imports/ui/components/captions/reader-menu/container'; import ScreenshareButtonContainer from '/imports/ui/components/actions-bar/screenshare/container'; +import ReactionsButtonContainer from './reactions-button/container'; import AudioControlsContainer from '../audio/audio-controls/container'; import JoinVideoOptionsContainer from '../video-provider/video-button/container'; import PresentationOptionsContainer from './presentation-options/component'; @@ -12,6 +14,38 @@ import RaiseHandDropdownContainer from './raise-hand/container'; import { isPresentationEnabled } from '/imports/ui/services/features'; class ActionsBar extends PureComponent { + constructor(props) { + super(props); + + this.state = { + isCaptionsReaderMenuModalOpen: false, + }; + + this.setCaptionsReaderMenuModalIsOpen = this.setCaptionsReaderMenuModalIsOpen.bind(this); + this.setRenderRaiseHand = this.renderRaiseHand.bind(this); + this.actionsBarRef = React.createRef(); + } + + setCaptionsReaderMenuModalIsOpen(value) { + this.setState({ isCaptionsReaderMenuModalOpen: value }) + } + + renderRaiseHand() { + const { + isReactionsButtonEnabled, isRaiseHandButtonEnabled, setEmojiStatus, currentUser, intl, + } = this.props; + + return (<> + {isReactionsButtonEnabled ? + <> + + + : + isRaiseHandButtonEnabled ? + : null} + ); + } + render() { const { amIPresenter, @@ -24,16 +58,18 @@ class ActionsBar extends PureComponent { isSharingVideo, isSharedNotesPinned, hasScreenshare, + hasGenericContent, + hasCameraAsContent, stopExternalVideoShare, + isTimerActive, + isTimerEnabled, isCaptionsAvailable, isMeteorConnected, isPollingEnabled, isSelectRandomUserEnabled, - isRaiseHandButtonEnabled, + isRaiseHandButtonCentered, isThereCurrentPresentation, allowExternalVideo, - setEmojiStatus, - currentUser, layoutContextDispatch, actionsBarStyle, setMeetingLayout, @@ -42,10 +78,13 @@ class ActionsBar extends PureComponent { setPresentationFitToWidth, } = this.props; - const shouldShowOptionsButton = (isPresentationEnabled() && isThereCurrentPresentation) - || isSharingVideo || hasScreenshare || isSharedNotesPinned; + const { isCaptionsReaderMenuModalOpen } = this.state; + + const shouldShowOptionsButton = (isPresentationEnabled() && isThereCurrentPresentation) + || isSharingVideo || hasScreenshare || isSharedNotesPinned; return ( {isCaptionsAvailable ? ( - + <> + + { + isCaptionsReaderMenuModalOpen ? this.setCaptionsReaderMenuModalIsOpen(false), + priority: "low", + setIsOpen: this.setCaptionsReaderMenuModalIsOpen, + isOpen: isCaptionsReaderMenuModalOpen, + }} + /> : null + } + ) : null} { !deviceInfo.isMobile @@ -94,6 +149,7 @@ class ActionsBar extends PureComponent { isMeteorConnected, }} /> + {isRaiseHandButtonCentered && this.renderRaiseHand()} { shouldShowOptionsButton ? @@ -105,19 +161,12 @@ class ActionsBar extends PureComponent { hasExternalVideo={isSharingVideo} hasScreenshare={hasScreenshare} hasPinnedSharedNotes={isSharedNotesPinned} + hasGenericContent={hasGenericContent} + hasCameraAsContent={hasCameraAsContent} /> : null } - {isRaiseHandButtonEnabled - ? ( - - ) : null} + {!isRaiseHandButtonCentered && this.renderRaiseHand()} ); diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx index 52b2ca3e3bb5..2075d3c0a0b8 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx @@ -12,9 +12,10 @@ import Service from './service'; import UserListService from '/imports/ui/components/user-list/service'; import ExternalVideoService from '/imports/ui/components/external-video-player/service'; import CaptionsService from '/imports/ui/components/captions/service'; +import TimerService from '/imports/ui/components/timer/service'; import { layoutSelectOutput, layoutDispatch } from '../layout/context'; -import { isVideoBroadcasting } from '/imports/ui/components/screenshare/service'; import { isExternalVideoEnabled, isPollingEnabled, isPresentationEnabled } from '/imports/ui/services/features'; +import { isScreenBroadcasting, isCameraAsContentBroadcasting } from '/imports/ui/components/screenshare/service'; import MediaService from '../media/service'; @@ -47,6 +48,14 @@ const ActionsBarContainer = (props) => { const SELECT_RANDOM_USER_ENABLED = Meteor.settings.public.selectRandomUser.enabled; const RAISE_HAND_BUTTON_ENABLED = Meteor.settings.public.app.raiseHandActionButton.enabled; +const RAISE_HAND_BUTTON_CENTERED = Meteor.settings.public.app.raiseHandActionButton.centered; + +const isReactionsButtonEnabled = () => { + const USER_REACTIONS_ENABLED = Meteor.settings.public.userReaction.enabled; + const REACTIONS_BUTTON_ENABLED = Meteor.settings.public.app.reactionsButton.enabled; + + return USER_REACTIONS_ENABLED && REACTIONS_BUTTON_ENABLED; +}; export default withTracker(() => ({ amIModerator: Service.amIModerator(), @@ -58,12 +67,17 @@ export default withTracker(() => ({ parseCurrentSlideContent: PresentationService.parseCurrentSlideContent, isSharingVideo: Service.isSharingVideo(), isSharedNotesPinned: Service.isSharedNotesPinned(), - hasScreenshare: isVideoBroadcasting(), + hasScreenshare: isScreenBroadcasting(), + hasCameraAsContent: isCameraAsContentBroadcasting(), isCaptionsAvailable: CaptionsService.isCaptionsAvailable(), + isTimerActive: TimerService.isActive(), + isTimerEnabled: TimerService.isEnabled(), isMeteorConnected: Meteor.status().connected, isPollingEnabled: isPollingEnabled() && isPresentationEnabled(), isSelectRandomUserEnabled: SELECT_RANDOM_USER_ENABLED, isRaiseHandButtonEnabled: RAISE_HAND_BUTTON_ENABLED, + isRaiseHandButtonCentered: RAISE_HAND_BUTTON_CENTERED, + isReactionsButtonEnabled: isReactionsButtonEnabled(), isThereCurrentPresentation: Presentations.findOne({ meetingId: Auth.meetingID, current: true }, { fields: {} }), allowExternalVideo: isExternalVideoEnabled(), diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx index 7bc212278e6b..d3a03b9722bc 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx @@ -1,18 +1,18 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl } from 'react-intl'; -import _ from 'lodash'; +import { range } from '/imports/utils/array-utils'; import deviceInfo from '/imports/utils/deviceInfo'; import Button from '/imports/ui/components/common/button/component'; import { Session } from 'meteor/session'; -import Modal from '/imports/ui/components/common/modal/fullscreen/component'; -import { withModalMounter } from '/imports/ui/components/common/modal/service'; +import ModalFullscreen from '/imports/ui/components/common/modal/fullscreen/component'; import SortList from './sort-user-list/component'; import Styled from './styles'; import Icon from '/imports/ui/components/common/icon/component'; import { isImportSharedNotesFromBreakoutRoomsEnabled, isImportPresentationWithAnnotationsFromBreakoutRoomsEnabled } from '/imports/ui/services/features'; import { addNewAlert } from '/imports/ui/components/screenreader-alert/service'; import PresentationUploaderService from '/imports/ui/components/presentation/presentation-uploader/service'; +import { uniqueId } from '/imports/utils/string-utils'; const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator; @@ -172,7 +172,15 @@ const intlMessages = defineMessages({ movedUserLabel: { id: 'app.createBreakoutRoom.movedUserLabel', description: 'screen reader alert when users are moved to rooms', - } + }, + manageRooms: { + id: 'app.createBreakoutRoom.manageRoomsLabel', + description: 'Label for manage rooms', + }, + sendInvitationToMods: { + id: 'app.createBreakoutRoom.sendInvitationToMods', + description: 'label for checkbox send invitation to moderators', + }, }); const BREAKOUT_LIM = Meteor.settings.public.app.breakouts.breakoutRoomLimit; @@ -191,7 +199,6 @@ const propTypes = { getUsersNotJoined: PropTypes.func.isRequired, getBreakouts: PropTypes.func.isRequired, sendInvitation: PropTypes.func.isRequired, - mountModal: PropTypes.func.isRequired, isBreakoutRecordable: PropTypes.bool, }; @@ -200,7 +207,7 @@ const setPresentationVisibility = (state) => { if (presentationInnerWrapper) { presentationInnerWrapper.style.display = state; } -} +}; class BreakoutRoom extends PureComponent { constructor(props) { @@ -220,7 +227,7 @@ class BreakoutRoom extends PureComponent { this.renderUserItemByRoom = this.renderUserItemByRoom.bind(this); this.renderRoomsGrid = this.renderRoomsGrid.bind(this); this.renderBreakoutForm = this.renderBreakoutForm.bind(this); - this.renderCheckboxes = this.renderCheckboxes.bind(this); + this.renderCheckboxes = this.renderUnassingUsers.bind(this); this.renderRoomSortList = this.renderRoomSortList.bind(this); this.renderDesktop = this.renderDesktop.bind(this); this.renderMobile = this.renderMobile.bind(this); @@ -231,6 +238,7 @@ class BreakoutRoom extends PureComponent { this.setInvitationConfig = this.setInvitationConfig.bind(this); this.setRecord = this.setRecord.bind(this); this.setCaptureNotes = this.setCaptureNotes.bind(this); + this.setInviteMods = this.setInviteMods.bind(this); this.setCaptureSlides = this.setCaptureSlides.bind(this); this.blurDurationTime = this.blurDurationTime.bind(this); this.removeRoomUsers = this.removeRoomUsers.bind(this); @@ -247,18 +255,19 @@ class BreakoutRoom extends PureComponent { roomNamesChanged: [], roomSelected: 0, preventClosing: true, - leastOneUserIsValid: true, + leastOneUserIsValid: null, numberOfRoomsIsValid: true, roomNameDuplicatedIsValid: true, roomNameEmptyIsValid: true, record: false, captureNotes: false, + inviteMods: false, captureSlides: false, durationIsValid: true, breakoutJoinedUsers: null, }; - this.btnLevelId = _.uniqueId('btn-set-level-'); + this.btnLevelId = uniqueId('btn-set-level-'); this.handleMoveEvent = this.handleMoveEvent.bind(this); this.handleShiftUser = this.handleShiftUser.bind(this); @@ -268,7 +277,7 @@ class BreakoutRoom extends PureComponent { const { breakoutJoinedUsers, getLastBreakouts, groups, isUpdate, allowUserChooseRoomByDefault, captureSharedNotesByDefault, - captureWhiteboardByDefault, + captureWhiteboardByDefault, inviteModsByDefault, } = this.props; setPresentationVisibility('none'); this.setRoomUsers(); @@ -299,6 +308,7 @@ class BreakoutRoom extends PureComponent { freeJoin: allowUserChooseRoomByDefault, captureSlides: captureWhiteboardByDefault, captureNotes: captureSharedNotesByDefault, + inviteMods: inviteModsByDefault, }); const lastBreakouts = getLastBreakouts(); @@ -318,6 +328,11 @@ class BreakoutRoom extends PureComponent { } } + const unassignedUsers = document.getElementById('breakoutBox-0'); + if (unassignedUsers) { + unassignedUsers.addEventListener('keydown', this.handleMoveEvent, true); + } + const { numberOfRooms } = this.state; const { users } = this.props; const { users: prevUsers } = prevProps; @@ -342,6 +357,10 @@ class BreakoutRoom extends PureComponent { roomList.removeEventListener('keydown', this.handleMoveEvent, true); } } + const unassignedUsers = document.getElementById('breakoutBox-0'); + if (unassignedUsers) { + unassignedUsers.removeEventListener('keydown', this.handleMoveEvent, true); + } } handleShiftUser(activeListSibling) { @@ -355,6 +374,12 @@ class BreakoutRoom extends PureComponent { this.changeUserRoom(u.userId, users[index].room); } }); + } else { + users.forEach((u, index) => { + if (`roomUserItem-${u.userId}` === document.activeElement.id) { + this.changeUserRoom(u.userId, 0); + } + }); } } @@ -410,10 +435,10 @@ class BreakoutRoom extends PureComponent { } handleDismiss() { - const { mountModal } = this.props; + const { setIsOpen } = this.props; setPresentationVisibility('block'); return new Promise((resolve) => { - mountModal(null); + setIsOpen(false); this.setState({ preventClosing: false, @@ -436,6 +461,7 @@ class BreakoutRoom extends PureComponent { numberOfRooms, durationTime, durationIsValid, + inviteMods, } = this.state; if ((durationTime || 0) < MIN_BREAKOUT_TIME) { @@ -452,13 +478,13 @@ class BreakoutRoom extends PureComponent { return; } - const duplicatedNames = _.range(1, numberOfRooms + 1).filter((n) => this.hasNameDuplicated(n)); + const duplicatedNames = range(1, numberOfRooms + 1).filter((n) => this.hasNameDuplicated(n)); if (duplicatedNames.length > 0) { this.setState({ roomNameDuplicatedIsValid: false }); return; } - const emptyNames = _.range(1, numberOfRooms + 1) + const emptyNames = range(1, numberOfRooms + 1) .filter((n) => this.getRoomName(n).length === 0); if (emptyNames.length > 0) { this.setState({ roomNameEmptyIsValid: false }); @@ -467,7 +493,7 @@ class BreakoutRoom extends PureComponent { this.handleDismiss(); - const rooms = _.range(1, numberOfRooms + 1).map((seq) => ({ + const rooms = range(1, numberOfRooms + 1).map((seq) => ({ users: this.getUserByRoom(seq).map((u) => u.userId), name: this.getFullName(seq), captureNotesFilename: this.getCaptureFilename(seq, false), @@ -478,7 +504,7 @@ class BreakoutRoom extends PureComponent { sequence: seq, })); - createBreakoutRoom(rooms, durationTime, record, captureNotes, captureSlides); + createBreakoutRoom(rooms, durationTime, record, captureNotes, captureSlides, inviteMods); Session.set('isUserListOpen', true); } @@ -549,6 +575,7 @@ class BreakoutRoom extends PureComponent { onAssignRandomly() { const { numberOfRooms } = this.state; + this.setState({ hasUsersAssign: false }); const { users } = this.state; // We only want to assign viewers so filter out the moderators. We also want to get // all users each run so that clicking the button again will reshuffle @@ -569,6 +596,7 @@ class BreakoutRoom extends PureComponent { onAssignReset() { const { users } = this.state; + this.setState({ hasUsersAssign: true }); users.forEach((u) => { if (u.room !== null && u.room > 0) { @@ -620,6 +648,10 @@ class BreakoutRoom extends PureComponent { this.setState({ captureNotes: e.target.checked }); } + setInviteMods(e) { + this.setState({ inviteMods: e.target.checked }); + } + setCaptureSlides(e) { this.setState({ captureSlides: e.target.checked }); } @@ -662,10 +694,10 @@ class BreakoutRoom extends PureComponent { ? intl.formatMessage(intlMessages.captureSlidesType) : intl.formatMessage(intlMessages.captureNotesType); - const fileName = `${this.getRoomName(position,true)}_${captureType}`.replace(/ /g, '_'); + const fileName = `${this.getRoomName(position, true)}_${captureType}`.replace(/ /g, '_'); const fileNameDuplicatedCount = presentations.filter((pres) => pres.filename?.startsWith(fileName) - || pres.name?.startsWith(fileName)).length; + || pres.name?.startsWith(fileName)).length; return fileNameDuplicatedCount === 0 ? fileName : `${fileName}(${fileNameDuplicatedCount + 1})`; } @@ -761,7 +793,7 @@ class BreakoutRoom extends PureComponent { hasNameDuplicated(position) { const { numberOfRooms } = this.state; const currName = this.getRoomName(position).trim(); - const equals = _.range(1, numberOfRooms + 1) + const equals = range(1, numberOfRooms + 1) .filter((n) => this.getRoomName(n).trim() === currName); if (equals.length > 1) return true; @@ -784,7 +816,7 @@ class BreakoutRoom extends PureComponent { roomNameDuplicatedIsValid: true, roomNameEmptyIsValid: true, }, () => { - const rooms = _.range(1, lastBreakouts.length + 1).map((seq) => this.getRoomName(seq)); + const rooms = range(1, lastBreakouts.length + 1).map((seq) => this.getRoomName(seq)); users.forEach((u) => { const lastUserBreakout = getBreakoutUserWasIn(u.userId, u.extId); @@ -830,6 +862,43 @@ class BreakoutRoom extends PureComponent { }); } + renderContent() { + const { intl } = this.props; + const { + leastOneUserIsValid, + allowDrop, + } = this.state; + const drop = (room) => (ev) => { + ev.preventDefault(); + const data = ev.dataTransfer.getData('text'); + this.changeUserRoom(data, room); + this.setState({ selectedId: '' }); + }; + + return ( + + + + + + + {this.renderUserItemByRoom(0)} + + + {intl.formatMessage(intlMessages.leastOneWarnBreakout)} + + + {this.renderRoomsGrid()} + + ); + } + renderRoomsGrid() { const { intl, isUpdate } = this.props; const { @@ -865,25 +934,8 @@ class BreakoutRoom extends PureComponent { return ( { this.listOfUsers = r; }} data-test="roomGrid"> - - - - - - {this.renderUserItemByRoom(0)} - - - {intl.formatMessage(intlMessages.leastOneWarnBreakout)} - - { - _.range(1, rooms + 1).map((value) => ( + range(1, rooms + 1).map((value) => (
{ - _.range(MIN_BREAKOUT_ROOMS, MAX_BREAKOUT_ROOMS + 1).map((item) => ()) + range(MIN_BREAKOUT_ROOMS, MAX_BREAKOUT_ROOMS + 1).map((item) => ()) }
@@ -969,44 +1027,8 @@ class BreakoutRoom extends PureComponent { aria-label={intl.formatMessage(intlMessages.duration)} data-test="durationTime" /> - -