Merge remote-tracking branch 'upstream/v3.0.x-release' into v3.0.x-release
This commit is contained in:
commit
c2474ae607
2
.github/actions/merge-branches/action.yml
vendored
2
.github/actions/merge-branches/action.yml
vendored
@ -6,7 +6,7 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Checkout ${{ github.event.pull_request.base.ref || 'master' }}
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.ref || '' }}
|
||||
fetch-depth: 0 # Fetch all history
|
||||
|
106
.github/workflows/automated-tests.yml
vendored
106
.github/workflows/automated-tests.yml
vendored
@ -75,7 +75,7 @@ jobs:
|
||||
- package: others
|
||||
build-list: bbb-mkclean bbb-pads bbb-libreoffice-docker bbb-transcription-controller bigbluebutton bbb-livekit
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Merge branches
|
||||
uses: ./.github/actions/merge-branches
|
||||
- name: Set cache-key vars
|
||||
@ -90,7 +90,7 @@ jobs:
|
||||
- name: Handle cache
|
||||
if: matrix.cache-files-list != ''
|
||||
id: cache-action
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: artifacts.tar
|
||||
key: ${{ runner.os }}-${{ matrix.package }}-${{ env.BIGBLUEBUTTON_RELEASE }}-commits-${{ env.CACHE_KEY_FILES }}-urls-${{ env.CACHE_KEY_URLS }}
|
||||
@ -102,7 +102,7 @@ jobs:
|
||||
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
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: artifacts_${{ matrix.package }}.tar
|
||||
path: artifacts.tar
|
||||
@ -112,74 +112,76 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1/8, 2/8, 3/8, 4/8, 5/8, 6/8, 7/8, 8/8]
|
||||
shard: [1, 2, 3, 4, 5, 6, 7, 8]
|
||||
env:
|
||||
shard: ${{ matrix.shard }}/8
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- 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
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: artifacts_bbb-apps-akka.tar
|
||||
- run: tar xf artifacts.tar
|
||||
- name: Download artifacts_bbb-config
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: artifacts_bbb-config.tar
|
||||
- run: tar xf artifacts.tar
|
||||
- name: Download artifacts_bbb-export-annotations
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: artifacts_bbb-export-annotations.tar
|
||||
- run: tar xf artifacts.tar
|
||||
- name: Download artifacts_bbb-learning-dashboard
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: artifacts_bbb-learning-dashboard.tar
|
||||
- run: tar xf artifacts.tar
|
||||
- name: Download artifacts_bbb-playback-record
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: artifacts_bbb-playback-record.tar
|
||||
- run: tar xf artifacts.tar
|
||||
- name: Download artifacts_bbb-graphql-server
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: artifacts_bbb-graphql-server.tar
|
||||
- run: tar xf artifacts.tar
|
||||
- name: Download artifacts_bbb-etherpad
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: artifacts_bbb-etherpad.tar
|
||||
- run: tar xf artifacts.tar
|
||||
- name: Download artifacts_bbb-freeswitch
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: artifacts_bbb-freeswitch.tar
|
||||
- run: tar xf artifacts.tar
|
||||
- name: Download artifacts_bbb-webrtc
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: artifacts_bbb-webrtc.tar
|
||||
- run: tar xf artifacts.tar
|
||||
- name: Download artifacts_bbb-web
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: artifacts_bbb-web.tar
|
||||
- run: tar xf artifacts.tar
|
||||
- name: Download artifacts_bbb-fsesl-akka
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: artifacts_bbb-fsesl-akka.tar
|
||||
- run: tar xf artifacts.tar
|
||||
- name: Download artifacts_bbb-html5
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: artifacts_bbb-html5.tar
|
||||
- run: tar xf artifacts.tar
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: artifacts_others.tar
|
||||
- run: tar xf artifacts.tar
|
||||
@ -253,17 +255,20 @@ jobs:
|
||||
apt --purge -y remove apache2-bin
|
||||
'
|
||||
- name: Install BBB
|
||||
timeout-minutes: 25
|
||||
run: |
|
||||
sudo -i <<EOF
|
||||
set -e
|
||||
cd /root/ && wget -nv https://raw.githubusercontent.com/bigbluebutton/bbb-install/v3.0.x-release/bbb-install.sh -O bbb-install.sh
|
||||
cat bbb-install.sh | sed "s|> /etc/apt/sources.list.d/bigbluebutton.list||g" | bash -s -- -v jammy-30-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
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 25
|
||||
max_attempts: 2
|
||||
command: |
|
||||
sudo -i <<EOF
|
||||
set -e
|
||||
cd /root/ && wget -nv https://raw.githubusercontent.com/bigbluebutton/bbb-install/v3.0.x-release/bbb-install.sh -O bbb-install.sh
|
||||
cat bbb-install.sh | sed "s|> /etc/apt/sources.list.d/bigbluebutton.list||g" | bash -s -- -v jammy-30-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: List systemctl services
|
||||
timeout-minutes: 1
|
||||
run: |
|
||||
@ -279,13 +284,13 @@ jobs:
|
||||
npx playwright install
|
||||
'
|
||||
- name: Run tests
|
||||
uses: nick-fields/retry@v2
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 25
|
||||
max_attempts: 3
|
||||
max_attempts: 2
|
||||
command: |
|
||||
cd ./bigbluebutton-tests/playwright
|
||||
npm run test-chromium-ci -- --shard ${{ matrix.shard }}
|
||||
npm run test-chromium-ci -- --shard ${{ env.shard }}
|
||||
env:
|
||||
NODE_EXTRA_CA_CERTS: /usr/local/share/ca-certificates/bbb-dev/bbb-dev-ca.crt
|
||||
ACTIONS_RUNNER_DEBUG: true
|
||||
@ -305,13 +310,13 @@ 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 -- --shard ${{ matrix.shard }}
|
||||
npm run test-firefox-ci -- --shard ${{ env.shard }}
|
||||
'
|
||||
- if: always() && github.event_name == 'pull_request'
|
||||
name: Upload blob report to GitHub Actions Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: all-blob-reports
|
||||
name: blob-report-${{ matrix.shard }}
|
||||
path: bigbluebutton-tests/playwright/blob-report
|
||||
- if: failure()
|
||||
name: Prepare artifacts (configs and logs)
|
||||
@ -337,42 +342,55 @@ jobs:
|
||||
chmod a+r -R /home/runner/work/bigbluebutton/bigbluebutton/configs
|
||||
bbb-conf --zip
|
||||
ls -t /root/*.tar.gz | head -1 | xargs -I '{}' cp '{}' /home/runner/work/bigbluebutton/bigbluebutton/bbb-logs.tar.gz
|
||||
echo "MATRIX_SHARD=$(echo ${{ matrix.shard }} | tr '/' '_')" >> $GITHUB_ENV
|
||||
echo "MATRIX_SHARD=${{ matrix.shard }}_8" >> $GITHUB_ENV
|
||||
EOF
|
||||
- if: failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bbb-configs-${{ env.MATRIX_SHARD }}
|
||||
path: configs
|
||||
- if: failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bbb-logs-${{ env.MATRIX_SHARD }}
|
||||
path: ./bbb-logs.tar.gz
|
||||
upload-report:
|
||||
if: always()
|
||||
if: always() && !contains(github.event.head_commit.message, 'Merge pull request')
|
||||
needs: install-and-run-tests
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
hasReportData: ${{ needs.install-and-run-tests.result == 'success' || needs.install-and-run-tests.result == 'failure' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install dependencies
|
||||
if: ${{ env.hasReportData }}
|
||||
working-directory: ./bigbluebutton-tests/playwright
|
||||
run: npm ci
|
||||
- name: Merge artifacts
|
||||
uses: actions/upload-artifact/merge@v4
|
||||
with:
|
||||
name: all-blob-reports
|
||||
pattern: blob-report-*
|
||||
delete-merged: true
|
||||
- name: Download all blob reports from GitHub Actions Artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
if: ${{ env.hasReportData }}
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: all-blob-reports
|
||||
path: bigbluebutton-tests/playwright/all-blob-reports
|
||||
- name: Merge into HTML Report
|
||||
if: ${{ env.hasReportData }}
|
||||
working-directory: ./bigbluebutton-tests/playwright
|
||||
run: npx playwright merge-reports --reporter html ./all-blob-reports
|
||||
- name: Upload HTML tests report
|
||||
uses: actions/upload-artifact@v3
|
||||
if: ${{ env.hasReportData }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tests-report
|
||||
overwrite: true
|
||||
path: |
|
||||
bigbluebutton-tests/playwright/playwright-report
|
||||
bigbluebutton-tests/playwright/test-results
|
||||
@ -385,7 +403,7 @@ jobs:
|
||||
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
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pr-comment-data
|
||||
path: pr-comment-data
|
||||
|
2
.github/workflows/check-merge-conflict.yml
vendored
2
.github/workflows/check-merge-conflict.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check for dirty pull requests
|
||||
uses: eps1lon/actions-label-merge-conflict@releases/2.x
|
||||
uses: eps1lon/actions-label-merge-conflict@v3
|
||||
with:
|
||||
dirtyLabel: "status: conflict"
|
||||
repoToken: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
4
.github/workflows/deploy-docs.yml
vendored
4
.github/workflows/deploy-docs.yml
vendored
@ -22,8 +22,8 @@ jobs:
|
||||
working-directory: ./docs
|
||||
steps:
|
||||
# Setup
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
cache: yarn
|
||||
|
2
.github/workflows/ts-code-compilation.yml
vendored
2
.github/workflows/ts-code-compilation.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
||||
ts-code-compilation:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Merge branches
|
||||
|
2
.github/workflows/ts-code-validation.yml
vendored
2
.github/workflows/ts-code-validation.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
||||
ts-code-validation:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Merge branches
|
||||
|
@ -5,7 +5,7 @@ import org.slf4j.LoggerFactory
|
||||
|
||||
import java.io.{ ByteArrayInputStream, File }
|
||||
import scala.io.BufferedSource
|
||||
import scala.util.{ Failure, Success, Try }
|
||||
import scala.util.{ Failure, Success }
|
||||
|
||||
object ClientSettings extends SystemConfiguration {
|
||||
var clientSettingsFromFile: Map[String, Object] = Map("" -> "")
|
||||
@ -82,6 +82,24 @@ object ClientSettings extends SystemConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
def getConfigPropertyValueByPathAsListOfStringOrElse(map: Map[String, Any], path: String, alternativeValue: List[String]): List[String] = {
|
||||
getConfigPropertyValueByPath(map, path) match {
|
||||
case Some(configValue: List[String]) => configValue
|
||||
case _ =>
|
||||
logger.debug(s"Config `$path` with type List[String] not found in clientSettings.")
|
||||
alternativeValue
|
||||
}
|
||||
}
|
||||
|
||||
def getConfigPropertyValueByPathAsListOfIntOrElse(map: Map[String, Any], path: String, alternativeValue: List[Int]): List[Int] = {
|
||||
getConfigPropertyValueByPath(map, path) match {
|
||||
case Some(configValue: List[Int]) => configValue
|
||||
case _ =>
|
||||
logger.debug(s"Config `$path` with type List[Int] not found in clientSettings.")
|
||||
alternativeValue
|
||||
}
|
||||
}
|
||||
|
||||
def getConfigPropertyValueByPath(map: Map[String, Any], path: String): Option[Any] = {
|
||||
val keys = path.split("\\.")
|
||||
|
||||
|
@ -138,10 +138,3 @@ case class UserClosedAllGraphqlConnectionsInternalMsg(userId: String) extends In
|
||||
* @param userId
|
||||
*/
|
||||
case class UserEstablishedGraphqlConnectionInternalMsg(userId: String) extends InMessage
|
||||
|
||||
// DeskShare
|
||||
case class DeskShareStartedRequest(conferenceName: String, callerId: String, callerIdName: String) extends InMessage
|
||||
case class DeskShareStoppedRequest(conferenceName: String, callerId: String, callerIdName: String) extends InMessage
|
||||
case class DeskShareRTMPBroadcastStartedRequest(conferenceName: String, streamname: String, videoWidth: Int, videoHeight: Int, timestamp: String) extends InMessage
|
||||
case class DeskShareRTMPBroadcastStoppedRequest(conferenceName: String, streamname: String, videoWidth: Int, videoHeight: Int, timestamp: String) extends InMessage
|
||||
case class DeskShareGetDeskShareInfoRequest(conferenceName: String, requesterID: String, replyTo: String) extends InMessage
|
||||
|
@ -70,7 +70,6 @@ class WhiteboardModel extends SystemConfiguration {
|
||||
val newAnnotation = oldAnnotation.get.copy(annotationInfo = finalAnnotationInfo)
|
||||
newAnnotationsMap += (annotation.id -> newAnnotation)
|
||||
annotationsAdded :+= newAnnotation
|
||||
PresAnnotationDAO.insertOrUpdate(newAnnotation, newAnnotation)
|
||||
println(s"Updated annotation on page [${wb.id}]. After numAnnotations=[${newAnnotationsMap.size}].")
|
||||
} else {
|
||||
println(s"User $userId doesn't have permission to edit annotation ${annotation.id}, ignoring...")
|
||||
@ -78,13 +77,14 @@ class WhiteboardModel extends SystemConfiguration {
|
||||
} else if (annotation.annotationInfo.contains("type")) {
|
||||
newAnnotationsMap += (annotation.id -> annotation)
|
||||
annotationsAdded :+= annotation
|
||||
PresAnnotationDAO.insertOrUpdate(annotation, annotation)
|
||||
println(s"Adding annotation to page [${wb.id}]. After numAnnotations=[${newAnnotationsMap.size}].")
|
||||
} else {
|
||||
println(s"New annotation [${annotation.id}] with no type, ignoring...")
|
||||
}
|
||||
}
|
||||
|
||||
PresAnnotationDAO.insertOrUpdateMap(newAnnotationsMap)
|
||||
|
||||
val newWb = wb.copy(annotationsMap = newAnnotationsMap)
|
||||
saveWhiteboard(newWb)
|
||||
annotationsAdded
|
||||
@ -143,7 +143,7 @@ class WhiteboardModel extends SystemConfiguration {
|
||||
val updatedWb = wb.copy(annotationsMap = newAnnotationsMap)
|
||||
saveWhiteboard(updatedWb)
|
||||
|
||||
annotationsIdsRemoved.map(PresAnnotationDAO.delete(wbId, userId, _))
|
||||
PresAnnotationDAO.delete(annotationsIdsRemoved)
|
||||
|
||||
annotationsIdsRemoved
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import org.apache.pekko.actor.ActorContext
|
||||
|
||||
class AudioCaptionsApp2x(implicit val context: ActorContext)
|
||||
extends UpdateTranscriptPubMsgHdlr
|
||||
with TranscriptionProviderErrorMsgHdlr
|
||||
with AudioFloorChangedVoiceConfEvtMsgHdlr {
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,31 @@
|
||||
package org.bigbluebutton.core.apps.audiocaptions
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.db.UserTranscriptionErrorDAO
|
||||
import org.bigbluebutton.core.models.AudioCaptions
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
|
||||
trait TranscriptionProviderErrorMsgHdlr {
|
||||
this: AudioCaptionsApp2x =>
|
||||
|
||||
def handleTranscriptionProviderErrorMsg(msg: TranscriptionProviderErrorMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
|
||||
def broadcastEvent(userId: String, errorCode: String, errorMessage: String): Unit = {
|
||||
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, "nodeJSapp")
|
||||
val envelope = BbbCoreEnvelope(TranscriptionProviderErrorEvtMsg.NAME, routing)
|
||||
val header = BbbClientMsgHeader(TranscriptionProviderErrorEvtMsg.NAME, meetingId, userId)
|
||||
val body = TranscriptionProviderErrorEvtMsgBody(errorCode, errorMessage)
|
||||
val event = TranscriptionProviderErrorEvtMsg(header, body)
|
||||
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
|
||||
bus.outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
broadcastEvent(msg.header.userId, msg.body.errorCode, msg.body.errorMessage)
|
||||
|
||||
UserTranscriptionErrorDAO.insert(msg.header.userId, msg.header.meetingId, msg.body.errorCode, msg.body.errorMessage)
|
||||
|
||||
}
|
||||
}
|
@ -1,12 +1,13 @@
|
||||
package org.bigbluebutton.core.apps.audiocaptions
|
||||
|
||||
import org.bigbluebutton.ClientSettings.getConfigPropertyValueByPathAsStringOrElse
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.db.CaptionDAO
|
||||
import org.bigbluebutton.core.models.{AudioCaptions, Users2x}
|
||||
import org.bigbluebutton.core.models.{AudioCaptions, UserState, Users2x}
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
|
||||
import java.sql.Timestamp
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
trait UpdateTranscriptPubMsgHdlr {
|
||||
this: AudioCaptionsApp2x =>
|
||||
@ -25,6 +26,17 @@ trait UpdateTranscriptPubMsgHdlr {
|
||||
bus.outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
def sendPadUpdatePubMsg(userId: String, defaultPad: String, text: String, transcript: Boolean): Unit = {
|
||||
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, "nodeJSapp")
|
||||
val envelope = BbbCoreEnvelope(PadUpdatePubMsg.NAME, routing)
|
||||
val header = BbbClientMsgHeader(PadUpdatePubMsg.NAME, meetingId, userId)
|
||||
val body = PadUpdatePubMsgBody(defaultPad, text, transcript)
|
||||
val event = PadUpdatePubMsg(header, body)
|
||||
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
|
||||
bus.outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
// Adapt to the current captions' recording process
|
||||
def editTranscript(
|
||||
userId: String,
|
||||
@ -80,6 +92,28 @@ trait UpdateTranscriptPubMsgHdlr {
|
||||
msg.body.locale,
|
||||
msg.body.result,
|
||||
)
|
||||
|
||||
if(msg.body.result) {
|
||||
val userName = Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId).get match {
|
||||
case u: UserState => u.name
|
||||
case _ => "???"
|
||||
}
|
||||
|
||||
val now = LocalDateTime.now()
|
||||
val formatter = DateTimeFormatter.ofPattern("HH:mm:ss")
|
||||
val formattedTime = now.format(formatter)
|
||||
|
||||
val userSpoke = s"\n $userName ($formattedTime): $transcript"
|
||||
|
||||
val defaultPad = getConfigPropertyValueByPathAsStringOrElse(
|
||||
liveMeeting.clientSettings,
|
||||
"public.captions.defaultPad",
|
||||
alternativeValue = ""
|
||||
)
|
||||
|
||||
sendPadUpdatePubMsg(msg.header.userId, defaultPad, userSpoke, transcript = true)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package org.bigbluebutton.core.apps.breakout
|
||||
|
||||
import org.bigbluebutton.core.api.BreakoutRoomEndedInternalMsg
|
||||
import org.bigbluebutton.core.db.BreakoutRoomDAO
|
||||
import org.bigbluebutton.core.db.{ BreakoutRoomDAO, NotificationDAO }
|
||||
import org.bigbluebutton.core.domain.MeetingState2x
|
||||
import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter }
|
||||
import org.bigbluebutton.core2.message.senders.MsgBuilder
|
||||
@ -37,6 +37,7 @@ trait BreakoutRoomEndedInternalMsgHdlr {
|
||||
Vector()
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
|
||||
BreakoutRoomDAO.updateRoomsEnded(liveMeeting.props.meetingProp.intId)
|
||||
state.update(None)
|
||||
|
@ -5,7 +5,7 @@ import org.bigbluebutton.core.api.EjectUserFromBreakoutInternalMsg
|
||||
import org.bigbluebutton.core.apps.breakout.BreakoutHdlrHelpers.getRedirectUrls
|
||||
import org.bigbluebutton.core.apps.{PermissionCheck, RightsManagementTrait}
|
||||
import org.bigbluebutton.core.bus.BigBlueButtonEvent
|
||||
import org.bigbluebutton.core.db.BreakoutRoomUserDAO
|
||||
import org.bigbluebutton.core.db.{BreakoutRoomUserDAO, NotificationDAO}
|
||||
import org.bigbluebutton.core.domain.MeetingState2x
|
||||
import org.bigbluebutton.core.models.EjectReasonCode
|
||||
import org.bigbluebutton.core.running.{MeetingActor, OutMsgRouter}
|
||||
@ -75,6 +75,7 @@ trait ChangeUserBreakoutReqMsgHdlr extends RightsManagementTrait {
|
||||
Vector(roomTo.shortName)
|
||||
)
|
||||
outGW.send(notifyUserEvent)
|
||||
NotificationDAO.insert(notifyUserEvent)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,9 +6,8 @@ import org.bigbluebutton.core.bus.BigBlueButtonEvent
|
||||
import org.bigbluebutton.core.domain.{ MeetingEndReason, MeetingState2x }
|
||||
import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter }
|
||||
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
|
||||
import org.bigbluebutton.core.db.UserBreakoutRoomDAO
|
||||
import org.bigbluebutton.core.db.{ BreakoutRoomDAO, NotificationDAO, UserBreakoutRoomDAO }
|
||||
import org.bigbluebutton.core2.message.senders.MsgBuilder
|
||||
import org.bigbluebutton.core.db.BreakoutRoomDAO
|
||||
|
||||
trait EndAllBreakoutRoomsMsgHdlr extends RightsManagementTrait {
|
||||
this: MeetingActor =>
|
||||
@ -39,6 +38,7 @@ trait EndAllBreakoutRoomsMsgHdlr extends RightsManagementTrait {
|
||||
Vector()
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
}
|
||||
BreakoutRoomDAO.updateRoomsEnded(meetingId)
|
||||
state.update(None)
|
||||
|
@ -1,13 +1,14 @@
|
||||
package org.bigbluebutton.core.apps.breakout
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.api.{ SendMessageToBreakoutRoomInternalMsg }
|
||||
import org.bigbluebutton.core.api.SendMessageToBreakoutRoomInternalMsg
|
||||
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
|
||||
import org.bigbluebutton.core.bus.BigBlueButtonEvent
|
||||
import org.bigbluebutton.core.db.NotificationDAO
|
||||
import org.bigbluebutton.core.domain.MeetingState2x
|
||||
import org.bigbluebutton.core.models.{ RegisteredUsers }
|
||||
import org.bigbluebutton.core.models.RegisteredUsers
|
||||
import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter }
|
||||
import org.bigbluebutton.core2.message.senders.{ MsgBuilder }
|
||||
import org.bigbluebutton.core2.message.senders.MsgBuilder
|
||||
|
||||
trait SendMessageToAllBreakoutRoomsMsgHdlr extends RightsManagementTrait {
|
||||
this: MeetingActor =>
|
||||
@ -32,7 +33,7 @@ trait SendMessageToAllBreakoutRoomsMsgHdlr extends RightsManagementTrait {
|
||||
val event = buildSendMessageToAllBreakoutRoomsEvtMsg(msg.header.userId, msg.body.msg, breakoutModel.rooms.size)
|
||||
outGW.send(event)
|
||||
|
||||
val notifyModeratorEvent = MsgBuilder.buildNotifyUserInMeetingEvtMsg(
|
||||
val notifyUserEvent = MsgBuilder.buildNotifyUserInMeetingEvtMsg(
|
||||
msg.header.userId,
|
||||
liveMeeting.props.meetingProp.intId,
|
||||
"info",
|
||||
@ -41,7 +42,8 @@ trait SendMessageToAllBreakoutRoomsMsgHdlr extends RightsManagementTrait {
|
||||
"Message for chat sent successfully",
|
||||
Vector(s"${breakoutModel.rooms.size}")
|
||||
)
|
||||
outGW.send(notifyModeratorEvent)
|
||||
outGW.send(notifyUserEvent)
|
||||
NotificationDAO.insert(notifyUserEvent)
|
||||
|
||||
log.debug("Sending message '{}' to all breakout rooms in meeting {}", msg.body.msg, props.meetingProp.intId)
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.api.{ SendTimeRemainingAuditInternalMsg, UpdateBreakoutRoomTimeInternalMsg }
|
||||
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
|
||||
import org.bigbluebutton.core.bus.BigBlueButtonEvent
|
||||
import org.bigbluebutton.core.db.{ BreakoutRoomDAO, MeetingDAO }
|
||||
import org.bigbluebutton.core.db.{ BreakoutRoomDAO, MeetingDAO, NotificationDAO }
|
||||
import org.bigbluebutton.core.domain.MeetingState2x
|
||||
import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter }
|
||||
import org.bigbluebutton.core2.message.senders.{ MsgBuilder, Sender }
|
||||
@ -63,9 +63,10 @@ trait UpdateBreakoutRoomsTimeMsgHdlr extends RightsManagementTrait {
|
||||
Vector(s"${msg.body.timeInMinutes}")
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
}
|
||||
|
||||
val notifyModeratorEvent = MsgBuilder.buildNotifyUserInMeetingEvtMsg(
|
||||
val notifyUserEvent = MsgBuilder.buildNotifyUserInMeetingEvtMsg(
|
||||
msg.header.userId,
|
||||
liveMeeting.props.meetingProp.intId,
|
||||
"info",
|
||||
@ -74,7 +75,8 @@ trait UpdateBreakoutRoomsTimeMsgHdlr extends RightsManagementTrait {
|
||||
"Sent to the moderator that requested breakout duration change",
|
||||
Vector(s"${msg.body.timeInMinutes}")
|
||||
)
|
||||
outGW.send(notifyModeratorEvent)
|
||||
outGW.send(notifyUserEvent)
|
||||
NotificationDAO.insert(notifyUserEvent)
|
||||
|
||||
log.debug("Updating {} minutes for breakout rooms time in meeting {}", msg.body.timeInMinutes, props.meetingProp.intId)
|
||||
BreakoutRoomDAO.updateRoomsDuration(props.meetingProp.intId, newDurationInSeconds)
|
||||
|
@ -5,8 +5,8 @@ import org.bigbluebutton.core.models.{ Layouts, LayoutsType }
|
||||
import org.bigbluebutton.core.running.OutMsgRouter
|
||||
import org.bigbluebutton.core2.MeetingStatus2x
|
||||
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
|
||||
import org.bigbluebutton.core.db.LayoutDAO
|
||||
import org.bigbluebutton.core2.message.senders.{ MsgBuilder }
|
||||
import org.bigbluebutton.core.db.{ LayoutDAO, NotificationDAO }
|
||||
import org.bigbluebutton.core2.message.senders.MsgBuilder
|
||||
|
||||
trait BroadcastLayoutMsgHdlr extends RightsManagementTrait {
|
||||
this: LayoutApp2x =>
|
||||
@ -73,6 +73,7 @@ trait BroadcastLayoutMsgHdlr extends RightsManagementTrait {
|
||||
Vector()
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ trait PadUpdatePubMsgHdlr {
|
||||
bus.outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
if (Pads.hasAccess(liveMeeting, msg.body.externalId, msg.header.userId)) {
|
||||
if (Pads.hasAccess(liveMeeting, msg.body.externalId, msg.header.userId) || msg.body.transcript == true) {
|
||||
Pads.getGroup(liveMeeting.pads, msg.body.externalId) match {
|
||||
case Some(group) => broadcastEvent(group.groupId, msg.body.externalId, msg.body.text)
|
||||
case _ =>
|
||||
|
@ -7,8 +7,8 @@ import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.domain.MeetingState2x
|
||||
import org.bigbluebutton.core.models.Polls
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
|
||||
import org.bigbluebutton.core.db.{ ChatMessageDAO, JsonUtils }
|
||||
import org.bigbluebutton.core.apps.{PermissionCheck, RightsManagementTrait}
|
||||
import org.bigbluebutton.core.db.{ChatMessageDAO, JsonUtils, NotificationDAO}
|
||||
import org.bigbluebutton.core2.message.senders.MsgBuilder
|
||||
import spray.json.DefaultJsonProtocol.jsonFormat2
|
||||
|
||||
@ -37,6 +37,7 @@ trait ShowPollResultReqMsgHdlr extends RightsManagementTrait {
|
||||
Vector()
|
||||
)
|
||||
bus.outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
|
||||
// SendWhiteboardAnnotationPubMsg
|
||||
val annotationRouting = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, liveMeeting.props.meetingProp.intId, msg.header.userId)
|
||||
|
@ -3,7 +3,7 @@ package org.bigbluebutton.core.apps.users
|
||||
import org.bigbluebutton.LockSettingsUtil
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
|
||||
import org.bigbluebutton.core.db.MeetingLockSettingsDAO
|
||||
import org.bigbluebutton.core.db.{ MeetingLockSettingsDAO, NotificationDAO }
|
||||
import org.bigbluebutton.core.models._
|
||||
import org.bigbluebutton.core.running.OutMsgRouter
|
||||
import org.bigbluebutton.core.running.MeetingActor
|
||||
@ -66,6 +66,7 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
|
||||
Vector()
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
|
||||
LockSettingsUtil.enforceCamLockSettingsForAllUsers(liveMeeting, outGW)
|
||||
} else {
|
||||
@ -78,6 +79,7 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
|
||||
Vector()
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,6 +94,7 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
|
||||
Vector()
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
VoiceUsers.findAll(liveMeeting.voiceUsers) foreach { vu =>
|
||||
if (vu.intId.startsWith(IntIdPrefixType.DIAL_IN)) { // only Dial-in users need this
|
||||
val eventExplicitLock = buildLockMessage(liveMeeting.props.meetingProp.intId, vu.intId, msg.body.setBy, settings.disableMic)
|
||||
@ -109,6 +112,7 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
|
||||
Vector()
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,6 +127,7 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
|
||||
Vector()
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
} else {
|
||||
val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg(
|
||||
liveMeeting.props.meetingProp.intId,
|
||||
@ -133,6 +138,7 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
|
||||
Vector()
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
}
|
||||
}
|
||||
|
||||
@ -147,6 +153,7 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
|
||||
Vector()
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
} else {
|
||||
val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg(
|
||||
liveMeeting.props.meetingProp.intId,
|
||||
@ -157,6 +164,7 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
|
||||
Vector()
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,6 +179,7 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
|
||||
Vector()
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
} else {
|
||||
val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg(
|
||||
liveMeeting.props.meetingProp.intId,
|
||||
@ -181,6 +190,7 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
|
||||
Vector()
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
}
|
||||
}
|
||||
|
||||
@ -195,6 +205,7 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
|
||||
Vector()
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
} else {
|
||||
val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg(
|
||||
liveMeeting.props.meetingProp.intId,
|
||||
@ -205,6 +216,7 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
|
||||
Vector()
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import org.bigbluebutton.core.models.{ RegisteredUsers, Roles, UserState, Users2
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
|
||||
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
|
||||
import org.bigbluebutton.LockSettingsUtil
|
||||
import org.bigbluebutton.core.db.NotificationDAO
|
||||
import org.bigbluebutton.core2.message.senders.{ MsgBuilder, Sender }
|
||||
|
||||
trait ChangeUserRoleCmdMsgHdlr extends RightsManagementTrait {
|
||||
@ -43,6 +44,7 @@ trait ChangeUserRoleCmdMsgHdlr extends RightsManagementTrait {
|
||||
Vector()
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
|
||||
Users2x.changeRole(liveMeeting.users2x, uvo, msg.body.role)
|
||||
val event = buildUserRoleChangedEvtMsg(liveMeeting.props.meetingProp.intId, msg.body.userId,
|
||||
@ -59,6 +61,7 @@ trait ChangeUserRoleCmdMsgHdlr extends RightsManagementTrait {
|
||||
Vector()
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
|
||||
val newUvo: UserState = Users2x.changeRole(liveMeeting.users2x, uvo, msg.body.role)
|
||||
val event = buildUserRoleChangedEvtMsg(liveMeeting.props.meetingProp.intId, msg.body.userId,
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.bigbluebutton.core.apps.users
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.db.NotificationDAO
|
||||
import org.bigbluebutton.core.models._
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
|
||||
import org.bigbluebutton.core.util.ColorPicker
|
||||
@ -107,6 +108,7 @@ trait RegisterUserReqMsgHdlr {
|
||||
Vector(s"${regUser.name}")
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
case GuestStatus.DENY =>
|
||||
val g = GuestApprovedVO(regUser.id, GuestStatus.DENY)
|
||||
UsersApp.approveOrRejectGuest(liveMeeting, outGW, g, SystemUser.ID)
|
||||
|
@ -10,7 +10,7 @@ import org.bigbluebutton.core.api.SendRecordingTimerInternalMsg
|
||||
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
|
||||
import org.bigbluebutton.core2.message.senders.MsgBuilder
|
||||
import org.bigbluebutton.core.apps.voice.VoiceApp
|
||||
import org.bigbluebutton.core.db.MeetingRecordingDAO
|
||||
import org.bigbluebutton.core.db.{ MeetingRecordingDAO, NotificationDAO }
|
||||
|
||||
trait SetRecordingStatusCmdMsgHdlr extends RightsManagementTrait {
|
||||
this: UsersApp =>
|
||||
@ -49,6 +49,7 @@ trait SetRecordingStatusCmdMsgHdlr extends RightsManagementTrait {
|
||||
Vector()
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
|
||||
MeetingStatus2x.recordingStarted(liveMeeting.status)
|
||||
MeetingRecordingDAO.insertRecording(liveMeeting.props.meetingProp.intId, msg.body.setBy)
|
||||
@ -75,6 +76,7 @@ trait SetRecordingStatusCmdMsgHdlr extends RightsManagementTrait {
|
||||
Vector()
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
|
||||
MeetingStatus2x.recordingStopped(liveMeeting.status)
|
||||
MeetingRecordingDAO.updateStopped(liveMeeting.props.meetingProp.intId, msg.body.setBy)
|
||||
|
@ -0,0 +1,41 @@
|
||||
package org.bigbluebutton.core.apps.users
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.models.{ UserState, Users2x }
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
|
||||
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
|
||||
import org.bigbluebutton.core.domain.MeetingState2x
|
||||
|
||||
trait SetUserSpeechOptionsMsgHdlr extends RightsManagementTrait {
|
||||
this: UsersApp =>
|
||||
|
||||
val liveMeeting: LiveMeeting
|
||||
val outGW: OutMsgRouter
|
||||
|
||||
def handleSetUserSpeechOptionsReqMsg(msg: SetUserSpeechOptionsReqMsg): Unit = {
|
||||
log.info("handleSetUserSpeechOptionsReqMsg: partialUtterances={} minUtteranceLength={} userId={}", msg.body.partialUtterances, msg.body.minUtteranceLength, msg.header.userId)
|
||||
|
||||
def broadcastUserSpeechOptionsChanged(user: UserState, partialUtterances: Boolean, minUtteranceLength: Int): Unit = {
|
||||
val routingChange = Routing.addMsgToClientRouting(
|
||||
MessageTypes.BROADCAST_TO_MEETING,
|
||||
liveMeeting.props.meetingProp.intId, user.intId
|
||||
)
|
||||
val envelopeChange = BbbCoreEnvelope(UserSpeechOptionsChangedEvtMsg.NAME, routingChange)
|
||||
val headerChange = BbbClientMsgHeader(UserSpeechOptionsChangedEvtMsg.NAME, liveMeeting.props.meetingProp.intId, user.intId)
|
||||
|
||||
val bodyChange = UserSpeechOptionsChangedEvtMsgBody(partialUtterances, minUtteranceLength)
|
||||
val eventChange = UserSpeechOptionsChangedEvtMsg(headerChange, bodyChange)
|
||||
val msgEventChange = BbbCommonEnvCoreMsg(envelopeChange, eventChange)
|
||||
outGW.send(msgEventChange)
|
||||
}
|
||||
|
||||
for {
|
||||
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId)
|
||||
} yield {
|
||||
var changeLocale: Option[UserState] = None;
|
||||
//changeLocale = Users2x.setUserSpeechLocale(liveMeeting.users2x, msg.header.userId, msg.body.locale)
|
||||
broadcastUserSpeechOptionsChanged(user, msg.body.partialUtterances, msg.body.minUtteranceLength)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
package org.bigbluebutton.core.apps.users
|
||||
|
||||
import org.bigbluebutton.ClientSettings.{ getConfigPropertyValueByPathAsListOfIntOrElse, getConfigPropertyValueByPathAsListOfStringOrElse }
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.apps.RightsManagementTrait
|
||||
import org.bigbluebutton.core.db.UserConnectionStatusDAO
|
||||
import org.bigbluebutton.core.models.{ UserState, Users2x }
|
||||
import org.bigbluebutton.core.models.Users2x
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
|
||||
|
||||
trait UserConnectionAliveReqMsgHdlr extends RightsManagementTrait {
|
||||
@ -13,13 +14,42 @@ trait UserConnectionAliveReqMsgHdlr extends RightsManagementTrait {
|
||||
val outGW: OutMsgRouter
|
||||
|
||||
def handleUserConnectionAliveReqMsg(msg: UserConnectionAliveReqMsg): Unit = {
|
||||
log.info("handleUserConnectionAliveReqMsg: userId={}", msg.body.userId)
|
||||
log.info("handleUserConnectionAliveReqMsg: networkRttInMs={} userId={}", msg.body.networkRttInMs, msg.body.userId)
|
||||
|
||||
for {
|
||||
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId)
|
||||
} yield {
|
||||
UserConnectionStatusDAO.updateUserAlive(user.intId)
|
||||
val rtt: Option[Double] = msg.body.networkRttInMs match {
|
||||
case 0 => None
|
||||
case rtt: Double => Some(rtt)
|
||||
}
|
||||
|
||||
val status = getLevelFromRtt(msg.body.networkRttInMs)
|
||||
|
||||
UserConnectionStatusDAO.updateUserAlive(user.intId, rtt, status)
|
||||
}
|
||||
}
|
||||
|
||||
def getLevelFromRtt(networkRttInMs: Double): String = {
|
||||
val levelOptions = getConfigPropertyValueByPathAsListOfStringOrElse(
|
||||
liveMeeting.clientSettings,
|
||||
"public.stats.level",
|
||||
List("warning", "danger", "critical")
|
||||
)
|
||||
|
||||
val rttOptions = getConfigPropertyValueByPathAsListOfIntOrElse(
|
||||
liveMeeting.clientSettings,
|
||||
"public.stats.rtt",
|
||||
List(500, 1000, 2000)
|
||||
)
|
||||
|
||||
val statusRttXLevel = levelOptions.zip(rttOptions).reverse
|
||||
|
||||
val statusFound = statusRttXLevel.collectFirst {
|
||||
case (level, rtt) if networkRttInMs > rtt => level
|
||||
}
|
||||
|
||||
statusFound.getOrElse("normal")
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,25 +0,0 @@
|
||||
package org.bigbluebutton.core.apps.users
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.apps.RightsManagementTrait
|
||||
import org.bigbluebutton.core.db.UserConnectionStatusDAO
|
||||
import org.bigbluebutton.core.models.{ UserState, Users2x }
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
|
||||
|
||||
trait UserConnectionUpdateRttReqMsgHdlr extends RightsManagementTrait {
|
||||
this: UsersApp =>
|
||||
|
||||
val liveMeeting: LiveMeeting
|
||||
val outGW: OutMsgRouter
|
||||
|
||||
def handleUserConnectionUpdateRttReqMsg(msg: UserConnectionUpdateRttReqMsg): Unit = {
|
||||
log.info("handleUserConnectionUpdateRttReqMsg: networkRttInMs={} userId={}", msg.body.networkRttInMs, msg.body.userId)
|
||||
|
||||
for {
|
||||
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId)
|
||||
} yield {
|
||||
UserConnectionStatusDAO.updateUserRtt(user.intId, msg.body.networkRttInMs)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ package org.bigbluebutton.core.apps.users
|
||||
|
||||
import org.bigbluebutton.common2.msgs.UserJoinMeetingReqMsg
|
||||
import org.bigbluebutton.core.apps.breakout.BreakoutHdlrHelpers
|
||||
import org.bigbluebutton.core.db.{ UserDAO, UserStateDAO }
|
||||
import org.bigbluebutton.core.db.{ NotificationDAO, UserDAO, UserStateDAO }
|
||||
import org.bigbluebutton.core.domain.MeetingState2x
|
||||
import org.bigbluebutton.core.models._
|
||||
import org.bigbluebutton.core.running._
|
||||
@ -137,6 +137,7 @@ trait UserJoinMeetingReqMsgHdlr extends HandlerHelpers {
|
||||
Vector(newUser.name)
|
||||
)
|
||||
outGW.send(notifyUserEvent)
|
||||
NotificationDAO.insert(notifyUserEvent)
|
||||
}
|
||||
|
||||
private def clearCachedVoiceUser(regUser: RegisteredUser) =
|
||||
|
@ -158,6 +158,7 @@ class UsersApp(
|
||||
with RegisterUserReqMsgHdlr
|
||||
with ChangeUserRoleCmdMsgHdlr
|
||||
with SetUserSpeechLocaleMsgHdlr
|
||||
with SetUserSpeechOptionsMsgHdlr
|
||||
with SyncGetUsersMeetingRespMsgHdlr
|
||||
with LogoutAndEndMeetingCmdMsgHdlr
|
||||
with SetRecordingStatusCmdMsgHdlr
|
||||
@ -168,7 +169,6 @@ class UsersApp(
|
||||
with ChangeUserPinStateReqMsgHdlr
|
||||
with ChangeUserMobileFlagReqMsgHdlr
|
||||
with UserConnectionAliveReqMsgHdlr
|
||||
with UserConnectionUpdateRttReqMsgHdlr
|
||||
with ChangeUserReactionEmojiReqMsgHdlr
|
||||
with ChangeUserRaiseHandReqMsgHdlr
|
||||
with ChangeUserAwayReqMsgHdlr
|
||||
|
@ -3,7 +3,7 @@ package org.bigbluebutton.core.apps.webcam
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.apps.PermissionCheck
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.db.MeetingUsersPoliciesDAO
|
||||
import org.bigbluebutton.core.db.{ MeetingUsersPoliciesDAO, NotificationDAO }
|
||||
import org.bigbluebutton.core.models.{ RegisteredUsers, Roles, Users2x }
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
import org.bigbluebutton.core2.message.senders.{ MsgBuilder, Sender }
|
||||
@ -64,6 +64,7 @@ trait UpdateWebcamsOnlyForModeratorCmdMsgHdlr {
|
||||
Vector()
|
||||
)
|
||||
bus.outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
} else {
|
||||
val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg(
|
||||
meetingId,
|
||||
@ -74,6 +75,7 @@ trait UpdateWebcamsOnlyForModeratorCmdMsgHdlr {
|
||||
Vector()
|
||||
)
|
||||
bus.outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
}
|
||||
|
||||
broadcastEvent(meetingId, msg.body.setBy, value)
|
||||
|
@ -0,0 +1,70 @@
|
||||
package org.bigbluebutton.core.db
|
||||
import org.bigbluebutton.common2.msgs.{BbbCommonEnvCoreMsg, NotifyAllInMeetingEvtMsg, NotifyRoleInMeetingEvtMsg, NotifyUserInMeetingEvtMsg}
|
||||
import PostgresProfile.api._
|
||||
import spray.json.JsValue
|
||||
|
||||
import scala.util.{Failure, Success}
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
case class NotificationDbModel(
|
||||
// notificationId: String,
|
||||
meetingId: String,
|
||||
notificationType: String,
|
||||
icon: String,
|
||||
messageId: String,
|
||||
messageDescription: String,
|
||||
messageValues: JsValue,
|
||||
role: Option[String],
|
||||
userId: Option[String],
|
||||
createdAt: java.sql.Timestamp,
|
||||
)
|
||||
|
||||
class NotificationDbTableDef(tag: Tag) extends Table[NotificationDbModel](tag, None, "notification") {
|
||||
val meetingId = column[String]("meetingId")
|
||||
val notificationType = column[String]("notificationType")
|
||||
val icon = column[String]("icon")
|
||||
val messageId = column[String]("messageId")
|
||||
val messageDescription = column[String]("messageDescription")
|
||||
val messageValues = column[JsValue]("messageValues")
|
||||
val role = column[Option[String]]("role")
|
||||
val userId = column[Option[String]]("userId")
|
||||
val createdAt = column[java.sql.Timestamp]("createdAt")
|
||||
override def * = (meetingId, notificationType, icon, messageId, messageDescription, messageValues, role, userId, createdAt) <> (NotificationDbModel.tupled, NotificationDbModel.unapply)
|
||||
}
|
||||
|
||||
object NotificationDAO {
|
||||
def insert(notification: BbbCommonEnvCoreMsg) = {
|
||||
|
||||
val (meetingId, notificationType, icon, messageId, messageDescription, messageValues, role, userId) = notification.core match {
|
||||
case event: NotifyAllInMeetingEvtMsg =>
|
||||
(event.body.meetingId, event.body.notificationType, event.body.icon, event.body.messageId, event.body.messageDescription, event.body.messageValues, None, None)
|
||||
case event: NotifyRoleInMeetingEvtMsg =>
|
||||
(event.body.meetingId, event.body.notificationType, event.body.icon, event.body.messageId, event.body.messageDescription, event.body.messageValues, Some(event.body.role), None)
|
||||
case event: NotifyUserInMeetingEvtMsg =>
|
||||
(event.body.meetingId, event.body.notificationType, event.body.icon, event.body.messageId, event.body.messageDescription, event.body.messageValues, None, Some(event.body.userId))
|
||||
case _ =>
|
||||
("","", "", "", "", Vector(""), None, None)
|
||||
}
|
||||
|
||||
if (notificationType != "") {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[NotificationDbTableDef].forceInsert(
|
||||
NotificationDbModel(
|
||||
meetingId,
|
||||
notificationType,
|
||||
icon,
|
||||
messageId,
|
||||
messageDescription,
|
||||
JsonUtils.vectorToJson(messageValues),
|
||||
role,
|
||||
userId,
|
||||
createdAt = new java.sql.Timestamp(System.currentTimeMillis())
|
||||
)
|
||||
)
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) inserted/updated on Notification table!")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error inserting/updating Notification: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -71,6 +71,7 @@ object PluginDataChannelMessageDAO {
|
||||
.filter(_.meetingId === meetingId)
|
||||
.filter(_.pluginName === pluginName)
|
||||
.filter(_.dataChannel === dataChannel)
|
||||
.filter(_.deletedAt.isEmpty)
|
||||
.map(u => (u.deletedAt))
|
||||
.update(Some(new java.sql.Timestamp(System.currentTimeMillis())))
|
||||
).onComplete {
|
||||
@ -101,7 +102,8 @@ object PluginDataChannelMessageDAO {
|
||||
DatabaseConnection.db.run(
|
||||
sqlu"""UPDATE "pluginDataChannelMessage" SET
|
||||
"deletedAt" = current_timestamp
|
||||
WHERE "meetingId" = ${meetingId}
|
||||
WHERE "deletedAt" is null
|
||||
AND "meetingId" = ${meetingId}
|
||||
AND "pluginName" = ${pluginName}
|
||||
AND "dataChannel" = ${dataChannel}
|
||||
AND "messageId" = ${messageId}"""
|
||||
|
@ -3,6 +3,7 @@ package org.bigbluebutton.core.db
|
||||
import com.github.tminglei.slickpg._
|
||||
import org.apache.pekko.http.scaladsl.model.ParsingException
|
||||
import org.bigbluebutton.common2.domain.SimpleVoteOutVO
|
||||
import spray.json.DefaultJsonProtocol.{ StringJsonFormat, vectorFormat }
|
||||
import spray.json.{ JsArray, JsBoolean, JsNumber, JsObject, JsString, JsValue, JsonWriter, _ }
|
||||
|
||||
import scala.util.{ Failure, Success, Try }
|
||||
@ -55,6 +56,10 @@ object JsonUtils {
|
||||
genericMap.toJson
|
||||
}
|
||||
|
||||
def vectorToJson(genericVector: Vector[String]) = {
|
||||
genericVector.toJson
|
||||
}
|
||||
|
||||
def stringToJson(jsonString: String): JsValue = {
|
||||
Try(jsonString.parseJson) match {
|
||||
case Success(jsValue) => jsValue
|
||||
|
@ -1,9 +1,9 @@
|
||||
package org.bigbluebutton.core.db
|
||||
|
||||
import org.bigbluebutton.common2.msgs.AnnotationVO
|
||||
import PostgresProfile.api._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.util.{ Failure, Success }
|
||||
import slick.jdbc.PostgresProfile.api._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
case class PresAnnotationDbModel(
|
||||
annotationId: String,
|
||||
@ -42,8 +42,8 @@ object PresAnnotationDAO {
|
||||
)
|
||||
)
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) inserted on PresAnnotation table!")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error inserting PresAnnotation: $e")
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) inserted or updated on PresAnnotation table!")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error inserting or updating PresAnnotation: $e")
|
||||
}
|
||||
|
||||
}
|
||||
@ -51,8 +51,36 @@ object PresAnnotationDAO {
|
||||
}
|
||||
}
|
||||
|
||||
def delete(wbId: String, userId: String, annotationId: String) = {
|
||||
def prepareInsertOrUpdate(annotation: AnnotationVO) = {
|
||||
TableQuery[PresAnnotationDbTableDef].insertOrUpdate(
|
||||
PresAnnotationDbModel(
|
||||
annotationId = annotation.id,
|
||||
pageId = annotation.wbId,
|
||||
userId = annotation.userId,
|
||||
annotationInfo = JsonUtils.mapToJson(annotation.annotationInfo).compactPrint,
|
||||
lastHistorySequence = 0,
|
||||
lastUpdatedAt = new java.sql.Timestamp(System.currentTimeMillis())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def insertOrUpdateMap(annotations: Map[String, AnnotationVO]) = {
|
||||
DatabaseConnection.db.run(
|
||||
DBIO.sequence(
|
||||
annotations.map { annotation =>
|
||||
prepareInsertOrUpdate(annotation._2)
|
||||
}
|
||||
).transactionally
|
||||
)
|
||||
.onComplete {
|
||||
case Success(rowsAffected) =>
|
||||
DatabaseConnection.logger.debug(s"${rowsAffected.sum} row(s) inserted or updated on PresAnnotation table!")
|
||||
case Failure(e) =>
|
||||
DatabaseConnection.logger.debug(s"Error inserting or updating PresAnnotation: $e")
|
||||
}
|
||||
}
|
||||
|
||||
def delete(wbId: String, userId: String, annotationId: String) = {
|
||||
PresAnnotationHistoryDAO.delete(wbId, userId, annotationId).onComplete {
|
||||
case Success(sequence) => {
|
||||
DatabaseConnection.db.run(
|
||||
@ -69,4 +97,16 @@ object PresAnnotationDAO {
|
||||
}
|
||||
}
|
||||
|
||||
def delete(annotationIds: Array[String]) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[PresAnnotationDbTableDef]
|
||||
.filter(_.annotationId inSet annotationIds)
|
||||
.map(a => (a.annotationInfo, a.lastHistorySequence, a.lastUpdatedAt))
|
||||
.update("", 0, new java.sql.Timestamp(System.currentTimeMillis()))
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated annotationInfo=null on PresAnnotation table!")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating annotationInfo=null PresAnnotation: $e")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -5,22 +5,24 @@ import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.util.{ Failure, Success }
|
||||
|
||||
case class UserConnectionStatusDbModel(
|
||||
userId: String,
|
||||
meetingId: String,
|
||||
connectionAliveAt: Option[java.sql.Timestamp],
|
||||
userClientResponseAt: Option[java.sql.Timestamp],
|
||||
networkRttInMs: Option[Double]
|
||||
userId: String,
|
||||
meetingId: String,
|
||||
connectionAliveAt: Option[java.sql.Timestamp],
|
||||
networkRttInMs: Option[Double],
|
||||
status: String,
|
||||
statusUpdatedAt: Option[java.sql.Timestamp]
|
||||
)
|
||||
|
||||
class UserConnectionStatusDbTableDef(tag: Tag) extends Table[UserConnectionStatusDbModel](tag, None, "user_connectionStatus") {
|
||||
override def * = (
|
||||
userId, meetingId, connectionAliveAt, userClientResponseAt, networkRttInMs
|
||||
userId, meetingId, connectionAliveAt, networkRttInMs, status, statusUpdatedAt
|
||||
) <> (UserConnectionStatusDbModel.tupled, UserConnectionStatusDbModel.unapply)
|
||||
val userId = column[String]("userId", O.PrimaryKey)
|
||||
val meetingId = column[String]("meetingId")
|
||||
val connectionAliveAt = column[Option[java.sql.Timestamp]]("connectionAliveAt")
|
||||
val userClientResponseAt = column[Option[java.sql.Timestamp]]("userClientResponseAt")
|
||||
val networkRttInMs = column[Option[Double]]("networkRttInMs")
|
||||
val status = column[String]("status")
|
||||
val statusUpdatedAt = column[Option[java.sql.Timestamp]]("statusUpdatedAt")
|
||||
}
|
||||
|
||||
object UserConnectionStatusDAO {
|
||||
@ -32,8 +34,9 @@ object UserConnectionStatusDAO {
|
||||
userId = userId,
|
||||
meetingId = meetingId,
|
||||
connectionAliveAt = None,
|
||||
userClientResponseAt = None,
|
||||
networkRttInMs = None
|
||||
networkRttInMs = None,
|
||||
status = "normal",
|
||||
statusUpdatedAt = None
|
||||
)
|
||||
)
|
||||
).onComplete {
|
||||
@ -42,28 +45,23 @@ object UserConnectionStatusDAO {
|
||||
}
|
||||
}
|
||||
|
||||
def updateUserAlive(userId: String) = {
|
||||
def updateUserAlive(userId: String, rtt: Option[Double], status: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[UserConnectionStatusDbTableDef]
|
||||
.filter(_.userId === userId)
|
||||
.map(t => (t.connectionAliveAt))
|
||||
.update(Some(new java.sql.Timestamp(System.currentTimeMillis())))
|
||||
.map(t => (t.connectionAliveAt, t.networkRttInMs, t.status, t.statusUpdatedAt))
|
||||
.update(
|
||||
(
|
||||
Some(new java.sql.Timestamp(System.currentTimeMillis())),
|
||||
rtt,
|
||||
status,
|
||||
Some(new java.sql.Timestamp(System.currentTimeMillis())),
|
||||
)
|
||||
)
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated connectionAliveAt on UserConnectionStatus table!")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating connectionAliveAt on UserConnectionStatus: $e")
|
||||
}
|
||||
}
|
||||
|
||||
def updateUserRtt(userId: String, networkRttInMs: Double) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[UserConnectionStatusDbTableDef]
|
||||
.filter(_.userId === userId)
|
||||
.map(t => (t.networkRttInMs, t.userClientResponseAt))
|
||||
.update((Some(networkRttInMs), Some(new java.sql.Timestamp(System.currentTimeMillis()))))
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated networkRttInMs on UserConnectionStatus table!")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating networkRttInMs on UserConnectionStatus: $e")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -117,8 +117,8 @@ object UserDAO {
|
||||
.map(u => (u.guest, u.guestStatus, u.authed, u.joined))
|
||||
.update((false, "ALLOW", true, true))
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated on user voice table!")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating user voice: $e")
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated on user table!")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating user table: $e")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,48 @@
|
||||
package org.bigbluebutton.core.db
|
||||
|
||||
import org.bigbluebutton.core.models.{ VoiceUserState }
|
||||
import slick.jdbc.PostgresProfile.api._
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.util.{ Failure, Success }
|
||||
|
||||
case class UserTranscriptionErrorDbModel(
|
||||
userId: String,
|
||||
meetingId: String,
|
||||
errorCode: String,
|
||||
errorMessage: String,
|
||||
lastUpdatedAt: java.sql.Timestamp = new java.sql.Timestamp(System.currentTimeMillis())
|
||||
)
|
||||
|
||||
class UserTranscriptionErrorDbTableDef(tag: Tag) extends Table[UserTranscriptionErrorDbModel](tag, None, "user_transcriptionError") {
|
||||
override def * = (
|
||||
userId, meetingId, errorCode, errorMessage, lastUpdatedAt
|
||||
) <> (UserTranscriptionErrorDbModel.tupled, UserTranscriptionErrorDbModel.unapply)
|
||||
val userId = column[String]("userId", O.PrimaryKey)
|
||||
val meetingId = column[String]("meetingId")
|
||||
val errorCode = column[String]("errorCode")
|
||||
val errorMessage = column[String]("errorMessage")
|
||||
val lastUpdatedAt = column[java.sql.Timestamp]("lastUpdatedAt")
|
||||
}
|
||||
|
||||
object UserTranscriptionErrorDAO {
|
||||
def insert(userId: String, meetingId: String, errorCode: String, errorMessage: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[UserTranscriptionErrorDbTableDef].insertOrUpdate(
|
||||
UserTranscriptionErrorDbModel(
|
||||
userId = userId,
|
||||
meetingId = meetingId,
|
||||
errorCode = errorCode,
|
||||
errorMessage = errorMessage,
|
||||
lastUpdatedAt = new java.sql.Timestamp(System.currentTimeMillis()),
|
||||
)
|
||||
)
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => {
|
||||
DatabaseConnection.logger.debug(s"$rowsAffected row(s) inserted on user_transcriptionError table!")
|
||||
}
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error inserting user_transcriptionError: $e")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -7,12 +7,10 @@ import org.bigbluebutton.SystemConfiguration
|
||||
object AudioCaptions extends SystemConfiguration {
|
||||
def setFloor(audioCaptions: AudioCaptions, userId: String) = audioCaptions.floor = userId
|
||||
|
||||
def isFloor(audioCaptions: AudioCaptions, userId: String) = audioCaptions.floor == userId
|
||||
def isFloor(audioCaptions: AudioCaptions, userId: String) = true
|
||||
|
||||
def parseTranscript(transcript: String): String = {
|
||||
val words = transcript.split("\\s+") // Split on whitespaces
|
||||
val lines = words.grouped(transcriptWords).toArray // Group each X words into lines
|
||||
lines.takeRight(transcriptLines).map(l => l.mkString(" ")).mkString("\n") // Join the last X lines
|
||||
transcript
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -113,10 +113,10 @@ class ReceivedJsonMsgHandlerActor(
|
||||
routeGenericMsg[ChangeUserMobileFlagReqMsg](envelope, jsonNode)
|
||||
case UserConnectionAliveReqMsg.NAME =>
|
||||
routeGenericMsg[UserConnectionAliveReqMsg](envelope, jsonNode)
|
||||
case UserConnectionUpdateRttReqMsg.NAME =>
|
||||
routeGenericMsg[UserConnectionUpdateRttReqMsg](envelope, jsonNode)
|
||||
case SetUserSpeechLocaleReqMsg.NAME =>
|
||||
routeGenericMsg[SetUserSpeechLocaleReqMsg](envelope, jsonNode)
|
||||
case SetUserSpeechOptionsReqMsg.NAME =>
|
||||
routeGenericMsg[SetUserSpeechOptionsReqMsg](envelope, jsonNode)
|
||||
|
||||
// Poll
|
||||
case StartCustomPollReqMsg.NAME =>
|
||||
@ -406,6 +406,8 @@ class ReceivedJsonMsgHandlerActor(
|
||||
// AudioCaptions
|
||||
case UpdateTranscriptPubMsg.NAME =>
|
||||
routeGenericMsg[UpdateTranscriptPubMsg](envelope, jsonNode)
|
||||
case TranscriptionProviderErrorMsg.NAME =>
|
||||
routeGenericMsg[TranscriptionProviderErrorMsg](envelope, jsonNode)
|
||||
|
||||
// GroupChats
|
||||
case GetGroupChatsReqMsg.NAME =>
|
||||
|
@ -1,24 +0,0 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.core.record.events
|
||||
|
||||
trait AbstractDeskshareRecordEvent extends RecordEvent {
|
||||
setModule("DESKSHARE")
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.core.record.events
|
||||
|
||||
class DeskshareStartRtmpRecordEvent extends AbstractDeskshareRecordEvent {
|
||||
import DeskshareStartRtmpRecordEvent._
|
||||
|
||||
setEvent("DeskShareStartRTMP")
|
||||
|
||||
def setStreamPath(streamPath: String) {
|
||||
eventMap.put(STREAM_PATH, streamPath)
|
||||
}
|
||||
}
|
||||
|
||||
object DeskshareStartRtmpRecordEvent {
|
||||
protected final val STREAM_PATH = "startIstreamPathndex"
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.core.record.events
|
||||
|
||||
class DeskshareStopRtmpRecordEvent extends AbstractDeskshareRecordEvent {
|
||||
import DeskshareStopRtmpRecordEvent._
|
||||
|
||||
setEvent("DeskShareStopRTMP")
|
||||
|
||||
def setStreamPath(streamPath: String) {
|
||||
eventMap.put(STREAM_PATH, streamPath)
|
||||
}
|
||||
}
|
||||
|
||||
object DeskshareStopRtmpRecordEvent {
|
||||
protected final val STREAM_PATH = "startIstreamPathndex"
|
||||
}
|
@ -7,7 +7,7 @@ import org.bigbluebutton.core.apps.groupchats.GroupChatApp
|
||||
import org.bigbluebutton.core.apps.users.UsersApp
|
||||
import org.bigbluebutton.core.apps.voice.VoiceApp
|
||||
import org.bigbluebutton.core.bus.{BigBlueButtonEvent, InternalEventBus}
|
||||
import org.bigbluebutton.core.db.{BreakoutRoomUserDAO, MeetingDAO, MeetingRecordingDAO, UserBreakoutRoomDAO}
|
||||
import org.bigbluebutton.core.db.{BreakoutRoomUserDAO, MeetingDAO, MeetingRecordingDAO, NotificationDAO, UserBreakoutRoomDAO}
|
||||
import org.bigbluebutton.core.domain.{MeetingEndReason, MeetingState2x}
|
||||
import org.bigbluebutton.core.models._
|
||||
import org.bigbluebutton.core2.MeetingStatus2x
|
||||
@ -101,6 +101,7 @@ trait HandlerHelpers extends SystemConfiguration {
|
||||
Vector(s"${newUser.name}")
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
|
||||
val newState = startRecordingIfAutoStart2x(outGW, liveMeeting, state)
|
||||
if (!Users2x.hasPresenter(liveMeeting.users2x)) {
|
||||
|
@ -41,7 +41,7 @@ import org.bigbluebutton.core.apps.layout.LayoutApp2x
|
||||
import org.bigbluebutton.core.apps.meeting.{ SyncGetMeetingInfoRespMsgHdlr, ValidateConnAuthTokenSysMsgHdlr }
|
||||
import org.bigbluebutton.core.apps.plugin.PluginHdlrs
|
||||
import org.bigbluebutton.core.apps.users.ChangeLockSettingsInMeetingCmdMsgHdlr
|
||||
import org.bigbluebutton.core.db.UserStateDAO
|
||||
import org.bigbluebutton.core.db.{ NotificationDAO, UserStateDAO }
|
||||
import org.bigbluebutton.core.models.VoiceUsers.{ findAllFreeswitchCallers, findAllListenOnlyVoiceUsers }
|
||||
import org.bigbluebutton.core.models.Webcams.findAll
|
||||
import org.bigbluebutton.core2.MeetingStatus2x.hasAuthedUserJoined
|
||||
@ -405,8 +405,8 @@ class MeetingActor(
|
||||
case m: ChangeUserPinStateReqMsg => usersApp.handleChangeUserPinStateReqMsg(m)
|
||||
case m: ChangeUserMobileFlagReqMsg => usersApp.handleChangeUserMobileFlagReqMsg(m)
|
||||
case m: UserConnectionAliveReqMsg => usersApp.handleUserConnectionAliveReqMsg(m)
|
||||
case m: UserConnectionUpdateRttReqMsg => usersApp.handleUserConnectionUpdateRttReqMsg(m)
|
||||
case m: SetUserSpeechLocaleReqMsg => usersApp.handleSetUserSpeechLocaleReqMsg(m)
|
||||
case m: SetUserSpeechOptionsReqMsg => usersApp.handleSetUserSpeechOptionsReqMsg(m)
|
||||
|
||||
// Client requested to eject user
|
||||
case m: EjectUserFromMeetingCmdMsg =>
|
||||
@ -590,6 +590,7 @@ class MeetingActor(
|
||||
|
||||
// AudioCaptions
|
||||
case m: UpdateTranscriptPubMsg => audioCaptionsApp2x.handle(m, liveMeeting, msgBus)
|
||||
case m: TranscriptionProviderErrorMsg => audioCaptionsApp2x.handleTranscriptionProviderErrorMsg(m, liveMeeting, msgBus)
|
||||
|
||||
// GroupChat
|
||||
case m: CreateGroupChatReqMsg =>
|
||||
@ -923,6 +924,7 @@ class MeetingActor(
|
||||
Vector(s"${u.name}")
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
|
||||
if (u.presenter) {
|
||||
log.info("removeUsersWithExpiredUserLeftFlag will cause an automaticallyAssignPresenter because user={} left", u)
|
||||
|
@ -79,7 +79,6 @@ class AnalyticsActor(val includeChat: Boolean) extends Actor with ActorLogging {
|
||||
case m: ChangeUserPinStateReqMsg => logMessage(msg)
|
||||
case m: ChangeUserMobileFlagReqMsg => logMessage(msg)
|
||||
case m: UserConnectionAliveReqMsg => logMessage(msg)
|
||||
case m: UserConnectionUpdateRttReqMsg => logMessage(msg)
|
||||
case m: ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg => logMessage(msg)
|
||||
case m: ScreenshareRtmpBroadcastStoppedVoiceConfEvtMsg => logMessage(msg)
|
||||
case m: ScreenshareRtmpBroadcastStartedEvtMsg => logMessage(msg)
|
||||
|
@ -5,7 +5,8 @@ import org.bigbluebutton.core.models.{ UserState, Users2x, VoiceUserState, Voice
|
||||
import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter }
|
||||
import org.bigbluebutton.core2.MeetingStatus2x
|
||||
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
|
||||
import org.bigbluebutton.core2.message.senders.{ MsgBuilder }
|
||||
import org.bigbluebutton.core.db.NotificationDAO
|
||||
import org.bigbluebutton.core2.message.senders.MsgBuilder
|
||||
|
||||
trait MuteAllExceptPresentersCmdMsgHdlr extends RightsManagementTrait {
|
||||
this: MeetingActor =>
|
||||
@ -29,6 +30,7 @@ trait MuteAllExceptPresentersCmdMsgHdlr extends RightsManagementTrait {
|
||||
Vector()
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
|
||||
MeetingStatus2x.muteMeeting(liveMeeting.status)
|
||||
} else {
|
||||
@ -41,6 +43,7 @@ trait MuteAllExceptPresentersCmdMsgHdlr extends RightsManagementTrait {
|
||||
Vector()
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
|
||||
MeetingStatus2x.unmuteMeeting(liveMeeting.status)
|
||||
}
|
||||
|
@ -2,10 +2,11 @@ package org.bigbluebutton.core2.message.handlers
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
|
||||
import org.bigbluebutton.core.db.NotificationDAO
|
||||
import org.bigbluebutton.core.models.{ VoiceUserState, VoiceUsers }
|
||||
import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter }
|
||||
import org.bigbluebutton.core2.MeetingStatus2x
|
||||
import org.bigbluebutton.core2.message.senders.{ MsgBuilder }
|
||||
import org.bigbluebutton.core2.message.senders.MsgBuilder
|
||||
|
||||
trait MuteMeetingCmdMsgHdlr extends RightsManagementTrait {
|
||||
this: MeetingActor =>
|
||||
@ -54,6 +55,7 @@ trait MuteMeetingCmdMsgHdlr extends RightsManagementTrait {
|
||||
Vector()
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
|
||||
MeetingStatus2x.muteMeeting(liveMeeting.status)
|
||||
} else {
|
||||
@ -66,6 +68,7 @@ trait MuteMeetingCmdMsgHdlr extends RightsManagementTrait {
|
||||
Vector()
|
||||
)
|
||||
outGW.send(notifyEvent)
|
||||
NotificationDAO.insert(notifyEvent)
|
||||
|
||||
MeetingStatus2x.unmuteMeeting(liveMeeting.status)
|
||||
}
|
||||
|
@ -109,11 +109,6 @@ class RedisRecorderActor(
|
||||
// Pads
|
||||
case m: PadCreatedRespMsg => handlePadCreatedRespMsg(m)
|
||||
|
||||
// Screenshare
|
||||
case m: ScreenshareRtmpBroadcastStartedEvtMsg => handleScreenshareRtmpBroadcastStartedEvtMsg(m)
|
||||
case m: ScreenshareRtmpBroadcastStoppedEvtMsg => handleScreenshareRtmpBroadcastStoppedEvtMsg(m)
|
||||
//case m: DeskShareNotifyViewersRTMP => handleDeskShareNotifyViewersRTMP(m)
|
||||
|
||||
// AudioCaptions
|
||||
//case m: TranscriptUpdatedEvtMsg => handleTranscriptUpdatedEvtMsg(m) // temporarily disabling due to issue https://github.com/bigbluebutton/bigbluebutton/issues/19701
|
||||
|
||||
@ -509,33 +504,6 @@ class RedisRecorderActor(
|
||||
record(msg.header.meetingId, ev.toMap.asJava)
|
||||
}
|
||||
|
||||
private def handleScreenshareRtmpBroadcastStartedEvtMsg(msg: ScreenshareRtmpBroadcastStartedEvtMsg) {
|
||||
val ev = new DeskshareStartRtmpRecordEvent()
|
||||
ev.setMeetingId(msg.header.meetingId)
|
||||
ev.setStreamPath(msg.body.stream)
|
||||
|
||||
record(msg.header.meetingId, ev.toMap.asJava)
|
||||
}
|
||||
|
||||
private def handleScreenshareRtmpBroadcastStoppedEvtMsg(msg: ScreenshareRtmpBroadcastStoppedEvtMsg) {
|
||||
val ev = new DeskshareStopRtmpRecordEvent()
|
||||
ev.setMeetingId(msg.header.meetingId)
|
||||
ev.setStreamPath(msg.body.stream)
|
||||
|
||||
record(msg.header.meetingId, ev.toMap.asJava)
|
||||
}
|
||||
|
||||
/*
|
||||
private def handleDeskShareNotifyViewersRTMP(msg: DeskShareNotifyViewersRTMP) {
|
||||
val ev = new DeskShareNotifyViewersRTMPRecordEvent()
|
||||
ev.setMeetingId(msg.header.meetingId)
|
||||
ev.setStreamPath(msg.streamPath)
|
||||
ev.setBroadcasting(msg.broadcasting)
|
||||
|
||||
record(msg.header.meetingId, JavaConverters.mapAsScalaMap(ev.toMap).toMap)
|
||||
}
|
||||
*/
|
||||
|
||||
/* temporarily disabling due to issue https://github.com/bigbluebutton/bigbluebutton/issues/19701
|
||||
private def handleTranscriptUpdatedEvtMsg(msg: TranscriptUpdatedEvtMsg) {
|
||||
val ev = new TranscriptUpdatedRecordEvent()
|
||||
|
@ -1,5 +1,12 @@
|
||||
package org.bigbluebutton.common2.msgs
|
||||
|
||||
object TranscriptionProviderErrorMsg { val NAME = "TranscriptionProviderErrorMsg" }
|
||||
case class TranscriptionProviderErrorMsg(header: BbbClientMsgHeader, body: TranscriptionProviderErrorMsgBody) extends StandardMsg
|
||||
case class TranscriptionProviderErrorMsgBody(
|
||||
errorCode: String,
|
||||
errorMessage: String,
|
||||
)
|
||||
|
||||
// In messages
|
||||
object UpdateTranscriptPubMsg { val NAME = "UpdateTranscriptPubMsg" }
|
||||
case class UpdateTranscriptPubMsg(header: BbbClientMsgHeader, body: UpdateTranscriptPubMsgBody) extends StandardMsg
|
||||
@ -14,6 +21,10 @@ case class UpdateTranscriptPubMsgBody(
|
||||
)
|
||||
|
||||
// Out messages
|
||||
object TranscriptionProviderErrorEvtMsg { val NAME = "TranscriptionProviderErrorEvtMsg" }
|
||||
case class TranscriptionProviderErrorEvtMsg(header: BbbClientMsgHeader, body: TranscriptionProviderErrorEvtMsgBody) extends BbbCoreMsg
|
||||
case class TranscriptionProviderErrorEvtMsgBody(errorCode: String, errorMessage: String)
|
||||
|
||||
object TranscriptUpdatedEvtMsg { val NAME = "TranscriptUpdatedEvtMsg" }
|
||||
case class TranscriptUpdatedEvtMsg(header: BbbClientMsgHeader, body: TranscriptUpdatedEvtMsgBody) extends BbbCoreMsg
|
||||
case class TranscriptUpdatedEvtMsgBody(transcriptId: String, transcript: String, locale: String, result: Boolean)
|
||||
|
@ -107,7 +107,7 @@ case class PadTailEvtMsgBody(externalId: String, tail: String)
|
||||
// client -> apps
|
||||
object PadUpdatePubMsg { val NAME = "PadUpdatePubMsg" }
|
||||
case class PadUpdatePubMsg(header: BbbClientMsgHeader, body: PadUpdatePubMsgBody) extends StandardMsg
|
||||
case class PadUpdatePubMsgBody(externalId: String, text: String)
|
||||
case class PadUpdatePubMsgBody(externalId: String, text: String, transcript: Boolean)
|
||||
|
||||
// apps -> pads
|
||||
object PadUpdateCmdMsg { val NAME = "PadUpdateCmdMsg" }
|
||||
|
@ -298,14 +298,7 @@ case class ChangeUserMobileFlagReqMsgBody(userId: String, mobile: Boolean)
|
||||
*/
|
||||
object UserConnectionAliveReqMsg { val NAME = "UserConnectionAliveReqMsg" }
|
||||
case class UserConnectionAliveReqMsg(header: BbbClientMsgHeader, body: UserConnectionAliveReqMsgBody) extends StandardMsg
|
||||
case class UserConnectionAliveReqMsgBody(userId: String)
|
||||
|
||||
/**
|
||||
* Sent from client to inform the RTT (time it took to send the Alive and receive confirmation).
|
||||
*/
|
||||
object UserConnectionUpdateRttReqMsg { val NAME = "UserConnectionUpdateRttReqMsg" }
|
||||
case class UserConnectionUpdateRttReqMsg(header: BbbClientMsgHeader, body: UserConnectionUpdateRttReqMsgBody) extends StandardMsg
|
||||
case class UserConnectionUpdateRttReqMsgBody(userId: String, networkRttInMs: Double)
|
||||
case class UserConnectionAliveReqMsgBody(userId: String, networkRttInMs: Double)
|
||||
|
||||
/**
|
||||
* Sent to all clients about a user mobile flag.
|
||||
@ -532,3 +525,11 @@ case class SetUserSpeechLocaleReqMsgBody(locale: String, provider: String)
|
||||
object UserSpeechLocaleChangedEvtMsg { val NAME = "UserSpeechLocaleChangedEvtMsg" }
|
||||
case class UserSpeechLocaleChangedEvtMsg(header: BbbClientMsgHeader, body: UserSpeechLocaleChangedEvtMsgBody) extends BbbCoreMsg
|
||||
case class UserSpeechLocaleChangedEvtMsgBody(locale: String, provider: String)
|
||||
|
||||
object SetUserSpeechOptionsReqMsg { val NAME = "SetUserSpeechOptionsReqMsg" }
|
||||
case class SetUserSpeechOptionsReqMsg(header: BbbClientMsgHeader, body: SetUserSpeechOptionsReqMsgBody) extends StandardMsg
|
||||
case class SetUserSpeechOptionsReqMsgBody(partialUtterances: Boolean, minUtteranceLength: Int)
|
||||
|
||||
object UserSpeechOptionsChangedEvtMsg { val NAME = "UserSpeechOptionsChangedEvtMsg" }
|
||||
case class UserSpeechOptionsChangedEvtMsg(header: BbbClientMsgHeader, body: UserSpeechOptionsChangedEvtMsgBody) extends BbbCoreMsg
|
||||
case class UserSpeechOptionsChangedEvtMsgBody(partialUtterances: Boolean, minUtteranceLength: Int)
|
||||
|
@ -9,10 +9,8 @@ module.exports = {
|
||||
],
|
||||
'parserOptions': {
|
||||
'ecmaVersion': 'latest',
|
||||
'sourceType': 'module',
|
||||
},
|
||||
'rules': {
|
||||
'require-jsdoc': 0,
|
||||
'camelcase': 0,
|
||||
'max-len': 0,
|
||||
},
|
||||
};
|
@ -1,4 +0,0 @@
|
||||
const settings = require('./settings');
|
||||
const config = settings;
|
||||
|
||||
module.exports = config;
|
@ -1,41 +1,40 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "info",
|
||||
"msgName": "PresAnnStatusMsg"
|
||||
},
|
||||
"shared": {
|
||||
"presDir": "/var/bigbluebutton",
|
||||
"presAnnDropboxDir": "/tmp/pres-ann-dropbox",
|
||||
"cairosvg": "/usr/bin/cairosvg",
|
||||
"ghostscript": "/usr/bin/gs",
|
||||
"imagemagick": "/usr/bin/convert",
|
||||
"pdftocairo": "/usr/bin/pdftocairo"
|
||||
},
|
||||
"collector": {
|
||||
"pngWidthRasterizedSlides": 2560
|
||||
},
|
||||
"process": {
|
||||
"whiteboardTextEncoding": "utf-8",
|
||||
"maxImageWidth": 1440,
|
||||
"maxImageHeight": 1080,
|
||||
"textScaleFactor": 2,
|
||||
"pointsPerInch": 72,
|
||||
"pixelsPerInch": 96
|
||||
},
|
||||
"notifier": {
|
||||
"pod_id": "DEFAULT_PRESENTATION_POD",
|
||||
"is_downloadable": "false",
|
||||
"msgName": "NewPresFileAvailableMsg"
|
||||
},
|
||||
"bbbWebAPI": "http://127.0.0.1:8090",
|
||||
"bbbPadsAPI": "http://127.0.0.1:9002",
|
||||
"redis": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 6379,
|
||||
"password": null,
|
||||
"channels": {
|
||||
"queue": "exportJobs",
|
||||
"publish": "to-akka-apps-redis-channel"
|
||||
}
|
||||
"log": {
|
||||
"level": "info",
|
||||
"msgName": "PresAnnStatusMsg"
|
||||
},
|
||||
"shared": {
|
||||
"presAnnDropboxDir": "/tmp/pres-ann-dropbox",
|
||||
"cairosvg": "/usr/bin/cairosvg",
|
||||
"ghostscript": "/usr/bin/gs"
|
||||
},
|
||||
"process": {
|
||||
"maxImageWidth": 1440,
|
||||
"maxImageHeight": 1080,
|
||||
"pointsPerInch": 72,
|
||||
"pixelsPerInch": 96,
|
||||
"cairoSVGUnsafeFlag": false
|
||||
},
|
||||
"notifier": {
|
||||
"pod_id": "DEFAULT_PRESENTATION_POD",
|
||||
"is_downloadable": "false",
|
||||
"msgName": "NewPresFileAvailableMsg"
|
||||
},
|
||||
"bbbWebAPI": "http://127.0.0.1:8090",
|
||||
"bbbPadsAPI": "http://127.0.0.1:9002",
|
||||
"redis": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 6379,
|
||||
"password": null,
|
||||
"channels": {
|
||||
"queue": "exportJobs",
|
||||
"publish": "to-akka-apps-redis-channel"
|
||||
}
|
||||
},
|
||||
"fonts": {
|
||||
"draw": "/usr/local/share/fonts/CaveatBrush-Regular-2015-09-23.ttf",
|
||||
"sans": "/usr/local/share/fonts/CrimsonPro[wght]-1.003.ttf",
|
||||
"serif": "/usr/local/share/fonts/SourceSansPro-Regular-2.045.ttf",
|
||||
"mono": "/usr/local/share/fonts/SourceCodePro-Regular-2.038.ttf"
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
const config = require('../../config');
|
||||
import fs from 'fs';
|
||||
const config = JSON.parse(fs.readFileSync('./config/settings.json', 'utf8'));
|
||||
|
||||
const {level} = config.log;
|
||||
const trace = level.toLowerCase() === 'trace';
|
||||
@ -14,7 +15,7 @@ const parse = (messages) => {
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = class Logger {
|
||||
export default class Logger {
|
||||
constructor(context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
const config = require('../../config');
|
||||
import fs from 'fs';
|
||||
const config = JSON.parse(fs.readFileSync('./config/settings.json', 'utf8'));
|
||||
|
||||
const EXPORT_STATUSES = Object.freeze({
|
||||
COLLECTING: 'COLLECTING',
|
||||
@ -86,7 +87,4 @@ class NewPresFileAvailableMsg {
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
PresAnnStatusMsg,
|
||||
NewPresFileAvailableMsg,
|
||||
};
|
||||
export {PresAnnStatusMsg, NewPresFileAvailableMsg};
|
||||
|
@ -1,5 +1,6 @@
|
||||
const {Worker} = require('worker_threads');
|
||||
const path = require('path');
|
||||
import {Worker} from 'worker_threads';
|
||||
import {fileURLToPath} from 'url';
|
||||
import path from 'path';
|
||||
|
||||
const WorkerTypes = Object.freeze({
|
||||
Collector: 'collector',
|
||||
@ -9,19 +10,27 @@ const WorkerTypes = Object.freeze({
|
||||
|
||||
const kickOffWorker = (workerType, workerData) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const workerPath = path.join(__dirname, '..', '..', 'workers', `${workerType}.js`);
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const workerPath = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'workers',
|
||||
`${workerType}.js`);
|
||||
const worker = new Worker(workerPath, {workerData});
|
||||
worker.on('message', resolve);
|
||||
worker.on('error', reject);
|
||||
worker.on('exit', (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`Worker '${workerType}' stopped with exit code ${code}`));
|
||||
reject(
|
||||
new Error(`Worker '${workerType}' stopped with exit code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = class WorkerStarter {
|
||||
export default class WorkerStarter {
|
||||
constructor(workerData) {
|
||||
this.workerData = workerData;
|
||||
}
|
||||
|
@ -1,19 +1,21 @@
|
||||
const Logger = require('./lib/utils/logger');
|
||||
const WorkerStarter = require('./lib/utils/worker-starter');
|
||||
const config = require('./config');
|
||||
const fs = require('fs');
|
||||
const redis = require('redis');
|
||||
const {commandOptions} = require('redis');
|
||||
const path = require('path');
|
||||
import Logger from './lib/utils/logger.js';
|
||||
import WorkerStarter from './lib/utils/worker-starter.js';
|
||||
import fs from 'fs';
|
||||
import redis, {commandOptions} from 'redis';
|
||||
import path from 'path';
|
||||
|
||||
const logger = new Logger('presAnn Master');
|
||||
const config = JSON.parse(fs.readFileSync('./config/settings.json', 'utf8'));
|
||||
|
||||
logger.info('Running bbb-export-annotations');
|
||||
|
||||
(async () => {
|
||||
const client = redis.createClient({
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
password: config.redis.password,
|
||||
socket: {
|
||||
host: config.redis.host,
|
||||
port: config.redis.port
|
||||
}
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
503
bbb-export-annotations/package-lock.json
generated
503
bbb-export-annotations/package-lock.json
generated
@ -1,20 +1,22 @@
|
||||
{
|
||||
"name": "bbb-export-annotations",
|
||||
"version": "0.0.1",
|
||||
"version": "2.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bbb-export-annotations",
|
||||
"version": "0.0.1",
|
||||
"version": "2.0",
|
||||
"dependencies": {
|
||||
"@svgdotjs/svg.js": "^3.2.0",
|
||||
"axios": "^1.6.5",
|
||||
"form-data": "^4.0.0",
|
||||
"opentype.js": "^1.3.4",
|
||||
"perfect-freehand": "^1.0.16",
|
||||
"probe-image-size": "^7.2.3",
|
||||
"redis": "^4.0.3",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"xmlbuilder2": "^3.0.2"
|
||||
"svgdom": "^0.1.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.20.0",
|
||||
@ -165,55 +167,24 @@
|
||||
"@node-redis/client": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oozcitak/dom": {
|
||||
"version": "1.15.10",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.10.tgz",
|
||||
"integrity": "sha512-0JT29/LaxVgRcGKvHmSrUTEvZ8BXvZhGl2LASRUgHqDTC1M5g1pLmVv56IYNyt3bG2CUjDkc67wnyZC14pbQrQ==",
|
||||
"node_modules/@svgdotjs/svg.js": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.0.tgz",
|
||||
"integrity": "sha512-Tr8p+QVP7y+QT1GBlq1Tt57IvedVH8zCPoYxdHLX0Oof3a/PqnC/tXAkVufv1JQJfsDHlH/UrjcDfgxSofqSNA==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Fuzzyma"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.4.36",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.36.tgz",
|
||||
"integrity": "sha512-5lxnyLEYFskErRPenYItLRSge5DjrJngYKdVjRSrWfza9G6KkgHEXi0vUZiyUeMU5JfXH1YnvXZzSp8ul88o2Q==",
|
||||
"dependencies": {
|
||||
"@oozcitak/infra": "1.0.8",
|
||||
"@oozcitak/url": "1.0.4",
|
||||
"@oozcitak/util": "8.3.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
"legacy-swc-helpers": "npm:@swc/helpers@=0.4.14",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oozcitak/infra": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.8.tgz",
|
||||
"integrity": "sha512-JRAUc9VR6IGHOL7OGF+yrvs0LO8SlqGnPAMqyzOuFZPSZSXI7Xf2O9+awQPSMXgIWGtgUf/dA6Hs6X6ySEaWTg==",
|
||||
"dependencies": {
|
||||
"@oozcitak/util": "8.3.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oozcitak/url": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-1.0.4.tgz",
|
||||
"integrity": "sha512-kDcD8y+y3FCSOvnBI6HJgl00viO/nGbQoCINmQ0h98OhnGITrWR3bOGfwYCthgcrV8AnTJz8MzslTQbC3SOAmw==",
|
||||
"dependencies": {
|
||||
"@oozcitak/infra": "1.0.8",
|
||||
"@oozcitak/util": "8.3.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oozcitak/util": {
|
||||
"version": "8.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.8.tgz",
|
||||
"integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==",
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "17.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.16.tgz",
|
||||
"integrity": "sha512-ydLaGVfQOQ6hI1xK2A5nVh8bl0OGoIfYMxPWHqqYe9bTkWCfqiVvZoh2I/QF2sNSkZzZyROBoTefIEI+PB6iIA=="
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz",
|
||||
@ -302,6 +273,25 @@
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
@ -312,6 +302,14 @@
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/brotli": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
|
||||
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@ -337,6 +335,14 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/clone": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/cluster-key-slot": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz",
|
||||
@ -416,6 +422,11 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dfa": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
|
||||
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="
|
||||
},
|
||||
"node_modules/doctrine": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||
@ -590,18 +601,6 @@
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esprima": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
|
||||
"bin": {
|
||||
"esparse": "bin/esparse.js",
|
||||
"esvalidate": "bin/esvalidate.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/esquery": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
|
||||
@ -647,8 +646,7 @@
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||
},
|
||||
"node_modules/fast-json-stable-stringify": {
|
||||
"version": "2.1.0",
|
||||
@ -712,6 +710,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fontkit": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.2.tgz",
|
||||
"integrity": "sha512-jc4k5Yr8iov8QfS6u8w2CnHWVmbOGtdBtOXMze5Y+QD966Rx6PEVWXSEGwXlsDlKtu1G12cJjcsybnqhSk/+LA==",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.4.2",
|
||||
"brotli": "^1.3.2",
|
||||
"clone": "^2.1.2",
|
||||
"dfa": "^1.2.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"restructure": "^3.0.0",
|
||||
"tiny-inflate": "^1.0.3",
|
||||
"unicode-properties": "^1.4.0",
|
||||
"unicode-trie": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
@ -821,6 +835,20 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/image-size": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.2.tgz",
|
||||
"integrity": "sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg==",
|
||||
"dependencies": {
|
||||
"queue": "6.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"image-size": "bin/image-size.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
@ -859,8 +887,7 @@
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
@ -913,6 +940,15 @@
|
||||
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/legacy-swc-helpers": {
|
||||
"name": "@swc/helpers",
|
||||
"version": "0.4.14",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.14.tgz",
|
||||
"integrity": "sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/levn": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||
@ -998,6 +1034,21 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/opentype.js": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-1.3.4.tgz",
|
||||
"integrity": "sha512-d2JE9RP/6uagpQAVtJoF0pJJA/fgai89Cc50Yp0EJHk+eLp6QQ7gBoblsnubRULNY132I0J1QKMJ+JTbMqz4sw==",
|
||||
"dependencies": {
|
||||
"string.prototype.codepointat": "^0.2.1",
|
||||
"tiny-inflate": "^1.0.3"
|
||||
},
|
||||
"bin": {
|
||||
"ot": "bin/ot"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
|
||||
@ -1015,6 +1066,11 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
|
||||
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@ -1083,6 +1139,14 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/queue": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
|
||||
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
|
||||
"dependencies": {
|
||||
"inherits": "~2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/redis": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/redis/-/redis-4.0.3.tgz",
|
||||
@ -1136,6 +1200,11 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/restructure": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.0.tgz",
|
||||
"integrity": "sha512-Xj8/MEIhhfj9X2rmD9iJ4Gga9EFqVlpMj3vfLnV2r/Mh5jRMryNV+6lWh9GdJtDBcBSPIqzRdfBQ3wDtNFv/uw=="
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
@ -1190,11 +1259,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||
},
|
||||
"node_modules/stream-parser": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/stream-parser/-/stream-parser-0.3.1.tgz",
|
||||
@ -1216,6 +1280,11 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"node_modules/string.prototype.codepointat": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz",
|
||||
"integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg=="
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
@ -1252,12 +1321,31 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/svgdom": {
|
||||
"version": "0.1.17",
|
||||
"resolved": "https://registry.npmjs.org/svgdom/-/svgdom-0.1.17.tgz",
|
||||
"integrity": "sha512-lhmZmXF0T2mCCrHnuNxgXnS72H3NJzTXz2oNoWxp+XGWpeuwlkJFko44ci9XEcrY1+zIE+8v99I561V010ZJyQ==",
|
||||
"dependencies": {
|
||||
"fontkit": "^2.0.2",
|
||||
"image-size": "^1.0.2",
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Fuzzyma"
|
||||
}
|
||||
},
|
||||
"node_modules/text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tiny-inflate": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
||||
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="
|
||||
},
|
||||
"node_modules/truncate-utf8-bytes": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
|
||||
@ -1266,6 +1354,11 @@
|
||||
"utf8-byte-length": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
@ -1290,6 +1383,24 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/unicode-properties": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
|
||||
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.0",
|
||||
"unicode-trie": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unicode-trie": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
|
||||
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
|
||||
"dependencies": {
|
||||
"pako": "^0.2.5",
|
||||
"tiny-inflate": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
@ -1340,41 +1451,6 @@
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/xmlbuilder2": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-3.0.2.tgz",
|
||||
"integrity": "sha512-h4MUawGY21CTdhV4xm3DG9dgsqyhDkZvVJBx88beqX8wJs3VgyGQgAn5VreHuae6unTQxh115aMK5InCVmOIKw==",
|
||||
"dependencies": {
|
||||
"@oozcitak/dom": "1.15.10",
|
||||
"@oozcitak/infra": "1.0.8",
|
||||
"@oozcitak/util": "8.3.8",
|
||||
"@types/node": "*",
|
||||
"js-yaml": "3.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlbuilder2/node_modules/argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"dependencies": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlbuilder2/node_modules/js-yaml": {
|
||||
"version": "3.14.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
|
||||
"integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
|
||||
"dependencies": {
|
||||
"argparse": "^1.0.7",
|
||||
"esprima": "^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
@ -1491,43 +1567,20 @@
|
||||
"integrity": "sha512-+nTn6EewVj3GlUXPuD3dgheWqo219jTxlo6R+pg24OeVvFHx9aFGGiyOgj3vBPhWUdRZ0xMcujXV5ki4fbLyMw==",
|
||||
"requires": {}
|
||||
},
|
||||
"@oozcitak/dom": {
|
||||
"version": "1.15.10",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.10.tgz",
|
||||
"integrity": "sha512-0JT29/LaxVgRcGKvHmSrUTEvZ8BXvZhGl2LASRUgHqDTC1M5g1pLmVv56IYNyt3bG2CUjDkc67wnyZC14pbQrQ==",
|
||||
"@svgdotjs/svg.js": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.0.tgz",
|
||||
"integrity": "sha512-Tr8p+QVP7y+QT1GBlq1Tt57IvedVH8zCPoYxdHLX0Oof3a/PqnC/tXAkVufv1JQJfsDHlH/UrjcDfgxSofqSNA=="
|
||||
},
|
||||
"@swc/helpers": {
|
||||
"version": "0.4.36",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.36.tgz",
|
||||
"integrity": "sha512-5lxnyLEYFskErRPenYItLRSge5DjrJngYKdVjRSrWfza9G6KkgHEXi0vUZiyUeMU5JfXH1YnvXZzSp8ul88o2Q==",
|
||||
"requires": {
|
||||
"@oozcitak/infra": "1.0.8",
|
||||
"@oozcitak/url": "1.0.4",
|
||||
"@oozcitak/util": "8.3.8"
|
||||
"legacy-swc-helpers": "npm:@swc/helpers@=0.4.14",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"@oozcitak/infra": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.8.tgz",
|
||||
"integrity": "sha512-JRAUc9VR6IGHOL7OGF+yrvs0LO8SlqGnPAMqyzOuFZPSZSXI7Xf2O9+awQPSMXgIWGtgUf/dA6Hs6X6ySEaWTg==",
|
||||
"requires": {
|
||||
"@oozcitak/util": "8.3.8"
|
||||
}
|
||||
},
|
||||
"@oozcitak/url": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-1.0.4.tgz",
|
||||
"integrity": "sha512-kDcD8y+y3FCSOvnBI6HJgl00viO/nGbQoCINmQ0h98OhnGITrWR3bOGfwYCthgcrV8AnTJz8MzslTQbC3SOAmw==",
|
||||
"requires": {
|
||||
"@oozcitak/infra": "1.0.8",
|
||||
"@oozcitak/util": "8.3.8"
|
||||
}
|
||||
},
|
||||
"@oozcitak/util": {
|
||||
"version": "8.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.8.tgz",
|
||||
"integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ=="
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "17.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.16.tgz",
|
||||
"integrity": "sha512-ydLaGVfQOQ6hI1xK2A5nVh8bl0OGoIfYMxPWHqqYe9bTkWCfqiVvZoh2I/QF2sNSkZzZyROBoTefIEI+PB6iIA=="
|
||||
},
|
||||
"acorn": {
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz",
|
||||
@ -1595,6 +1648,11 @@
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
@ -1605,6 +1663,14 @@
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"brotli": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
|
||||
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
|
||||
"requires": {
|
||||
"base64-js": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@ -1621,6 +1687,11 @@
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"clone": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="
|
||||
},
|
||||
"cluster-key-slot": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz",
|
||||
@ -1685,6 +1756,11 @@
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
|
||||
},
|
||||
"dfa": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
|
||||
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="
|
||||
},
|
||||
"doctrine": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||
@ -1811,11 +1887,6 @@
|
||||
"eslint-visitor-keys": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"esprima": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
|
||||
},
|
||||
"esquery": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
|
||||
@ -1849,8 +1920,7 @@
|
||||
"fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||
},
|
||||
"fast-json-stable-stringify": {
|
||||
"version": "2.1.0",
|
||||
@ -1894,6 +1964,22 @@
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
|
||||
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw=="
|
||||
},
|
||||
"fontkit": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.2.tgz",
|
||||
"integrity": "sha512-jc4k5Yr8iov8QfS6u8w2CnHWVmbOGtdBtOXMze5Y+QD966Rx6PEVWXSEGwXlsDlKtu1G12cJjcsybnqhSk/+LA==",
|
||||
"requires": {
|
||||
"@swc/helpers": "^0.4.2",
|
||||
"brotli": "^1.3.2",
|
||||
"clone": "^2.1.2",
|
||||
"dfa": "^1.2.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"restructure": "^3.0.0",
|
||||
"tiny-inflate": "^1.0.3",
|
||||
"unicode-properties": "^1.4.0",
|
||||
"unicode-trie": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
@ -1973,6 +2059,14 @@
|
||||
"integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
|
||||
"dev": true
|
||||
},
|
||||
"image-size": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.2.tgz",
|
||||
"integrity": "sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg==",
|
||||
"requires": {
|
||||
"queue": "6.0.2"
|
||||
}
|
||||
},
|
||||
"import-fresh": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
@ -2002,8 +2096,7 @@
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"is-extglob": {
|
||||
"version": "2.1.1",
|
||||
@ -2047,6 +2140,14 @@
|
||||
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"legacy-swc-helpers": {
|
||||
"version": "npm:@swc/helpers@0.4.14",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.14.tgz",
|
||||
"integrity": "sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==",
|
||||
"requires": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"levn": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||
@ -2114,6 +2215,15 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"opentype.js": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-1.3.4.tgz",
|
||||
"integrity": "sha512-d2JE9RP/6uagpQAVtJoF0pJJA/fgai89Cc50Yp0EJHk+eLp6QQ7gBoblsnubRULNY132I0J1QKMJ+JTbMqz4sw==",
|
||||
"requires": {
|
||||
"string.prototype.codepointat": "^0.2.1",
|
||||
"tiny-inflate": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"optionator": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
|
||||
@ -2128,6 +2238,11 @@
|
||||
"word-wrap": "^1.2.3"
|
||||
}
|
||||
},
|
||||
"pako": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
|
||||
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="
|
||||
},
|
||||
"parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@ -2181,6 +2296,14 @@
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
||||
"dev": true
|
||||
},
|
||||
"queue": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
|
||||
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
|
||||
"requires": {
|
||||
"inherits": "~2.0.3"
|
||||
}
|
||||
},
|
||||
"redis": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/redis/-/redis-4.0.3.tgz",
|
||||
@ -2219,6 +2342,11 @@
|
||||
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
|
||||
"dev": true
|
||||
},
|
||||
"restructure": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.0.tgz",
|
||||
"integrity": "sha512-Xj8/MEIhhfj9X2rmD9iJ4Gga9EFqVlpMj3vfLnV2r/Mh5jRMryNV+6lWh9GdJtDBcBSPIqzRdfBQ3wDtNFv/uw=="
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
@ -2261,11 +2389,6 @@
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true
|
||||
},
|
||||
"sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||
},
|
||||
"stream-parser": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/stream-parser/-/stream-parser-0.3.1.tgz",
|
||||
@ -2289,6 +2412,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"string.prototype.codepointat": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz",
|
||||
"integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg=="
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
@ -2313,12 +2441,27 @@
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"svgdom": {
|
||||
"version": "0.1.17",
|
||||
"resolved": "https://registry.npmjs.org/svgdom/-/svgdom-0.1.17.tgz",
|
||||
"integrity": "sha512-lhmZmXF0T2mCCrHnuNxgXnS72H3NJzTXz2oNoWxp+XGWpeuwlkJFko44ci9XEcrY1+zIE+8v99I561V010ZJyQ==",
|
||||
"requires": {
|
||||
"fontkit": "^2.0.2",
|
||||
"image-size": "^1.0.2",
|
||||
"sax": "^1.2.4"
|
||||
}
|
||||
},
|
||||
"text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
||||
"dev": true
|
||||
},
|
||||
"tiny-inflate": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
||||
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="
|
||||
},
|
||||
"truncate-utf8-bytes": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
|
||||
@ -2327,6 +2470,11 @@
|
||||
"utf8-byte-length": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
},
|
||||
"type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
@ -2342,6 +2490,24 @@
|
||||
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
|
||||
"dev": true
|
||||
},
|
||||
"unicode-properties": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
|
||||
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
|
||||
"requires": {
|
||||
"base64-js": "^1.3.0",
|
||||
"unicode-trie": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"unicode-trie": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
|
||||
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
|
||||
"requires": {
|
||||
"pako": "^0.2.5",
|
||||
"tiny-inflate": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"uri-js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
@ -2383,37 +2549,6 @@
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"dev": true
|
||||
},
|
||||
"xmlbuilder2": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-3.0.2.tgz",
|
||||
"integrity": "sha512-h4MUawGY21CTdhV4xm3DG9dgsqyhDkZvVJBx88beqX8wJs3VgyGQgAn5VreHuae6unTQxh115aMK5InCVmOIKw==",
|
||||
"requires": {
|
||||
"@oozcitak/dom": "1.15.10",
|
||||
"@oozcitak/infra": "1.0.8",
|
||||
"@oozcitak/util": "8.3.8",
|
||||
"@types/node": "*",
|
||||
"js-yaml": "3.14.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"requires": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"js-yaml": {
|
||||
"version": "3.14.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
|
||||
"integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
|
||||
"requires": {
|
||||
"argparse": "^1.0.7",
|
||||
"esprima": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
|
@ -1,19 +1,22 @@
|
||||
{
|
||||
"name": "bbb-export-annotations",
|
||||
"version": "0.0.1",
|
||||
"version": "2.0",
|
||||
"description": "BigBlueButton's Presentation Annotation Exporter",
|
||||
"scripts": {
|
||||
"start": "node master.js",
|
||||
"lint:fix": "eslint --fix **/*.js"
|
||||
"lint:fix": "eslint --fix '**/*.js' --ignore-pattern 'node_modules/'",
|
||||
"lint": "eslint '**/*.js' --ignore-pattern 'node_modules/'"
|
||||
},
|
||||
"dependencies": {
|
||||
"@svgdotjs/svg.js": "^3.2.0",
|
||||
"axios": "^1.6.5",
|
||||
"form-data": "^4.0.0",
|
||||
"opentype.js": "^1.3.4",
|
||||
"perfect-freehand": "^1.0.16",
|
||||
"probe-image-size": "^7.2.3",
|
||||
"redis": "^4.0.3",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"xmlbuilder2": "^3.0.2"
|
||||
"svgdom": "^0.1.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.20.0",
|
||||
@ -22,5 +25,6 @@
|
||||
"engines": {
|
||||
"node": ">=18.16.0",
|
||||
"npm": ">=9.5.0"
|
||||
}
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
|
287
bbb-export-annotations/shapes/Arrow.js
Normal file
287
bbb-export-annotations/shapes/Arrow.js
Normal file
@ -0,0 +1,287 @@
|
||||
import {Path, Marker, Defs} from '@svgdotjs/svg.js';
|
||||
import {Shape} from './Shape.js';
|
||||
import {TAU, circleFromThreePoints, normalize, rotate}
|
||||
from '../shapes/helpers.js';
|
||||
import {ColorTypes} from '../shapes/Shape.js';
|
||||
|
||||
/**
|
||||
* Creates an SVG path from Tldraw v2 arrow data.
|
||||
*
|
||||
* @class Arrow
|
||||
* @extends {Shape}
|
||||
*/
|
||||
export class Arrow extends Shape {
|
||||
/**
|
||||
* @param {Object} arrow - The arrow shape JSON.
|
||||
*/
|
||||
constructor(arrow) {
|
||||
super(arrow);
|
||||
this.start = this.props?.start;
|
||||
this.end = this.props?.end;
|
||||
this.arrowheadStart = this.props?.arrowheadStart;
|
||||
this.arrowheadEnd = this.props?.arrowheadEnd;
|
||||
this.bend = this.props?.bend;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the midpoint of a curve considering the bend of the line.
|
||||
* The midpoint is adjusted by the bend property to represent the
|
||||
* actual midpoint of a quadratic Bezier curve defined by the start
|
||||
* and end points with the bend as control point offset.
|
||||
*
|
||||
* @return {number[]} An array containing the x and y coordinates.
|
||||
*/
|
||||
getMidpoint() {
|
||||
const mid = [
|
||||
(this.start.x + this.end.x) / 2,
|
||||
(this.start.y + this.end.y) / 2];
|
||||
|
||||
const unitVector = normalize([
|
||||
this.end.x - this.start.x,
|
||||
this.end.y - this.start.y]);
|
||||
|
||||
const unitRotated = rotate(unitVector);
|
||||
const bendOffset = [
|
||||
unitRotated[0] * -this.bend,
|
||||
unitRotated[1] * -this.bend];
|
||||
|
||||
const middle = [
|
||||
mid[0] + bendOffset[0],
|
||||
mid[1] + bendOffset[1]];
|
||||
|
||||
return middle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the angle in radians between the line segments joining the start
|
||||
* point to the midpoint and the endpoint to the midpoint of a given set of
|
||||
* points. Assumes that `this.getMidpoint()` is a method which calculates the
|
||||
* midpoint between the start and end points, `this.start` is the start point,
|
||||
* and `this.end` is the end point of the line segments. The points are
|
||||
* objects with `x` and `y`properties representing their coordinates.
|
||||
* @return {number} Angle between the two line segments at the midpoint.
|
||||
*/
|
||||
getTheta() {
|
||||
const [middleX, middleY] = this.getMidpoint();
|
||||
|
||||
const ab = Math.hypot(this.start.y - middleY, this.start.x - middleX);
|
||||
const bc = Math.hypot(middleY - this.end.y, middleX - this.end.x);
|
||||
const ca = Math.hypot(this.end.y - this.start.y, this.end.x - this.start.x);
|
||||
const theta = Math.acos((bc * bc + ca * ca - ab * ab) / (2 * bc * ca)) * 2;
|
||||
return theta || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the path for the arrow, considering straight and curved lines.
|
||||
*
|
||||
* @return {string} - The SVG path string.
|
||||
*/
|
||||
constructPath() {
|
||||
const [startX, startY] = [this.start.x, this.start.y];
|
||||
const [endX, endY] = [this.end.x, this.end.y];
|
||||
const bend = this.bend;
|
||||
|
||||
const isStraightLine = (bend.toFixed(2) === '0.00');
|
||||
const straightLine = `M ${startX} ${startY} L ${endX} ${endY}`;
|
||||
|
||||
if (isStraightLine) {
|
||||
return straightLine;
|
||||
}
|
||||
|
||||
const [middleX, middleY] = this.getMidpoint();
|
||||
|
||||
const [,, r] = circleFromThreePoints(
|
||||
[startX, startY],
|
||||
[middleX, middleY],
|
||||
[endX, endY]);
|
||||
|
||||
// Could not calculate a circle
|
||||
if (!r) {
|
||||
return straightLine;
|
||||
}
|
||||
|
||||
const radius = r.toFixed(2);
|
||||
|
||||
// Whether to draw the longer arc
|
||||
const theta = this.getTheta();
|
||||
const largeArcFlag = theta > (TAU / 4) ? '1' : '0';
|
||||
|
||||
// Clockwise or counterclockwise
|
||||
const sweepFlag = ((endX - startX) * (middleY - startY) -
|
||||
(middleX - startX) * (endY - startY) > 0 ? '0' : '1');
|
||||
|
||||
const path = `M ${startX} ${startY} ` +
|
||||
`A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ` +
|
||||
`${endX} ${endY}`;
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the tangent angles at the start and end points of a path.
|
||||
* This method assumes that the path is an instance of a class with
|
||||
* a method `pointAt` which returns a point with `x` and `y` properties
|
||||
* given a distance along the path.
|
||||
*
|
||||
* @param {Object} path - SVG.js path with the `pointAt` method.
|
||||
* @return {Object} An object with `startAngleDegrees` and `endAngleDegrees`
|
||||
* properties.
|
||||
*/
|
||||
getTangentAngleAtEnds(path) {
|
||||
const length = path.length();
|
||||
const start = path.pointAt(0);
|
||||
const epsilon = 0.01; // A small value
|
||||
const end = path.pointAt(length);
|
||||
|
||||
// Get points just a little further along the path to calculate the tangent
|
||||
const startTangentPoint = path.pointAt(epsilon);
|
||||
const endTangentPoint = path.pointAt(length - epsilon);
|
||||
|
||||
// Calculate angles using Math.atan2 to find the slope of the tangent
|
||||
const startAngleRadians = Math.atan2(
|
||||
startTangentPoint.y - start.y,
|
||||
startTangentPoint.x - start.x) + + TAU / 2;
|
||||
|
||||
const endAngleRadians = Math.atan2(
|
||||
end.y - endTangentPoint.y,
|
||||
end.x - endTangentPoint.x);
|
||||
|
||||
// Convert to degrees
|
||||
const startAngleDegrees = startAngleRadians * (360 / TAU);
|
||||
const endAngleDegrees = endAngleRadians * (360 / TAU);
|
||||
|
||||
return {startAngleDegrees, endAngleDegrees};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a marker element with specified attributes
|
||||
* and shape based on the type. The marker is configured with default
|
||||
* properties which can be overridden according to the type.
|
||||
* The marker type determines the path and fill of the SVG element.
|
||||
*
|
||||
* @param {string} type - One of 'arrow', 'diamond', 'triangle', 'inverted',
|
||||
* 'square', 'dot', 'bar'.
|
||||
* @param {string} url - URL reference in SVG.
|
||||
* @param {number} [angle=0] - Angle in degrees for the marker.
|
||||
* @return {Marker} A new Marker instance.
|
||||
*/
|
||||
createMarker(type, url, angle = 0) {
|
||||
const arrowMarker = new Marker({
|
||||
id: url,
|
||||
viewBox: '0 0 10 10',
|
||||
refX: '5',
|
||||
refY: '5',
|
||||
markerWidth: '6',
|
||||
markerHeight: '6',
|
||||
orient: angle,
|
||||
});
|
||||
|
||||
const fillColor = Shape.colorToHex(this.color, ColorTypes.FillColor);
|
||||
|
||||
switch (type) {
|
||||
case 'arrow':
|
||||
arrowMarker.path('M 0 0 L 10 5 L 0 10 Z').fill(this.shapeColor);
|
||||
break;
|
||||
case 'diamond':
|
||||
arrowMarker.path('M 5 0 L 10 5 L 5 10 L 0 5 z')
|
||||
.stroke(this.shapeColor)
|
||||
.fill(fillColor);
|
||||
break;
|
||||
case 'triangle':
|
||||
arrowMarker.path('M 0 0 L 10 5 L 0 10 Z')
|
||||
.stroke(this.shapeColor)
|
||||
.fill(fillColor);
|
||||
break;
|
||||
case 'inverted':
|
||||
arrowMarker.attr('orient', angle + 180);
|
||||
arrowMarker.path('M 0 0 L 10 5 L 0 10 Z')
|
||||
.stroke(this.shapeColor)
|
||||
.fill(fillColor);
|
||||
break;
|
||||
case 'square':
|
||||
arrowMarker.path('M 0 0 L 10 0 L 10 10 L 0 10 Z')
|
||||
.stroke(this.shapeColor)
|
||||
.fill(fillColor);
|
||||
break;
|
||||
case 'dot':
|
||||
const circleSize = 5;
|
||||
arrowMarker.attr('refX', '0');
|
||||
arrowMarker.attr('refY', circleSize / 2);
|
||||
arrowMarker.attr('markerUnits', 'strokeWidth');
|
||||
arrowMarker.attr('markerWidth', '6');
|
||||
arrowMarker.attr('markerHeight', '6');
|
||||
arrowMarker.stroke('context-stroke');
|
||||
arrowMarker.fill(fillColor);
|
||||
arrowMarker.circle(circleSize)
|
||||
.stroke(this.shapeColor)
|
||||
.fill(fillColor);
|
||||
break;
|
||||
case 'bar':
|
||||
arrowMarker.attr('refX', '0');
|
||||
arrowMarker.attr('refY', '2.5');
|
||||
arrowMarker.path('M 0 0 L 0 -5 L 2 -5 L 2 5 L 0 5 Z')
|
||||
.stroke(this.shapeColor)
|
||||
.fill(this.shapeColor);
|
||||
break;
|
||||
default:
|
||||
arrowMarker.path('M 0 0 L 10 5 L 0 10 z').fill(this.shapeColor);
|
||||
}
|
||||
return arrowMarker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the arrow object as an SVG group element.
|
||||
*
|
||||
* @return {G} - An SVG group element.
|
||||
*/
|
||||
draw() {
|
||||
const arrowGroup = this.shapeGroup;
|
||||
const arrowPath = new Path();
|
||||
const pathData = this.constructPath();
|
||||
|
||||
arrowPath.attr({
|
||||
'd': pathData,
|
||||
'stroke': this.shapeColor,
|
||||
'stroke-width': this.thickness,
|
||||
'style': this.dasharray,
|
||||
'fill': 'none',
|
||||
});
|
||||
|
||||
const angles = this.getTangentAngleAtEnds(arrowPath);
|
||||
|
||||
// If there are arrowheads, create the markers
|
||||
if (this.arrowheadStart !== 'none' || this.arrowheadEnd !== 'none') {
|
||||
const defs = new Defs();
|
||||
|
||||
// There is an arrowhead at the start
|
||||
if (this.arrowheadStart !== 'none') {
|
||||
const url = `${this.arrowheadStart}-${this.id}-start`;
|
||||
const startMarker = this.createMarker(
|
||||
this.arrowheadStart,
|
||||
url,
|
||||
angles.startAngleDegrees);
|
||||
|
||||
defs.add(startMarker);
|
||||
arrowPath.attr('marker-start', `url(#${url})`);
|
||||
}
|
||||
|
||||
// There is an arrowhead at the end
|
||||
if (this.arrowheadEnd !== 'none') {
|
||||
const url = `${this.arrowheadEnd}-${this.id}-end`;
|
||||
const endMarker = this.createMarker(
|
||||
this.arrowheadEnd,
|
||||
url,
|
||||
angles.endAngleDegrees);
|
||||
defs.add(endMarker);
|
||||
arrowPath.attr('marker-end', `url(#${url})`);
|
||||
}
|
||||
|
||||
arrowGroup.add(defs);
|
||||
}
|
||||
|
||||
arrowGroup.add(arrowPath);
|
||||
this.drawLabel(arrowGroup);
|
||||
|
||||
return arrowGroup;
|
||||
}
|
||||
}
|
43
bbb-export-annotations/shapes/ArrowDown.js
Normal file
43
bbb-export-annotations/shapes/ArrowDown.js
Normal file
@ -0,0 +1,43 @@
|
||||
import {Polygon as SVGPolygon} from '@svgdotjs/svg.js';
|
||||
import {Geo} from './Geo.js';
|
||||
|
||||
/**
|
||||
* Creates an SVG down-arrow shape.
|
||||
*
|
||||
* @class ArrowDown
|
||||
* @extends {Geo}
|
||||
*/
|
||||
export class ArrowDown extends Geo {
|
||||
/**
|
||||
* Draws a down arrow shape on the SVG canvas.
|
||||
* @return {G} Returns the SVG group element containing the down arrow.
|
||||
*/
|
||||
draw() {
|
||||
const w = this.w;
|
||||
const h = this.h + this.growY;
|
||||
const ox = w * 0.16;
|
||||
const oy = Math.min(w, h) * 0.38;
|
||||
|
||||
const points = [
|
||||
[ox, 0],
|
||||
[w - ox, 0],
|
||||
[w - ox, h - oy],
|
||||
[w, h - oy],
|
||||
[w / 2, h],
|
||||
[0, h - oy],
|
||||
[ox, h - oy],
|
||||
].map(([x, y]) => `${x},${y}`).join(' ');
|
||||
|
||||
const arrowGroup = this.shapeGroup;
|
||||
const arrow = new SVGPolygon();
|
||||
arrow.plot(points)
|
||||
.stroke({color: this.shapeColor, width: this.thickness})
|
||||
.style({dasharray: this.dasharray});
|
||||
|
||||
this.setFill(arrow);
|
||||
arrowGroup.add(arrow);
|
||||
this.drawLabel(arrowGroup);
|
||||
|
||||
return arrowGroup;
|
||||
}
|
||||
}
|
43
bbb-export-annotations/shapes/ArrowLeft.js
Normal file
43
bbb-export-annotations/shapes/ArrowLeft.js
Normal file
@ -0,0 +1,43 @@
|
||||
import {Polygon as SVGPolygon} from '@svgdotjs/svg.js';
|
||||
import {Geo} from './Geo.js';
|
||||
|
||||
/**
|
||||
* Creates an SVG left-arrow shape.
|
||||
*
|
||||
* @class ArrowLeft
|
||||
* @extends {Geo}
|
||||
*/
|
||||
export class ArrowLeft extends Geo {
|
||||
/**
|
||||
* Draws a left arrow shape on the SVG canvas.
|
||||
* @return {G} Returns the SVG group element containing the left arrow.
|
||||
*/
|
||||
draw() {
|
||||
const w = this.w;
|
||||
const h = this.h + this.growY;
|
||||
const ox = Math.min(w, h) * 0.38;
|
||||
const oy = h * 0.16;
|
||||
|
||||
const points = [
|
||||
[ox, 0],
|
||||
[ox, oy],
|
||||
[w, oy],
|
||||
[w, h - oy],
|
||||
[ox, h - oy],
|
||||
[ox, h],
|
||||
[0, h / 2],
|
||||
].map(([x, y]) => `${x},${y}`).join(' ');
|
||||
|
||||
const arrowGroup = this.shapeGroup;
|
||||
const arrow = new SVGPolygon();
|
||||
arrow.plot(points)
|
||||
.stroke({color: this.shapeColor, width: this.thickness})
|
||||
.style({dasharray: this.dasharray});
|
||||
|
||||
this.setFill(arrow);
|
||||
arrowGroup.add(arrow);
|
||||
this.drawLabel(arrowGroup);
|
||||
|
||||
return arrowGroup;
|
||||
}
|
||||
}
|
43
bbb-export-annotations/shapes/ArrowRight.js
Normal file
43
bbb-export-annotations/shapes/ArrowRight.js
Normal file
@ -0,0 +1,43 @@
|
||||
import {Polygon as SVGPolygon} from '@svgdotjs/svg.js';
|
||||
import {Geo} from './Geo.js';
|
||||
|
||||
/**
|
||||
* Creates an SVG right-arrow shape.
|
||||
*
|
||||
* @class ArrowRight
|
||||
* @extends {Geo}
|
||||
*/
|
||||
export class ArrowRight extends Geo {
|
||||
/**
|
||||
* Draws a right arrow shape on the SVG canvas.
|
||||
* @return {G} Returns the SVG group element containing the right arrow.
|
||||
*/
|
||||
draw() {
|
||||
const w = this.w;
|
||||
const h = this.h + this.growY;
|
||||
const ox = Math.min(w, h) * 0.38;
|
||||
const oy = h * 0.16;
|
||||
|
||||
const points = [
|
||||
[0, oy],
|
||||
[w - ox, oy],
|
||||
[w - ox, 0],
|
||||
[w, h / 2],
|
||||
[w - ox, h],
|
||||
[w - ox, h - oy],
|
||||
[0, h - oy],
|
||||
].map(([x, y]) => `${x},${y}`).join(' ');
|
||||
|
||||
const arrowGroup = this.shapeGroup;
|
||||
const arrow = new SVGPolygon();
|
||||
arrow.plot(points)
|
||||
.stroke({color: this.shapeColor, width: this.thickness})
|
||||
.style({dasharray: this.dasharray});
|
||||
|
||||
this.setFill(arrow);
|
||||
arrowGroup.add(arrow);
|
||||
this.drawLabel(arrowGroup);
|
||||
|
||||
return arrowGroup;
|
||||
}
|
||||
}
|
43
bbb-export-annotations/shapes/ArrowUp.js
Normal file
43
bbb-export-annotations/shapes/ArrowUp.js
Normal file
@ -0,0 +1,43 @@
|
||||
import {Polygon as SVGPolygon} from '@svgdotjs/svg.js';
|
||||
import {Geo} from './Geo.js';
|
||||
|
||||
/**
|
||||
* Creates an SVG up-arrow shape.
|
||||
*
|
||||
* @class ArrowUp
|
||||
* @extends {Geo}
|
||||
*/
|
||||
export class ArrowUp extends Geo {
|
||||
/**
|
||||
* Draws an up arrow shape on the SVG canvas.
|
||||
* @return {G} Returns the SVG group element containing the up arrow.
|
||||
*/
|
||||
draw() {
|
||||
const w = this.w;
|
||||
const h = this.h + this.growY;
|
||||
const ox = w * 0.16;
|
||||
const oy = Math.min(w, h) * 0.38;
|
||||
|
||||
const points = [
|
||||
[w / 2, 0],
|
||||
[w, oy],
|
||||
[w - ox, oy],
|
||||
[w - ox, h],
|
||||
[ox, h],
|
||||
[ox, oy],
|
||||
[0, oy],
|
||||
].map(([x, y]) => `${x},${y}`).join(' ');
|
||||
|
||||
const arrowGroup = this.shapeGroup;
|
||||
const arrow = new SVGPolygon();
|
||||
arrow.plot(points)
|
||||
.stroke({color: this.shapeColor, width: this.thickness})
|
||||
.style({dasharray: this.dasharray});
|
||||
|
||||
this.setFill(arrow);
|
||||
arrowGroup.add(arrow);
|
||||
this.drawLabel(arrowGroup);
|
||||
|
||||
return arrowGroup;
|
||||
}
|
||||
}
|
67
bbb-export-annotations/shapes/Checkbox.js
Normal file
67
bbb-export-annotations/shapes/Checkbox.js
Normal file
@ -0,0 +1,67 @@
|
||||
import {Line} from '@svgdotjs/svg.js';
|
||||
import {Rectangle} from './Rectangle.js';
|
||||
|
||||
/**
|
||||
* Creates an SVG "Checkbox" shape, which is a rectangle with a checkmark in it.
|
||||
*
|
||||
* @class Checkbox
|
||||
* @extends {Rectangle}
|
||||
*/
|
||||
export class Checkbox extends Rectangle {
|
||||
/**
|
||||
* Gets the lines to draw a checkmark inside a given width and height.
|
||||
* @param {number} w The width of the bounding rectangle.
|
||||
* @param {number} h The height of the bounding rectangle.
|
||||
* @return {Array} An array of lines, each defined by two arrays
|
||||
* representing points.
|
||||
* @see {@link https://github.com/tldraw/tldraw/blob/main/packages/tldraw/src/lib/shapes/geo/GeoShapeUtil.tsx} Adapted from Tldraw.
|
||||
*/
|
||||
static getCheckBoxLines(w, h) {
|
||||
const size = Math.min(w, h) * 0.82;
|
||||
const ox = (w - size) / 2;
|
||||
const oy = (h - size) / 2;
|
||||
|
||||
const clampX = (x) => Math.max(0, Math.min(w, x));
|
||||
const clampY = (y) => Math.max(0, Math.min(h, y));
|
||||
|
||||
return [
|
||||
[
|
||||
[clampX(ox + size * 0.25), clampY(oy + size * 0.52)],
|
||||
[clampX(ox + size * 0.45), clampY(oy + size * 0.82)],
|
||||
],
|
||||
[
|
||||
[clampX(ox + size * 0.45), clampY(oy + size * 0.82)],
|
||||
[clampX(ox + size * 0.82), clampY(oy + size * 0.22)],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a "Checkbox" shape on the SVG canvas.
|
||||
* @return {G} Returns the SVG group element containing
|
||||
* the rectangle and the checkmark.
|
||||
*/
|
||||
draw() {
|
||||
// Draw the base rectangle
|
||||
const rectGroup = super.draw();
|
||||
|
||||
// Get the lines for the checkmark
|
||||
const lines = Checkbox.getCheckBoxLines(this.w, this.h + this.growY);
|
||||
|
||||
lines.forEach(([start, end]) => {
|
||||
const line = new Line();
|
||||
line.plot(start[0], start[1], end[0], end[1])
|
||||
.stroke({color: this.shapeColor,
|
||||
width: this.thickness,
|
||||
linecap: 'round'})
|
||||
.style({dasharray: this.dasharray});
|
||||
|
||||
// Add the line to the group
|
||||
rectGroup.add(line);
|
||||
});
|
||||
|
||||
this.drawLabel(rectGroup);
|
||||
|
||||
return rectGroup;
|
||||
}
|
||||
}
|
309
bbb-export-annotations/shapes/Cloud.js
Normal file
309
bbb-export-annotations/shapes/Cloud.js
Normal file
@ -0,0 +1,309 @@
|
||||
import {Path} from '@svgdotjs/svg.js';
|
||||
import {Geo} from './Geo.js';
|
||||
import {angle, rng, TAU, getPointOnCircle, calculateDistance,
|
||||
clockwiseAngleDist} from '../shapes/helpers.js';
|
||||
/**
|
||||
* Class representing a Cloud shape.
|
||||
* @see {@link https://github.com/tldraw/tldraw/blob/main/packages/tldraw/src/lib/shapes/geo/cloudOutline.ts} Adapted from Tldraw.
|
||||
*/
|
||||
export class Cloud extends Geo {
|
||||
/**
|
||||
* Generate points on an arc between two given points.
|
||||
*
|
||||
* @param {Object} startPoint - Starting point with 'x' and 'y' properties.
|
||||
* @param {Object} endPoint - End point with 'x' and 'y' properties.
|
||||
* @param {Object|null} center - Center point with 'x' and 'y' properties
|
||||
* @param {number} radius - The radius of the circle.
|
||||
* @param {number} numPoints - The number of points to generate along the arc.
|
||||
* @return {Array} Array of point objects representing the points on the arc.
|
||||
*/
|
||||
static pointsOnArc(startPoint, endPoint, center, radius, numPoints) {
|
||||
if (center === null) {
|
||||
return [startPoint, endPoint];
|
||||
}
|
||||
|
||||
const results = [];
|
||||
const startAngle = angle(center, startPoint);
|
||||
const endAngle = angle(center, endPoint);
|
||||
const l = clockwiseAngleDist(startAngle, endAngle);
|
||||
|
||||
for (let i = 0; i < numPoints; i++) {
|
||||
const t = i / (numPoints - 1);
|
||||
const angle = startAngle + l * t;
|
||||
const point = getPointOnCircle(center.x, center.y, radius, angle);
|
||||
results.push(point);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to get points on the "pill" shape.
|
||||
*
|
||||
* @static
|
||||
* @param {number} width - The width of the pill shape.
|
||||
* @param {number} height - The height of the pill shape.
|
||||
* @param {number} numPoints - The number of points to generate.
|
||||
* @return {Array} - Array of points on the pill shape.
|
||||
*/
|
||||
static getPillPoints(width, height, numPoints) {
|
||||
const radius = Math.min(width, height) / 2;
|
||||
const longSide = Math.max(width, height) - radius * 2;
|
||||
const circumference = TAU * radius + 2 * longSide;
|
||||
const spacing = circumference / numPoints;
|
||||
|
||||
const sections = width > height ?
|
||||
[
|
||||
{type: 'straight', start: {x: radius, y: 0}, delta: {x: 1, y: 0}},
|
||||
{type: 'arc', center: {x: width - radius, y: radius},
|
||||
startAngle: -TAU / 4},
|
||||
{type: 'straight', start: {x: width - radius, y: height},
|
||||
delta: {x: -1, y: 0}},
|
||||
{type: 'arc', center: {x: radius, y: radius}, startAngle: TAU / 4},
|
||||
] :
|
||||
[
|
||||
{type: 'straight', start: {x: width, y: radius}, delta: {x: 0, y: 1}},
|
||||
{type: 'arc', center: {x: radius, y: height - radius}, startAngle: 0},
|
||||
{type: 'straight', start: {x: 0, y: height - radius},
|
||||
delta: {x: 0, y: - 1}},
|
||||
{type: 'arc', center: {x: radius, y: radius}, startAngle: TAU / 2},
|
||||
];
|
||||
|
||||
let sectionOffset = 0;
|
||||
const points = [];
|
||||
for (let i = 0; i < numPoints; i++) {
|
||||
const section = sections[0];
|
||||
if (section.type === 'straight') {
|
||||
points.push({
|
||||
x: section.start.x + section.delta.x * sectionOffset,
|
||||
y: section.start.y + section.delta.y * sectionOffset,
|
||||
});
|
||||
} else {
|
||||
points.push(getPointOnCircle(
|
||||
section.center.x,
|
||||
section.center.y,
|
||||
radius,
|
||||
section.startAngle + sectionOffset / radius,
|
||||
));
|
||||
}
|
||||
sectionOffset += spacing;
|
||||
let sectionLength =
|
||||
section.type === 'straight' ? longSide : (TAU / 2) * radius;
|
||||
while (sectionOffset > sectionLength) {
|
||||
sectionOffset -= sectionLength;
|
||||
sections.push(sections.shift());
|
||||
sectionLength =
|
||||
sections[0].type === 'straight' ? longSide : (TAU / 2) * radius;
|
||||
}
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a numerical value based on the given size parameter.
|
||||
*
|
||||
* @static
|
||||
* @param {string} size - The size style, one of: 's', 'm', 'l', 'xl'.
|
||||
* @return {number} The numerical value corresponding to the given size.
|
||||
* @throws Will default to 130 if the size parameter doesn't match any case.
|
||||
*/
|
||||
static switchSize(size) {
|
||||
switch (size) {
|
||||
case 's':
|
||||
return 50;
|
||||
case 'm':
|
||||
return 70;
|
||||
case 'l':
|
||||
return 100;
|
||||
case 'xl':
|
||||
return 130;
|
||||
default:
|
||||
return 130;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the circumference of a pill shape.
|
||||
*
|
||||
* A pill shape is a rectangle with semi-circular ends. The function
|
||||
* calculates the total distance around the shape using its width and height.
|
||||
*
|
||||
* @static
|
||||
* @param {number} width - The width of the pill shape.
|
||||
* @param {number} height - The height of the pill shape.
|
||||
* @return {number} The circumference of the pill shape.
|
||||
*/
|
||||
static getPillCircumference(width, height) {
|
||||
const radius = Math.min(width, height) / 2;
|
||||
const longSide = Math.max(width, height) - radius * 2;
|
||||
|
||||
return TAU * radius + 2 * longSide;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get arcs for generating a cloud shape.
|
||||
*
|
||||
* @static
|
||||
* @param {number} width - The width of the cloud.
|
||||
* @param {number} height - The height of the cloud.
|
||||
* @param {string} seed - The random seed for the cloud.
|
||||
* @param {Object} size - The size style for the cloud.
|
||||
* @return {Array} An array of arcs data.
|
||||
*/
|
||||
static getCloudArcs(width, height, seed, size) {
|
||||
const getRandom = rng(seed);
|
||||
|
||||
const pillCircumference = Cloud.getPillCircumference(width, height);
|
||||
|
||||
const numBumps = Math.max(
|
||||
Math.ceil(pillCircumference / Cloud.switchSize(size)),
|
||||
6,
|
||||
Math.ceil(pillCircumference / Math.min(width, height)),
|
||||
);
|
||||
|
||||
const targetBumpProtrusion = (pillCircumference / numBumps) * 0.2;
|
||||
|
||||
const innerWidth = Math.max(width - targetBumpProtrusion * 2, 1);
|
||||
const innerHeight = Math.max(height - targetBumpProtrusion * 2, 1);
|
||||
const paddingX = (width - innerWidth) / 2;
|
||||
const paddingY = (height - innerHeight) / 2;
|
||||
|
||||
const distanceBetweenPointsOnPerimeter =
|
||||
Cloud.getPillCircumference(innerWidth, innerHeight) / numBumps;
|
||||
|
||||
let bumpPoints = Cloud.getPillPoints(innerWidth, innerHeight, numBumps);
|
||||
bumpPoints = bumpPoints.map((p) => {
|
||||
return {
|
||||
x: p.x + paddingX,
|
||||
y: p.y + paddingY,
|
||||
};
|
||||
});
|
||||
|
||||
const maxWiggleX = width < 20 ? 0 : targetBumpProtrusion * 0.3;
|
||||
const maxWiggleY = height < 20 ? 0 : targetBumpProtrusion * 0.3;
|
||||
const wiggledPoints = bumpPoints.slice(0);
|
||||
|
||||
for (let i = 0; i < Math.floor(numBumps / 2); i++) {
|
||||
wiggledPoints[i].x += getRandom() * maxWiggleX;
|
||||
wiggledPoints[i].y += getRandom() * maxWiggleY;
|
||||
wiggledPoints[numBumps - i - 1].x += getRandom() * maxWiggleX;
|
||||
wiggledPoints[numBumps - i - 1].y += getRandom() * maxWiggleY;
|
||||
}
|
||||
|
||||
const arcs = [];
|
||||
|
||||
for (let i = 0; i < wiggledPoints.length; i++) {
|
||||
const j = i === wiggledPoints.length - 1 ? 0 : i + 1;
|
||||
const leftWigglePoint = wiggledPoints[i];
|
||||
const rightWigglePoint = wiggledPoints[j];
|
||||
const leftPoint = bumpPoints[i];
|
||||
const rightPoint = bumpPoints[j];
|
||||
|
||||
const midPoint = {
|
||||
x: (leftPoint.x + rightPoint.x) / 2,
|
||||
y: (leftPoint.y + rightPoint.y) / 2,
|
||||
};
|
||||
|
||||
const offsetAngle = Math.atan2(rightPoint.y - leftPoint.y,
|
||||
rightPoint.x - leftPoint.x) - TAU;
|
||||
|
||||
const distanceBetweenOriginalPoints =
|
||||
Math.sqrt(Math.pow(rightPoint.x - leftPoint.x, 2) +
|
||||
Math.pow(rightPoint.y - leftPoint.y, 2));
|
||||
|
||||
const curvatureOffset =
|
||||
distanceBetweenPointsOnPerimeter - distanceBetweenOriginalPoints;
|
||||
|
||||
const distanceBetweenWigglePoints =
|
||||
Math.sqrt(Math.pow(rightWigglePoint.x - leftWigglePoint.x, 2) +
|
||||
Math.pow(rightWigglePoint.y - leftWigglePoint.y, 2));
|
||||
|
||||
const relativeSize =
|
||||
distanceBetweenWigglePoints / distanceBetweenOriginalPoints;
|
||||
const finalDistance = (Math.max(paddingX, paddingY) +
|
||||
curvatureOffset) * relativeSize;
|
||||
|
||||
const arcPoint = {
|
||||
x: midPoint.x + Math.cos(offsetAngle) * finalDistance,
|
||||
y: midPoint.y + Math.sin(offsetAngle) * finalDistance,
|
||||
};
|
||||
|
||||
arcPoint.x = Math.min(Math.max(arcPoint.x, 0), width);
|
||||
arcPoint.y = Math.min(Math.max(arcPoint.y, 0), height);
|
||||
|
||||
arcs.push({
|
||||
leftPoint: leftWigglePoint,
|
||||
rightPoint: rightWigglePoint,
|
||||
arcPoint,
|
||||
});
|
||||
}
|
||||
|
||||
return arcs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an SVG path string to represent a cloud shape using arc segments.
|
||||
*
|
||||
* @param {number} width - The width of the cloud.
|
||||
* @param {number} height - The height of the cloud.
|
||||
* @param {number} seed - The seed value for randomization (if applicable).
|
||||
* @param {number} size - The size of the cloud.
|
||||
* @return {string} - An SVG path string representing the cloud.
|
||||
*/
|
||||
static cloudSvgPath(width, height, seed, size) {
|
||||
// Get cloud arcs based on input parameters
|
||||
const arcs = Cloud.getCloudArcs(width, height, seed, size);
|
||||
|
||||
// Initialize SVG path starting with the 'M' command
|
||||
// for the first arc's leftPoint
|
||||
const initialX = arcs[0].leftPoint.x.toFixed(2);
|
||||
const initialY = arcs[0].leftPoint.y.toFixed(2);
|
||||
let path = `M${initialX},${initialY}`;
|
||||
|
||||
// Loop through all arcs to construct the 'A' commands for the SVG path
|
||||
for (const {leftPoint, rightPoint, arcPoint} of arcs) {
|
||||
// Approximate radius through heuristic, as determining the true
|
||||
// radius from the circle formed by the three points proved numerically
|
||||
// unstable.
|
||||
const radius = calculateDistance(leftPoint, arcPoint).toFixed(2);
|
||||
|
||||
const endPointX = rightPoint.x.toFixed(2);
|
||||
const endPointY = rightPoint.y.toFixed(2);
|
||||
|
||||
path += `A${radius},${radius} 0 0, 1 ${endPointX},${endPointY}`;
|
||||
}
|
||||
|
||||
// Close the SVG path with 'Z'
|
||||
path += ' Z';
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a cloud shape on the SVG canvas. It uses a predefined SVG path
|
||||
* for the cloud shape, which is scaled to the dimensions of the instance.
|
||||
* @return {G} An SVG group element (`<g>`)
|
||||
* that contains the cloud path and label.
|
||||
*/
|
||||
draw() {
|
||||
const points = Cloud.cloudSvgPath(
|
||||
this.w,
|
||||
this.h + this.growY,
|
||||
this.id,
|
||||
this.size);
|
||||
|
||||
const cloudGroup = this.shapeGroup;
|
||||
const cloud = new Path({
|
||||
'd': points,
|
||||
'stroke': this.shapeColor,
|
||||
'stroke-width': this.thickness,
|
||||
'style': this.dasharray,
|
||||
});
|
||||
|
||||
this.setFill(cloud);
|
||||
cloudGroup.add(cloud);
|
||||
this.drawLabel(cloudGroup);
|
||||
|
||||
return cloudGroup;
|
||||
}
|
||||
}
|
43
bbb-export-annotations/shapes/Diamond.js
Normal file
43
bbb-export-annotations/shapes/Diamond.js
Normal file
@ -0,0 +1,43 @@
|
||||
import {Polygon as SVGPolygon} from '@svgdotjs/svg.js';
|
||||
import {Geo} from './Geo.js';
|
||||
|
||||
/**
|
||||
* Creates an SVG diamond shape from Tldraw v2 JSON data.
|
||||
*
|
||||
* @class Diamond
|
||||
* @extends {Geo}
|
||||
*/
|
||||
export class Diamond extends Geo {
|
||||
/**
|
||||
* Draws a diamond shape on the SVG canvas.
|
||||
* @return {G} Returns the SVG group element containing the diamond.
|
||||
*/
|
||||
draw() {
|
||||
const width = this.w;
|
||||
const height = this.h + this.growY;
|
||||
const halfWidth = width / 2;
|
||||
const halfHeight = height / 2;
|
||||
|
||||
// Shape begins from the upper left corner
|
||||
const points = [
|
||||
[0, halfHeight],
|
||||
[halfWidth, 0],
|
||||
[width, halfHeight],
|
||||
[halfWidth, height],
|
||||
].map((p) => p.join(',')).join(' ');
|
||||
|
||||
const diamondGroup = this.shapeGroup;
|
||||
const diamond = new SVGPolygon({
|
||||
points,
|
||||
'stroke': this.shapeColor,
|
||||
'stroke-width': this.thickness,
|
||||
'style': this.dasharray,
|
||||
});
|
||||
|
||||
this.setFill(diamond);
|
||||
diamondGroup.add(diamond);
|
||||
this.drawLabel(diamondGroup);
|
||||
|
||||
return diamondGroup;
|
||||
}
|
||||
}
|
126
bbb-export-annotations/shapes/Draw.js
Normal file
126
bbb-export-annotations/shapes/Draw.js
Normal file
@ -0,0 +1,126 @@
|
||||
import pkg from 'perfect-freehand';
|
||||
import {TAU} from '../shapes/helpers.js';
|
||||
import {Path} from '@svgdotjs/svg.js';
|
||||
import {Shape} from './Shape.js';
|
||||
|
||||
const {getStrokePoints, getStrokeOutlinePoints} = pkg;
|
||||
|
||||
/**
|
||||
* Creates an SVG path from Tldraw v2 pencil data.
|
||||
*
|
||||
* @class Draw
|
||||
* @extends {Shape}
|
||||
*/
|
||||
export class Draw extends Shape {
|
||||
/**
|
||||
* @param {Object} draw - The draw shape JSON.
|
||||
*/
|
||||
constructor(draw) {
|
||||
super(draw);
|
||||
this.segments = this.props?.segments;
|
||||
this.isClosed = this.props?.isClosed;
|
||||
this.isComplete = this.props?.isComplete;
|
||||
}
|
||||
|
||||
static simulatePressure = {
|
||||
easing: (t) => Math.sin((t * TAU) / 4),
|
||||
simulatePressure: true,
|
||||
};
|
||||
|
||||
static realPressure = {
|
||||
easing: (t) => t * t,
|
||||
simulatePressure: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Turns an array of points into a path of quadradic curves.
|
||||
* @param {Array} annotationPoints
|
||||
* @param {Boolean} closed - whether the path end and start
|
||||
* should be connected (default)
|
||||
* @return {Array} - an SVG quadratic curve path
|
||||
*/
|
||||
getSvgPath(annotationPoints, closed = true) {
|
||||
const svgPath = annotationPoints.reduce(
|
||||
(acc, [x0, y0], i, arr) => {
|
||||
if (!arr[i + 1]) return acc;
|
||||
const [x1, y1] = arr[i + 1];
|
||||
acc.push(x0.toFixed(2), y0.toFixed(2),
|
||||
((x0 + x1) / 2).toFixed(2),
|
||||
((y0 + y1) / 2).toFixed(2));
|
||||
return acc;
|
||||
},
|
||||
|
||||
['M', ...annotationPoints[0], 'Q'],
|
||||
);
|
||||
|
||||
if (closed) svgPath.push('Z');
|
||||
return svgPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the draw object as an SVG group element.
|
||||
*
|
||||
* @return {G} - An SVG group element.
|
||||
*/
|
||||
draw() {
|
||||
const shapePoints = this.segments[0]?.points;
|
||||
const shapePointsLength = shapePoints?.length || 0;
|
||||
const isDashDraw = (this.dash === 'draw');
|
||||
const drawGroup = this.shapeGroup;
|
||||
|
||||
const options = {
|
||||
size: 1 + this.thickness * 1.5,
|
||||
thinning: 0.65,
|
||||
streamline: 0.65,
|
||||
smoothing: 0.65,
|
||||
...(shapePoints[1]?.z === 0.5 ?
|
||||
this.simulatePressure : this.realPressure),
|
||||
last: this.isComplete,
|
||||
};
|
||||
|
||||
const strokePoints = getStrokePoints(shapePoints, options);
|
||||
|
||||
const drawPath = new Path();
|
||||
const fillShape = new Path();
|
||||
|
||||
const last = shapePoints[shapePointsLength - 1];
|
||||
|
||||
// Avoid single dots from not being drawn
|
||||
if (strokePoints[0].point[0] == last[0] &&
|
||||
strokePoints[0].point[1] == last[1]) {
|
||||
strokePoints.push({point: last});
|
||||
}
|
||||
|
||||
const solidPath = strokePoints.map((strokePoint) => strokePoint.point);
|
||||
const svgPath = this.getSvgPath(solidPath, this.isClosed);
|
||||
|
||||
fillShape.attr('d', svgPath);
|
||||
|
||||
// In case the background shape is the shape itself, add the stroke to it
|
||||
if (!isDashDraw) {
|
||||
fillShape.attr('stroke', this.shapeColor);
|
||||
fillShape.attr('stroke-width', this.thickness);
|
||||
fillShape.attr('style', this.dasharray);
|
||||
}
|
||||
|
||||
// Fill only applies for closed shapes
|
||||
if (!this.isClosed) {
|
||||
this.fill = 'none';
|
||||
}
|
||||
|
||||
this.setFill(fillShape);
|
||||
|
||||
if (isDashDraw) {
|
||||
const strokeOutlinePoints = getStrokeOutlinePoints(strokePoints, options);
|
||||
const svgPath = this.getSvgPath(strokeOutlinePoints);
|
||||
|
||||
drawPath.attr('fill', this.shapeColor);
|
||||
drawPath.attr('d', svgPath);
|
||||
}
|
||||
|
||||
drawGroup.add(fillShape);
|
||||
drawGroup.add(drawPath);
|
||||
|
||||
return drawGroup;
|
||||
}
|
||||
}
|
36
bbb-export-annotations/shapes/Ellipse.js
Normal file
36
bbb-export-annotations/shapes/Ellipse.js
Normal file
@ -0,0 +1,36 @@
|
||||
import {Ellipse as SVGEllipse} from '@svgdotjs/svg.js';
|
||||
import {Geo} from './Geo.js';
|
||||
|
||||
/**
|
||||
* Creates an SVG ellipse shape from Tldraw v2 JSON data.
|
||||
*
|
||||
* @class Ellipse
|
||||
* @extends {Geo}
|
||||
*/
|
||||
export class Ellipse extends Geo {
|
||||
/**
|
||||
* Draws an ellipse shape on the SVG canvas.
|
||||
* @return {G} Returns the SVG group element containing the ellipse.
|
||||
*/
|
||||
draw() {
|
||||
const rx = this.w / 2;
|
||||
const ry = (this.h + this.growY) / 2;
|
||||
|
||||
const ellipseGroup = this.shapeGroup;
|
||||
const ellipse = new SVGEllipse({
|
||||
'cx': rx.toFixed(2),
|
||||
'cy': ry.toFixed(2),
|
||||
'rx': rx.toFixed(2),
|
||||
'ry': ry.toFixed(2),
|
||||
'stroke': this.shapeColor,
|
||||
'stroke-width': this.thickness,
|
||||
'style': this.dasharray,
|
||||
});
|
||||
|
||||
this.setFill(ellipse);
|
||||
ellipseGroup.add(ellipse);
|
||||
this.drawLabel(ellipseGroup);
|
||||
|
||||
return ellipseGroup;
|
||||
}
|
||||
}
|
99
bbb-export-annotations/shapes/Frame.js
Normal file
99
bbb-export-annotations/shapes/Frame.js
Normal file
@ -0,0 +1,99 @@
|
||||
import {Shape} from './Shape.js';
|
||||
import {Rect, Text, ClipPath, Defs, G} from '@svgdotjs/svg.js';
|
||||
import {ColorTypes} from '../shapes/Shape.js';
|
||||
import {overlayAnnotation} from '../workers/process.js';
|
||||
|
||||
/**
|
||||
* Creates an SVG frame from Tldraw v2 pencil data.
|
||||
*
|
||||
* @class Frane
|
||||
* @extends {Shape}
|
||||
*/
|
||||
export class Frame extends Shape {
|
||||
/**
|
||||
* @param {Object} frame - The Frame shape JSON.
|
||||
*/
|
||||
constructor(frame) {
|
||||
super(frame);
|
||||
this.name = this.props?.name;
|
||||
this.w = this.props?.w;
|
||||
this.h = this.props?.h;
|
||||
this.children = frame.children;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the frame object as an SVG group element.
|
||||
*
|
||||
* @return {G} - An SVG group element.
|
||||
*/
|
||||
draw() {
|
||||
// Parent group
|
||||
const frameGroup = this.shapeGroup;
|
||||
|
||||
// Group for clipped elements
|
||||
const clipGroup = new G();
|
||||
|
||||
const fillColor = Shape.colorToHex(ColorTypes.SemiFillColor,
|
||||
ColorTypes.SemiFillColor);
|
||||
|
||||
const frameLabel = this.name || 'Frame';
|
||||
|
||||
// The text element is not clipped
|
||||
const textElement = new Text()
|
||||
.text(frameLabel)
|
||||
.move(0, -20)
|
||||
.font({
|
||||
'family': 'Arial',
|
||||
'size': 12,
|
||||
})
|
||||
.fill('black');
|
||||
|
||||
// The frame rectangle that is not clipped
|
||||
const frame = new Rect({
|
||||
'x': 0,
|
||||
'y': 0,
|
||||
'width': this.w,
|
||||
'height': this.h,
|
||||
'stroke': 'black',
|
||||
'stroke-width': 1,
|
||||
'fill': fillColor,
|
||||
});
|
||||
|
||||
// Create the clip path with the same properties as the frame
|
||||
const clipPath = new ClipPath().id(`clipFrame-${this.id}`);
|
||||
const clipFrame = new Rect({
|
||||
'x': 0,
|
||||
'y': 0,
|
||||
'width': this.w,
|
||||
'height': this.h,
|
||||
});
|
||||
|
||||
clipPath.add(clipFrame);
|
||||
|
||||
// Definitions for clip paths
|
||||
const defs = new Defs();
|
||||
defs.add(clipPath);
|
||||
|
||||
// Add defs to the parent group
|
||||
frameGroup.add(defs);
|
||||
|
||||
const children = this.children || [];
|
||||
|
||||
// Add the children to the clipGroup so they will be clipped
|
||||
children.forEach((child) => {
|
||||
overlayAnnotation(clipGroup, child);
|
||||
});
|
||||
|
||||
// Apply clipping to the clipGroup only
|
||||
clipGroup.clipWith(clipPath);
|
||||
|
||||
// Add non-clipped...
|
||||
frameGroup.add(frame);
|
||||
frameGroup.add(textElement);
|
||||
|
||||
// ...and clipped elements to the frame group
|
||||
frameGroup.add(clipGroup);
|
||||
|
||||
return frameGroup;
|
||||
}
|
||||
}
|
65
bbb-export-annotations/shapes/Geo.js
Normal file
65
bbb-export-annotations/shapes/Geo.js
Normal file
@ -0,0 +1,65 @@
|
||||
import {Shape} from './Shape.js';
|
||||
import {TAU} from './helpers.js';
|
||||
/**
|
||||
* Class representing geometric shapes.
|
||||
*
|
||||
* @class Geo
|
||||
* @extends {Shape}
|
||||
*/
|
||||
export class Geo extends Shape {
|
||||
/**
|
||||
* Creates an instance of Geo.
|
||||
*
|
||||
* @param {Object} geo - JSON containing geometric shape properties.
|
||||
*/
|
||||
constructor(geo) {
|
||||
super(geo);
|
||||
this.url = this.props?.url;
|
||||
this.font = this.props?.font;
|
||||
this.w = this.props?.w;
|
||||
this.h = this.props?.h;
|
||||
this.growY = this.props?.growY;
|
||||
this.align = this.props?.align;
|
||||
this.geo = this.props?.geo;
|
||||
this.verticalAlign = this.props?.verticalAlign;
|
||||
this.labelColor = this.props?.labelColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the vertices of a polygon given its dimensions and the number of sides.
|
||||
* @param {number} width The width of the bounding box for the polygon.
|
||||
* @param {number} height The height of the bounding box for the polygon.
|
||||
* @param {number} sides The number of sides for the polygon.
|
||||
* @return {Array} An array of objects with x and y coordinates for each vertex.
|
||||
* @see {@link https://github.com/tldraw/tldraw/blob/main/packages/editor/src/lib/primitives/utils.ts} Adapted from Tldraw.
|
||||
*/
|
||||
static getPolygonVertices(width, height, sides) {
|
||||
const cx = width / 2;
|
||||
const cy = height / 2;
|
||||
const pointsOnPerimeter = [];
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
|
||||
for (let i = 0; i < sides; i++) {
|
||||
const step = TAU / sides;
|
||||
const t = -(TAU / 4) + i * step;
|
||||
const x = cx + cx * Math.cos(t);
|
||||
const y = cy + cy * Math.sin(t);
|
||||
|
||||
if (x < minX) minX = x;
|
||||
if (y < minY) minY = y;
|
||||
|
||||
pointsOnPerimeter.push({x, y});
|
||||
}
|
||||
|
||||
if (minX !== 0 || minY !== 0) {
|
||||
for (let i = 0; i < pointsOnPerimeter.length; i++) {
|
||||
const pt = pointsOnPerimeter[i];
|
||||
pt.x -= minX;
|
||||
pt.y -= minY;
|
||||
}
|
||||
}
|
||||
|
||||
return pointsOnPerimeter;
|
||||
}
|
||||
}
|
42
bbb-export-annotations/shapes/Hexagon.js
Normal file
42
bbb-export-annotations/shapes/Hexagon.js
Normal file
@ -0,0 +1,42 @@
|
||||
import {Polygon as SVGPolygon} from '@svgdotjs/svg.js';
|
||||
import {Geo} from './Geo.js';
|
||||
|
||||
/**
|
||||
* Creates an SVG hexagon shape from Tldraw v2 JSON data.
|
||||
*
|
||||
* @class Hexagon
|
||||
* @extends {Geo}
|
||||
*/
|
||||
export class Hexagon extends Geo {
|
||||
/**
|
||||
* Draws a hexagon shape on the SVG canvas.
|
||||
* @return {G} Returns the SVG group element containing the hexagon.
|
||||
*/
|
||||
draw() {
|
||||
const width = this.w;
|
||||
const height = this.h + this.growY;
|
||||
const sides = 6;
|
||||
|
||||
// Get the vertices of the hexagon
|
||||
const pointsOnPerimeter = Geo.getPolygonVertices(width, height, sides);
|
||||
|
||||
// Convert the vertices to SVG polygon points format
|
||||
const points = pointsOnPerimeter.map((p) => `${p.x},${p.y}`).join(' ');
|
||||
|
||||
// Create the SVG polygon
|
||||
const hexagonGroup = this.shapeGroup;
|
||||
const hexagon = new SVGPolygon({
|
||||
points,
|
||||
'stroke': this.shapeColor,
|
||||
'stroke-width': this.thickness,
|
||||
'style': this.dasharray,
|
||||
});
|
||||
|
||||
// Fill the polygon if required
|
||||
this.setFill(hexagon);
|
||||
hexagonGroup.add(hexagon);
|
||||
this.drawLabel(hexagonGroup);
|
||||
|
||||
return hexagonGroup;
|
||||
}
|
||||
}
|
23
bbb-export-annotations/shapes/Highlight.js
Normal file
23
bbb-export-annotations/shapes/Highlight.js
Normal file
@ -0,0 +1,23 @@
|
||||
import {Draw} from './Draw.js';
|
||||
|
||||
/**
|
||||
* Represents a Highlight shape, extending the functionality of the Draw class.
|
||||
*
|
||||
* @class Highlight
|
||||
* @extends {Draw}
|
||||
*/
|
||||
export class Highlight extends Draw {
|
||||
/**
|
||||
* Creates an instance of the Highlight class.
|
||||
*
|
||||
* @param {Object} highlight - The highlighter's JSON data.
|
||||
*/
|
||||
constructor(highlight) {
|
||||
super(highlight);
|
||||
|
||||
this.fill = 'none';
|
||||
this.shapeColor = '#fedd00';
|
||||
this.thickness = this.thickness * 7;
|
||||
this.isClosed = false;
|
||||
}
|
||||
}
|
81
bbb-export-annotations/shapes/Line.js
Normal file
81
bbb-export-annotations/shapes/Line.js
Normal file
@ -0,0 +1,81 @@
|
||||
import {Path} from '@svgdotjs/svg.js';
|
||||
import {Shape} from './Shape.js';
|
||||
|
||||
/**
|
||||
* Creates an SVG path from Tldraw v2 line data.
|
||||
*
|
||||
* @class Line
|
||||
* @extends {Shape}
|
||||
*/
|
||||
export class Line extends Shape {
|
||||
/**
|
||||
* @param {Object} line - The line shape JSON.
|
||||
*/
|
||||
constructor(line) {
|
||||
super(line);
|
||||
this.handles = this.props?.handles;
|
||||
this.spline = this.props?.spline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the line type (spline, line), constructs the SVG path.
|
||||
*
|
||||
* @param {Object} handles - The vertex points and control points.
|
||||
* @param {string} spline - The type of spline ('cubic' or 'line').
|
||||
* @return {string} - The SVG path data.
|
||||
*/
|
||||
constructPath(handles, spline) {
|
||||
const start = handles.start;
|
||||
const end = handles.end;
|
||||
const ctl = handles['handle:a1V'];
|
||||
let path = `M${start.x},${start.y} `;
|
||||
|
||||
if (spline === 'cubic' && ctl) {
|
||||
// Compute adjusted control points to make curve pass through `ctl`
|
||||
const t = 0.5; // Assumes the curve passes through `ctl` at t = 0.5
|
||||
const b = {
|
||||
x: (ctl.x - (1-t) ** 3 * start.x - t ** 3 * end.x) / (3 * (1-t) * t),
|
||||
y: (ctl.y - (1-t) ** 3 * start.y - t ** 3 * end.y) / (3 * (1-t) * t),
|
||||
};
|
||||
const c = {
|
||||
x: (ctl.x - (1-t) ** 3 * start.x - t ** 3 * end.x) / (3 * (1-t) * t),
|
||||
y: (ctl.y - (1-t) ** 3 * start.y - t ** 3 * end.y) / (3 * (1-t) * t),
|
||||
};
|
||||
// Draw cubic spline
|
||||
path += `C${b.x},${b.y} ${c.x},${c.y} ${end.x},${end.y}`;
|
||||
} else if (spline === 'line' && ctl) {
|
||||
// Draw straight lines to and from control point
|
||||
path += `L${ctl.x},${ctl.y} L${end.x},${end.y}`;
|
||||
} else {
|
||||
// Draw straight line
|
||||
path += `L${end.x},${end.y}`;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the line object as an SVG group element.
|
||||
*
|
||||
* @return {G} - An SVG group element.
|
||||
*/
|
||||
draw() {
|
||||
const lineGroup = this.shapeGroup;
|
||||
const linePath = new Path();
|
||||
|
||||
const svgPath = this.constructPath(this.handles, this.spline);
|
||||
|
||||
linePath.attr({
|
||||
'd': svgPath,
|
||||
'stroke': this.shapeColor,
|
||||
'stroke-width': this.thickness,
|
||||
'style': this.dasharray,
|
||||
'fill': 'none',
|
||||
});
|
||||
|
||||
lineGroup.add(linePath);
|
||||
this.drawLabel(lineGroup);
|
||||
|
||||
return lineGroup;
|
||||
}
|
||||
}
|
60
bbb-export-annotations/shapes/Oval.js
Normal file
60
bbb-export-annotations/shapes/Oval.js
Normal file
@ -0,0 +1,60 @@
|
||||
import {Polygon as SVGPolygon} from '@svgdotjs/svg.js';
|
||||
import {Geo} from './Geo.js';
|
||||
import {TAU} from '../shapes/helpers.js';
|
||||
|
||||
/**
|
||||
* Creates an SVG oval shape from Tldraw v2 JSON data.
|
||||
*
|
||||
* @class Oval
|
||||
* @extends {Geo}
|
||||
*/
|
||||
export class Oval extends Geo {
|
||||
/**
|
||||
* Draws an oval shape on the SVG canvas.
|
||||
* @return {G} Returns the SVG group element containing the oval.
|
||||
* @see {@link https://github.com/tldraw/tldraw/blob/main/packages/editor/src/lib/primitives/geometry/Stadium2d.ts} Adapted from Tldraw.
|
||||
*/
|
||||
draw() {
|
||||
const w = Math.max(1, this.w);
|
||||
const h = Math.max(1, this.h + this.growY);
|
||||
const cx = w / 2;
|
||||
const cy = h / 2;
|
||||
|
||||
const len = 25; // Number of vertices to use for the oval
|
||||
const points = Array(len * 2 - 2).fill(null).map(() => []);
|
||||
|
||||
if (h > w) {
|
||||
for (let i = 0; i < len - 1; i++) {
|
||||
const t1 = -(TAU / 2) + ((TAU / 2) * i) / (len - 2);
|
||||
const t2 = ((TAU / 2) * i) / (len - 2);
|
||||
points[i] = [cx + cx * Math.cos(t1), cx + cx * Math.sin(t1)];
|
||||
points[i + (len - 1)] = [cx + cx * Math.cos(t2),
|
||||
h - cx + cx * Math.sin(t2)];
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < len - 1; i++) {
|
||||
const t1 = -(TAU / 4) + (TAU / 2 * i) / (len - 2);
|
||||
const t2 = (TAU / 4) + (TAU / 2 * -i) / (len - 2);
|
||||
points[i] = [w - cy + cy * Math.cos(t1), h - cy + cy * Math.sin(t1)];
|
||||
points[i + (len - 1)] = [cy - cy * Math.cos(t2),
|
||||
h - cy + cy * Math.sin(t2)];
|
||||
}
|
||||
}
|
||||
|
||||
const formattedPoints = points.map((p) => p.join(',')).join(' ');
|
||||
|
||||
const ovalGroup = this.shapeGroup;
|
||||
const oval = new SVGPolygon({
|
||||
'points': formattedPoints,
|
||||
'stroke': this.shapeColor,
|
||||
'stroke-width': this.thickness,
|
||||
'style': this.dasharray,
|
||||
});
|
||||
|
||||
this.setFill(oval);
|
||||
ovalGroup.add(oval);
|
||||
this.drawLabel(ovalGroup);
|
||||
|
||||
return ovalGroup;
|
||||
}
|
||||
}
|
43
bbb-export-annotations/shapes/Rectangle.js
Normal file
43
bbb-export-annotations/shapes/Rectangle.js
Normal file
@ -0,0 +1,43 @@
|
||||
import {Rect} from '@svgdotjs/svg.js';
|
||||
import {Geo} from './Geo.js';
|
||||
|
||||
/**
|
||||
* Creates an SVG rectangle shape from Tldraw v2 JSON data.
|
||||
*
|
||||
* @class Rectangle
|
||||
* @extends {Geo}
|
||||
*/
|
||||
export class Rectangle extends Geo {
|
||||
/**
|
||||
* Draws a rectangle shape based on the instance properties.
|
||||
*
|
||||
* @method draw
|
||||
* @return {G} An SVG group element containing the drawn rectangle shape.
|
||||
*
|
||||
*/
|
||||
draw() {
|
||||
const rectGroup = this.shapeGroup;
|
||||
|
||||
const rectangle = new Rect({
|
||||
'x': 0,
|
||||
'y': 0,
|
||||
'width': this.w,
|
||||
'height': this.h + this.growY,
|
||||
'stroke': this.shapeColor,
|
||||
'stroke-width': this.thickness,
|
||||
'style': this.dasharray,
|
||||
});
|
||||
|
||||
// Simulate perfect-freehand effect
|
||||
if (this.dash === 'draw') {
|
||||
rectangle.attr('rx', this.thickness);
|
||||
rectangle.attr('ry', this.thickness);
|
||||
}
|
||||
|
||||
this.setFill(rectangle);
|
||||
rectGroup.add(rectangle);
|
||||
this.drawLabel(rectGroup);
|
||||
|
||||
return rectGroup;
|
||||
}
|
||||
}
|
44
bbb-export-annotations/shapes/Rhombus.js
Normal file
44
bbb-export-annotations/shapes/Rhombus.js
Normal file
@ -0,0 +1,44 @@
|
||||
import {Polygon as SVGPolygon} from '@svgdotjs/svg.js';
|
||||
import {Geo} from './Geo.js';
|
||||
|
||||
/**
|
||||
* Creates an SVG rhombus shape from Tldraw v2 JSON data.
|
||||
*
|
||||
* @class Rhombus
|
||||
* @extends {Geo}
|
||||
*/
|
||||
export class Rhombus extends Geo {
|
||||
/**
|
||||
* Draws a rhombus shape on the SVG canvas.
|
||||
* @return {G} Returns the SVG group element containing the rhombus.
|
||||
*/
|
||||
draw() {
|
||||
const width = this.w;
|
||||
const height = this.h + this.growY;
|
||||
|
||||
// Internal angle between adjacent sides varies with width and height
|
||||
const offset = Math.min(width * 0.38, height * 0.38);
|
||||
|
||||
// Coordinates for the four vertices of the rhombus
|
||||
const points = [
|
||||
[offset, 0], // Top left vertex
|
||||
[width, 0], // Top right vertex
|
||||
[width - offset, height], // Bottom right vertex
|
||||
[0, height], // Bottom left vertex
|
||||
].map((p) => p.join(',')).join(' ');
|
||||
|
||||
const rhombusGroup = this.shapeGroup;
|
||||
const rhombus = new SVGPolygon({
|
||||
points,
|
||||
'stroke': this.shapeColor,
|
||||
'stroke-width': this.thickness,
|
||||
'style': this.dasharray,
|
||||
});
|
||||
|
||||
this.setFill(rhombus);
|
||||
rhombusGroup.add(rhombus);
|
||||
this.drawLabel(rhombusGroup);
|
||||
|
||||
return rhombusGroup;
|
||||
}
|
||||
}
|
507
bbb-export-annotations/shapes/Shape.js
Normal file
507
bbb-export-annotations/shapes/Shape.js
Normal file
@ -0,0 +1,507 @@
|
||||
import {Pattern, Line, Defs, Rect, G, Text, Tspan} from '@svgdotjs/svg.js';
|
||||
import {radToDegree} from '../shapes/helpers.js';
|
||||
import opentype from 'opentype.js';
|
||||
import fs from 'fs';
|
||||
/**
|
||||
* Represents a basic Tldraw shape on the whiteboard.
|
||||
*
|
||||
* @class Shape
|
||||
* @typedef {Object} ColorTypes
|
||||
* @property {'shape'} ShapeColor - Color for shape outlines or borders.
|
||||
* @property {'fill'} FillColor - Solid fill color inside the shape.
|
||||
* @property {'semi'} SemiFillColor - Semi fill shape color.
|
||||
* @property {'sticky'} StickyColor - Color for sticky notes.
|
||||
*/
|
||||
export class Shape {
|
||||
/**
|
||||
* Creates an instance of Shape.
|
||||
* @constructor
|
||||
* @param {Object} params - The shape's parameters.
|
||||
* @param {String} params.id - The the shape ID.
|
||||
* @param {Number} params.x - The shape's x-coordinate.
|
||||
* @param {Number} params.y - The shape's y-coordinate.
|
||||
* @param {Number} params.rotation - The shape's rotation angle in radians.
|
||||
* @param {Number} params.opacity - The shape's opacity.
|
||||
* @param {Object} params.props - Shape-specific properties.
|
||||
*/
|
||||
constructor({
|
||||
id,
|
||||
x,
|
||||
y,
|
||||
rotation,
|
||||
opacity,
|
||||
props,
|
||||
}) {
|
||||
this.id = id;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.rotation = rotation;
|
||||
this.opacity = opacity;
|
||||
this.props = props;
|
||||
|
||||
this.size = this.props?.size;
|
||||
this.color = this.props?.color;
|
||||
this.dash = this.props?.dash;
|
||||
this.fill = this.props?.fill;
|
||||
this.text = this.props?.text;
|
||||
|
||||
// Derived SVG properties
|
||||
this.thickness = Shape.getStrokeWidth(this.size);
|
||||
this.dasharray = Shape.determineDasharray(this.dash, this.size);
|
||||
this.shapeColor = Shape.colorToHex(this.color, ColorTypes.ShapeColor);
|
||||
|
||||
// SVG representation
|
||||
this.shapeGroup = new G({
|
||||
transform: this.getTransform(),
|
||||
opacity: this.opacity,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an SVG <defs> element with a pattern for filling the shape.
|
||||
*
|
||||
* @method getFillPattern
|
||||
* @param {String} shapeColor - The color to use for the pattern lines.
|
||||
* @return {Defs} An SVG <defs> element containing the pattern.
|
||||
*/
|
||||
getFillPattern(shapeColor) {
|
||||
const defs = new Defs();
|
||||
const pattern = new Pattern({
|
||||
id: `hash_pattern-${this.id}`,
|
||||
width: 8,
|
||||
height: 8,
|
||||
patternUnits: 'userSpaceOnUse',
|
||||
patternTransform: 'rotate(45 0 0)',
|
||||
});
|
||||
|
||||
pattern.add(new Rect({width: 8, height: 8, fill: 'white'}));
|
||||
pattern.add(new Line({'x1': 0, 'y1': 0, 'x2': 0, 'y2': 8,
|
||||
'stroke': shapeColor, 'stroke-width': 3.5,
|
||||
'stroke-dasharray': '4, 4'}));
|
||||
|
||||
defs.add(pattern);
|
||||
return defs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the appropriate fill style to the given SVG shape element based on
|
||||
* the object's `fill` property. It supports 'solid', 'semi', 'pattern', and
|
||||
* 'none' as fill options.
|
||||
* @param {SVGElement} shape - The element to be filled
|
||||
*/
|
||||
setFill(shape) {
|
||||
switch (this.fill) {
|
||||
case 'solid':
|
||||
const fillColor = Shape.colorToHex(this.color, ColorTypes.FillColor);
|
||||
shape.attr('fill', fillColor);
|
||||
break;
|
||||
|
||||
case 'semi':
|
||||
const semiColor = Shape.colorToHex(this.fill, ColorTypes.SemiFillColor);
|
||||
shape.attr('fill', semiColor);
|
||||
break;
|
||||
|
||||
case 'pattern':
|
||||
const shapeColor = Shape.colorToHex(this.color, ColorTypes.ShapeColor);
|
||||
const pattern = this.getFillPattern(shapeColor);
|
||||
this.shapeGroup.add(pattern);
|
||||
shape.attr('fill', `url(#hash_pattern-${this.id})`);
|
||||
break;
|
||||
|
||||
default:
|
||||
shape.attr('fill', 'none');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a transformation string for SVG elements based on the object's
|
||||
* rotation and position properties. The transformation includes translation,
|
||||
* rotation, and setting the transform origin to the center.
|
||||
*
|
||||
* @return {string} The SVG transform attribute value.
|
||||
*/
|
||||
getTransform() {
|
||||
const x = this.x.toFixed(2);
|
||||
const y = this.y.toFixed(2);
|
||||
const rotation = radToDegree(this.rotation);
|
||||
const translate = `translate(${x} ${y})`;
|
||||
const transformOrigin = 'transform-origin: center';
|
||||
const rotate = `rotate(${rotation})`;
|
||||
const transform = `${translate}; ${transformOrigin}; ${rotate}`;
|
||||
|
||||
return transform;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a tldraw color name to its corresponding HEX code.
|
||||
*
|
||||
* @param {string} color - The name of the color (e.g., 'blue', 'red').
|
||||
* @param {string} colorType - Context to select the appropriate mapping.
|
||||
* Valid values are 'shape', 'fill',
|
||||
* 'semi', and 'sticky'.
|
||||
*
|
||||
* @return {string} The HEX code for the given color and color type.
|
||||
* Returns '#0d0d0d' if not found.
|
||||
*/
|
||||
static colorToHex(color, colorType) {
|
||||
const colorMap = {
|
||||
'black': '#161616',
|
||||
'grey': '#9EA6B0',
|
||||
'light-violet': '#DD80F5',
|
||||
'violet': '#9C1FBE',
|
||||
'blue': '#3348E5',
|
||||
'light-blue': '#4099F5',
|
||||
'yellow': '#FDB365',
|
||||
'orange': '#F3500B',
|
||||
'green': '#148355',
|
||||
'light-green': '#38B845',
|
||||
'light-red': '#FC7075',
|
||||
'red': '#D61A25',
|
||||
};
|
||||
|
||||
const fillMap = {
|
||||
'black': '#E2E2E2',
|
||||
'grey': '#E7EAEC',
|
||||
'light-violet': '#F2E5F9',
|
||||
'violet': '#E7D3EF',
|
||||
'blue': '#D4D8F6',
|
||||
'light-blue': '#D6E8F9',
|
||||
'yellow': '#F8ECE0',
|
||||
'orange': '#F5DBCA',
|
||||
'green': '#CAE5DC',
|
||||
'light-green': '#D4EED9',
|
||||
'light-red': '#F0D1D3',
|
||||
'red': '#F0D1D3',
|
||||
};
|
||||
|
||||
const stickyMap = {
|
||||
'black': '#FEC78C',
|
||||
'grey': '#B6BDC3',
|
||||
'light-violet': '#E4A1F7',
|
||||
'violet': '#B65ACF',
|
||||
'blue': '#6476EC',
|
||||
'light-blue': '#6FB3F6',
|
||||
'yellow': '#FEC78C',
|
||||
'orange': '#F57D48',
|
||||
'green': '#47A37F',
|
||||
'light-green': '#64C46F',
|
||||
'light-red': '#FC9598',
|
||||
'red': '#E05458',
|
||||
};
|
||||
|
||||
const semiFillMap = {
|
||||
'semi': '#F5F9F7',
|
||||
};
|
||||
|
||||
const colors = {
|
||||
shape: colorMap,
|
||||
fill: fillMap,
|
||||
semi: semiFillMap,
|
||||
sticky: stickyMap,
|
||||
};
|
||||
|
||||
return colors[colorType][color] || '#0d0d0d';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines SVG style attributes based on the dash type.
|
||||
*
|
||||
* @param {string} dash - The type of dash ('dashed', 'dotted').
|
||||
* @param {string} size - The size ('s', 'm', 'l', 'xl').
|
||||
*
|
||||
* @return {string} A string representing the SVG attributes
|
||||
* for the given dash and gap.
|
||||
*/
|
||||
static determineDasharray(dash, size) {
|
||||
const gapSettings = {
|
||||
'dashed': {
|
||||
's': '4.37 4.91',
|
||||
'm': '8.16 10.21',
|
||||
'l': '11.85 14.81',
|
||||
'xl': '21.41 32.12',
|
||||
'default': '8 8',
|
||||
},
|
||||
|
||||
'dotted': {
|
||||
's': '0.02 4',
|
||||
'm': '0.03 8',
|
||||
'l': '0.05 12',
|
||||
'xl': '0.12 16',
|
||||
'default': '0.03 8',
|
||||
},
|
||||
};
|
||||
|
||||
const gap = gapSettings[dash]?.[size] ||
|
||||
gapSettings[dash]?.['default'] ||
|
||||
'0';
|
||||
|
||||
const dashSettings = {
|
||||
'dashed': `stroke-linecap:butt;stroke-dasharray:${gap};`,
|
||||
'dotted': `stroke-linecap:round;stroke-dasharray:${gap};`,
|
||||
};
|
||||
|
||||
return dashSettings[dash] || 'stroke-linejoin:round;stroke-linecap:round;';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the stroke width based on the size.
|
||||
*
|
||||
* @param {string} size - The size of the stroke ('s', 'm', 'l', 'xl').
|
||||
* @return {number} - The corresponding stroke width.
|
||||
*/
|
||||
static getStrokeWidth(size) {
|
||||
const strokeWidths = {
|
||||
's': 2,
|
||||
'm': 3.5,
|
||||
'l': 5,
|
||||
'xl': 7.5,
|
||||
};
|
||||
|
||||
return strokeWidths[size] || 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the font size in pixels.
|
||||
*
|
||||
* @param {string} size - The size of the font ('s', 'm', 'l', 'xl').
|
||||
* @return {number} - The corresponding font size, in pixels.
|
||||
*/
|
||||
static determineFontSize(size) {
|
||||
const fontSizes = {
|
||||
's': 26,
|
||||
'm': 36,
|
||||
'l': 54,
|
||||
'xl': 64,
|
||||
};
|
||||
|
||||
return fontSizes[size] || 16;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aligns horizontally based on the given alignment type.
|
||||
*
|
||||
* @param {string} align - One of ('start', 'middle', 'end').
|
||||
* @param {number} width - The width of the container.
|
||||
* @return {string} The calculated horizontal position as a string with
|
||||
* two decimal places. Coordinates are relative to the container.
|
||||
* @static
|
||||
*/
|
||||
static alignHorizontally(align, width) {
|
||||
switch (align) {
|
||||
case 'middle': return (width / 2).toFixed(2);
|
||||
case 'end': return (width).toFixed(2);
|
||||
default: return '0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aligns vertically based on the given alignment type.
|
||||
*
|
||||
* @param {string} align - One of ('start', 'middle', 'end').
|
||||
* @param {number} height - The height of the container.
|
||||
* @return {string} The calculated vertical position as a string with
|
||||
* two decimal places. Coordinates are relative to the container.
|
||||
* @static
|
||||
*/
|
||||
static alignVertically(align, height) {
|
||||
switch (align) {
|
||||
case 'middle': return (height / 2).toFixed(2);
|
||||
case 'end': return height.toFixed(2);
|
||||
default: return '0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the font to use based on the specified font family.
|
||||
* Supported families are 'draw', 'sans', 'serif', and 'mono'. Any other input
|
||||
* defaults to the Caveat Brush font.
|
||||
*
|
||||
* @param {string} family The name of the font family.
|
||||
* @return {string} The font that corresponds to the given family.
|
||||
* @static
|
||||
*/
|
||||
static determineFontFromFamily(family) {
|
||||
switch (family) {
|
||||
case 'sans': return 'Source Sans Pro';
|
||||
case 'serif': return 'Crimson Pro';
|
||||
case 'mono': return 'Source Code Pro';
|
||||
case 'draw':
|
||||
default: return 'Caveat Brush';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Measures the width of a given text string using font metrics.
|
||||
* @param {string} text - The text to measure.
|
||||
* @param {opentype.Font} font - The loaded font object.
|
||||
* @param {number} fontSize - The size of the font.
|
||||
* @return {number} The width of the text.
|
||||
*/
|
||||
measureTextWidth(text, font, fontSize) {
|
||||
const scale = 1 / font.unitsPerEm * fontSize;
|
||||
const glyphs = font.stringToGlyphs(text);
|
||||
let width = 0;
|
||||
|
||||
glyphs.forEach((glyph) => {
|
||||
if (glyph.advanceWidth) {
|
||||
width += glyph.advanceWidth * scale;
|
||||
}
|
||||
});
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps text to fit within a specified width and height.
|
||||
* @param {string} text - The text to wrap.
|
||||
* @param {number} width - The width of the bounding box.
|
||||
* @return {string[]} An array of strings, each being a line.
|
||||
*/
|
||||
wrapText(text, width) {
|
||||
const config = JSON.parse(
|
||||
fs.readFileSync(
|
||||
'./config/settings.json',
|
||||
'utf8'));
|
||||
|
||||
const font = this.props?.font || 'draw';
|
||||
const fontPath = config.fonts[font];
|
||||
|
||||
const words = text.split(' ');
|
||||
let line = '';
|
||||
const lines = [];
|
||||
|
||||
// Read the font file into a Buffer
|
||||
const fontBuffer = fs.readFileSync(fontPath);
|
||||
|
||||
// Convert the Buffer to an ArrayBuffer
|
||||
const arrayBuffer = fontBuffer.buffer.slice(
|
||||
fontBuffer.byteOffset,
|
||||
fontBuffer.byteOffset + fontBuffer.byteLength);
|
||||
|
||||
// Parse the font using the ArrayBuffer
|
||||
const parsedFont = opentype.parse(arrayBuffer);
|
||||
const fontSize = Shape.determineFontSize(this.size);
|
||||
|
||||
for (const word of words) {
|
||||
const testLine = line + word + ' ';
|
||||
const testWidth = this.measureTextWidth(
|
||||
testLine,
|
||||
parsedFont,
|
||||
fontSize);
|
||||
|
||||
if (testWidth > width) {
|
||||
if (line !== '') {
|
||||
lines.push(line);
|
||||
}
|
||||
line = word + ' ';
|
||||
} else {
|
||||
line = testLine;
|
||||
}
|
||||
}
|
||||
|
||||
if (line !== '') {
|
||||
lines.push(line.trim());
|
||||
}
|
||||
|
||||
// Split newlines into separate lines
|
||||
const brokenLines = lines
|
||||
.map((line) => line.split('\n'))
|
||||
.flat();
|
||||
|
||||
return brokenLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws label text on the SVG canvas.
|
||||
* @param {SVGG} group The SVG group element to add the label to.
|
||||
*/
|
||||
drawLabel(group) {
|
||||
// Do nothing if there is no text
|
||||
if (!this.text) return;
|
||||
|
||||
// Sticky notes have a width and height of 200 and can't be resized,
|
||||
// unless the text becomes too long.
|
||||
if (!this.w) {
|
||||
this.w = 200;
|
||||
}
|
||||
|
||||
if (!this.h) {
|
||||
this.h = 200;
|
||||
}
|
||||
|
||||
if (!this.growY) {
|
||||
this.growY = 0;
|
||||
}
|
||||
|
||||
const width = this.w;
|
||||
const height = this.h + this.growY;
|
||||
|
||||
const x = Shape.alignHorizontally(this.align, width);
|
||||
let y = Shape.alignVertically(this.verticalAlign, height);
|
||||
const lineHeight = Shape.determineFontSize(this.size);
|
||||
const fontFamily = Shape.determineFontFromFamily(this.props?.font);
|
||||
|
||||
if (this.verticalAlign === 'end' || this.verticalAlign === 'middle') {
|
||||
y -= (lineHeight / 2);
|
||||
}
|
||||
|
||||
// Create a new SVG text element
|
||||
// Text is escaped by SVG.js
|
||||
const textElement = new Text()
|
||||
.move(x, y)
|
||||
.font({
|
||||
'family': fontFamily,
|
||||
'size': lineHeight,
|
||||
'anchor': this.align,
|
||||
'alignment-baseline': 'baseline',
|
||||
});
|
||||
|
||||
const lines = this.wrapText(this.text, width);
|
||||
|
||||
lines.forEach((line) => {
|
||||
const tspan = new Tspan()
|
||||
.text(line)
|
||||
.attr({
|
||||
x: x,
|
||||
dy: lineHeight,
|
||||
});
|
||||
|
||||
textElement.add(tspan);
|
||||
});
|
||||
|
||||
// Set the fill color for the text
|
||||
textElement.fill(this.labelColor || 'black');
|
||||
|
||||
// If there's a URL, make the text clickable
|
||||
if (this.url) {
|
||||
textElement.linkTo(this.url);
|
||||
}
|
||||
|
||||
group.add(textElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder method for drawing the shape.
|
||||
* Intended to be overridden by subclasses.
|
||||
*
|
||||
* @method draw
|
||||
* @return {G} An empty SVG group element.
|
||||
*/
|
||||
draw() {
|
||||
return new G();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An object representing various types of colors used in shapes.
|
||||
* This object is frozen to prevent modifications.
|
||||
*
|
||||
* @const {ColorTypes}
|
||||
*/
|
||||
export const ColorTypes = Object.freeze({
|
||||
ShapeColor: 'shape',
|
||||
FillColor: 'fill',
|
||||
SemiFillColor: 'semi',
|
||||
StickyColor: 'sticky',
|
||||
});
|
90
bbb-export-annotations/shapes/Star.js
Normal file
90
bbb-export-annotations/shapes/Star.js
Normal file
@ -0,0 +1,90 @@
|
||||
import {Polygon as SVGPolygon} from '@svgdotjs/svg.js';
|
||||
import {Geo} from './Geo.js';
|
||||
import {TAU} from './helpers.js';
|
||||
/**
|
||||
* Creates an SVG star shape from Tldraw v2 JSON data.
|
||||
*
|
||||
* @class Star
|
||||
* @extends {Geo}
|
||||
*/
|
||||
export class Star extends Geo {
|
||||
/**
|
||||
* Calculates the vertices of an n-point star.
|
||||
*
|
||||
* @param {number} w - The width of the bounding box.
|
||||
* @param {number} h - The height of the bounding box.
|
||||
* @param {number} n - The number of points on the star.
|
||||
* @return {Array} - An array of {x, y} objects representing star vertices.
|
||||
* @see {@link https://github.com/tldraw/tldraw/blob/main/packages/editor/src/lib/primitives/utils.ts} Adapted from Tldraw.
|
||||
*/
|
||||
getStarVertices(w, h, n) {
|
||||
const sides = n;
|
||||
const step = TAU / sides / 2;
|
||||
|
||||
const rightMostIndex = Math.floor(sides / 4) * 2;
|
||||
const leftMostIndex = sides * 2 - rightMostIndex;
|
||||
const topMostIndex = 0;
|
||||
const bottomMostIndex = Math.floor(sides / 2) * 2;
|
||||
const maxX = (Math.cos(-(TAU/4) + rightMostIndex * step) * w) / 2;
|
||||
const minX = (Math.cos(-(TAU/4) + leftMostIndex * step) * w) / 2;
|
||||
const minY = (Math.sin(-(TAU/4) + topMostIndex * step) * h) / 2;
|
||||
const maxY = (Math.sin(-(TAU/4) + bottomMostIndex * step) * h) / 2;
|
||||
|
||||
const diffX = w - Math.abs(maxX - minX);
|
||||
const diffY = h - Math.abs(maxY - minY);
|
||||
|
||||
const offsetX = w / 2 + minX - (w / 2 - maxX);
|
||||
const offsetY = h / 2 + minY - (h / 2 - maxY);
|
||||
|
||||
const ratio = 1;
|
||||
const cx = (w - offsetX) / 2;
|
||||
const cy = (h - offsetY) / 2;
|
||||
|
||||
const ox = (w + diffX) / 2;
|
||||
const oy = (h + diffY) / 2;
|
||||
const ix = (ox * ratio) / 2;
|
||||
const iy = (oy * ratio) / 2;
|
||||
|
||||
const points = Array.from(Array(sides * 2)).map((_, i) => {
|
||||
const theta = -(TAU/4) + i * step;
|
||||
return {
|
||||
x: cx + (i % 2 ? ix : ox) * Math.cos(theta),
|
||||
y: cy + (i % 2 ? iy : oy) * Math.sin(theta),
|
||||
};
|
||||
});
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a star shape on the SVG canvas.
|
||||
* @return {G} Returns the SVG group element containing the star.
|
||||
*/
|
||||
draw() {
|
||||
const width = this.w;
|
||||
const height = this.h + this.growY;
|
||||
|
||||
// Get the vertices of the star
|
||||
const pointsOnPerimeter = this.getStarVertices(width, height, 5);
|
||||
|
||||
// Convert the vertices to SVG polygon points format
|
||||
const points = pointsOnPerimeter.map((p) => `${p.x},${p.y}`).join(' ');
|
||||
|
||||
// Create the SVG polygon
|
||||
const starGroup = this.shapeGroup;
|
||||
const star = new SVGPolygon({
|
||||
points,
|
||||
'stroke': this.shapeColor,
|
||||
'stroke-width': this.thickness,
|
||||
'style': this.dasharray,
|
||||
});
|
||||
|
||||
// Fill the polygon if required
|
||||
this.setFill(star);
|
||||
starGroup.add(star);
|
||||
|
||||
this.drawLabel(starGroup);
|
||||
|
||||
return starGroup;
|
||||
}
|
||||
}
|
53
bbb-export-annotations/shapes/StickyNote.js
Normal file
53
bbb-export-annotations/shapes/StickyNote.js
Normal file
@ -0,0 +1,53 @@
|
||||
import {Rect} from '@svgdotjs/svg.js';
|
||||
import {Shape, ColorTypes} from './Shape.js';
|
||||
|
||||
/**
|
||||
* Represents a sticky note, extending the Shape class.
|
||||
* @extends {Shape}
|
||||
*/
|
||||
export class StickyNote extends Shape {
|
||||
/**
|
||||
* Creates an instance of a StickyNote.
|
||||
* @param {Object} params - Parameters for the sticky note.
|
||||
* @param {string} [params.url] - URL associated with the sticky note.
|
||||
* @param {string} [params.text=""] - Text content of the sticky note.
|
||||
* @param {string} [params.align] - Text alignment within the sticky note.
|
||||
* @param {string} [params.verticalAlign] - Vertical text alignment.
|
||||
* @param {number} [params.growY] - Additional height for long notes.
|
||||
* @param {ColorTypes} [params.color] - Color category for the sticky note.
|
||||
*/
|
||||
constructor(params) {
|
||||
super(params);
|
||||
this.url = this.props?.url;
|
||||
this.text = this.props?.text || '';
|
||||
this.align = this.props?.align;
|
||||
this.verticalAlign = this.props?.verticalAlign;
|
||||
this.growY = this.props?.growY;
|
||||
this.shapeColor = Shape.colorToHex(this.color, ColorTypes.StickyColor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the sticky note and adds it to the SVG.
|
||||
* Overrides the placeholder draw method in the Shape base class.
|
||||
* @override
|
||||
* @method draw
|
||||
* @return {G} An SVG group element containing the note.
|
||||
*/
|
||||
draw() {
|
||||
const stickyNote = this.shapeGroup;
|
||||
const rectW = 200;
|
||||
const rectH = 200 + this.growY;
|
||||
const cornerRadius = 10;
|
||||
|
||||
// Create rectangle element
|
||||
const rect = new Rect()
|
||||
.size(rectW, rectH)
|
||||
.radius(cornerRadius)
|
||||
.fill(this.shapeColor);
|
||||
|
||||
stickyNote.add(rect);
|
||||
this.drawLabel(stickyNote);
|
||||
|
||||
return stickyNote;
|
||||
}
|
||||
}
|
59
bbb-export-annotations/shapes/TextShape.js
Normal file
59
bbb-export-annotations/shapes/TextShape.js
Normal file
@ -0,0 +1,59 @@
|
||||
import {Text} from '@svgdotjs/svg.js';
|
||||
import {Shape} from './Shape.js';
|
||||
|
||||
/**
|
||||
* Draws the text shape on the SVG canvas, aligning and styling it
|
||||
* based on the provided properties.
|
||||
* @override
|
||||
* @return {G} The SVG group element containing the text element.
|
||||
*/
|
||||
export class TextShape extends Shape {
|
||||
/**
|
||||
* Constructs a new TextShape instance with the given parameters.
|
||||
* Inherits from Shape and initializes text-specific properties.
|
||||
*
|
||||
* @param {Object} params - The configuration object for the text shape.
|
||||
* @param {string} [params.text=""] - The text content for the shape.
|
||||
* @param {string} [params.align] - The horizontal text alignment.
|
||||
* @param {number} [params.w] - The width of the shape.
|
||||
* @param {number} [params.h] - The height of the shape.
|
||||
* @param {string} [params.font] - The font family for the text.
|
||||
*/
|
||||
constructor(params) {
|
||||
super(params);
|
||||
this.text = this.props?.text || '';
|
||||
this.align = this.props?.align;
|
||||
this.w = this.props?.w;
|
||||
this.h = this.props?.h;
|
||||
this.fontSize = Shape.determineFontSize(this.size);
|
||||
this.fontFamily = Shape.determineFontFromFamily(this.props?.font);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the text shape and adds it to the SVG.
|
||||
* Overrides the placeholder draw method in the Shape base class.
|
||||
* @override
|
||||
* @method draw
|
||||
* @return {G} An SVG group element containing the text.
|
||||
*/
|
||||
draw() {
|
||||
const x = Shape.alignHorizontally(this.align, this.w);
|
||||
const y = 0;
|
||||
|
||||
const textGroup = this.shapeGroup;
|
||||
const textElement = new Text()
|
||||
.text(this.text)
|
||||
.move(x, y)
|
||||
.font({
|
||||
'family': this.fontFamily,
|
||||
'size': this.fontSize,
|
||||
'anchor': this.align,
|
||||
'alignment-baseline': 'middle',
|
||||
})
|
||||
.fill(this.shapeColor);
|
||||
|
||||
textGroup.add(textElement);
|
||||
|
||||
return textGroup;
|
||||
}
|
||||
}
|
45
bbb-export-annotations/shapes/Trapezoid.js
Normal file
45
bbb-export-annotations/shapes/Trapezoid.js
Normal file
@ -0,0 +1,45 @@
|
||||
import {Polygon as SVGPolygon} from '@svgdotjs/svg.js';
|
||||
import {Geo} from './Geo.js';
|
||||
|
||||
/**
|
||||
* Creates an SVG trapezoid shape from Tldraw v2 JSON data.
|
||||
*
|
||||
* @class Trapezoid
|
||||
* @extends {Geo}
|
||||
*/
|
||||
export class Trapezoid extends Geo {
|
||||
/**
|
||||
* Draws a trapezoid shape on the SVG canvas.
|
||||
* @return {G} Returns the SVG group element containing the trapezoid.
|
||||
*/
|
||||
draw() {
|
||||
const width = this.w;
|
||||
const height = this.h + this.growY;
|
||||
|
||||
// Adjust this value as needed for the trapezoid
|
||||
const topWidth = width * 0.6;
|
||||
const xOffset = (width - topWidth) / 2;
|
||||
|
||||
// Shape begins from the upper left corner
|
||||
const points = [
|
||||
[xOffset, 0],
|
||||
[xOffset + topWidth, 0],
|
||||
[width, height],
|
||||
[0, height],
|
||||
].map((p) => p.join(',')).join(' ');
|
||||
|
||||
const trapezoidGroup = this.shapeGroup;
|
||||
const trapezoid = new SVGPolygon({
|
||||
points,
|
||||
'stroke': this.shapeColor,
|
||||
'stroke-width': this.thickness,
|
||||
'style': this.dasharray,
|
||||
});
|
||||
|
||||
this.setFill(trapezoid);
|
||||
trapezoidGroup.add(trapezoid);
|
||||
this.drawLabel(trapezoidGroup);
|
||||
|
||||
return trapezoidGroup;
|
||||
}
|
||||
}
|
41
bbb-export-annotations/shapes/Triangle.js
Normal file
41
bbb-export-annotations/shapes/Triangle.js
Normal file
@ -0,0 +1,41 @@
|
||||
import {Polygon as SVGPolygon} from '@svgdotjs/svg.js';
|
||||
import {Geo} from './Geo.js';
|
||||
|
||||
/**
|
||||
* Creates an SVG triangle shape from Tldraw v2 JSON data.
|
||||
*
|
||||
* @class Triangle
|
||||
* @extends {Geo}
|
||||
*/
|
||||
export class Triangle extends Geo {
|
||||
/**
|
||||
* Draws a triangle shape on the SVG canvas.
|
||||
* @return {G} Returns the SVG group element containing the triangle.
|
||||
*/
|
||||
draw() {
|
||||
const width = this.w;
|
||||
const height = this.h + this.growY;
|
||||
const halfWidth = width / 2;
|
||||
|
||||
// Shape begins from the upper left corner
|
||||
const points = [
|
||||
[halfWidth, 0],
|
||||
[width, height],
|
||||
[0, height],
|
||||
].map((p) => p.join(',')).join(' ');
|
||||
|
||||
const triangleGroup = this.shapeGroup;
|
||||
const triangle = new SVGPolygon({
|
||||
points,
|
||||
'stroke': this.shapeColor,
|
||||
'stroke-width': this.thickness,
|
||||
'style': this.dasharray,
|
||||
});
|
||||
|
||||
this.setFill(triangle);
|
||||
triangleGroup.add(triangle);
|
||||
this.drawLabel(triangleGroup);
|
||||
|
||||
return triangleGroup;
|
||||
}
|
||||
}
|
40
bbb-export-annotations/shapes/XBox.js
Normal file
40
bbb-export-annotations/shapes/XBox.js
Normal file
@ -0,0 +1,40 @@
|
||||
import {Line} from '@svgdotjs/svg.js';
|
||||
import {Rectangle} from './Rectangle.js';
|
||||
|
||||
/**
|
||||
* Creates an SVG "XBox" shape, which is a rectangle
|
||||
* with an "X" drawn through it.
|
||||
*
|
||||
* @class XBox
|
||||
* @extends {Rectangle}
|
||||
*/
|
||||
export class XBox extends Rectangle {
|
||||
/**
|
||||
* Draws an "XBox" shape on the SVG canvas.
|
||||
* @return {G} Returns the SVG group element
|
||||
* containing the rectangle and the X.
|
||||
*/
|
||||
draw() {
|
||||
// Draw the base rectangle
|
||||
const rectGroup = super.draw();
|
||||
|
||||
// Add the first diagonal line from upper-left to lower-right
|
||||
const line1 = new Line();
|
||||
line1.plot(0, 0, this.w, this.h + this.growY)
|
||||
.stroke({color: this.shapeColor, width: this.thickness})
|
||||
.style({dasharray: this.dasharray});
|
||||
|
||||
// Add the second diagonal line from upper-right to lower-left
|
||||
const line2 = new Line();
|
||||
line2.plot(this.w, 0, 0, this.h + this.growY)
|
||||
.stroke({color: this.shapeColor, width: this.thickness})
|
||||
.style({dasharray: this.dasharray});
|
||||
|
||||
// Add the lines to the group
|
||||
rectGroup.add(line1);
|
||||
rectGroup.add(line2);
|
||||
this.drawLabel(rectGroup);
|
||||
|
||||
return rectGroup;
|
||||
}
|
||||
}
|
64
bbb-export-annotations/shapes/geoFactory.js
Normal file
64
bbb-export-annotations/shapes/geoFactory.js
Normal file
@ -0,0 +1,64 @@
|
||||
import {Geo} from './Geo.js';
|
||||
import {Rectangle} from './Rectangle.js';
|
||||
import {Ellipse} from './Ellipse.js';
|
||||
import {Diamond} from './Diamond.js';
|
||||
import {Triangle} from './Triangle.js';
|
||||
import {Trapezoid} from './Trapezoid.js';
|
||||
import {Rhombus} from './Rhombus.js';
|
||||
import {Hexagon} from './Hexagon.js';
|
||||
import {Oval} from './Oval.js';
|
||||
import {Star} from './Star.js';
|
||||
import {ArrowRight} from './ArrowRight.js';
|
||||
import {ArrowLeft} from './ArrowLeft.js';
|
||||
import {ArrowUp} from './ArrowUp.js';
|
||||
import {ArrowDown} from './ArrowDown.js';
|
||||
import {XBox} from './XBox.js';
|
||||
import {Checkbox} from './Checkbox.js';
|
||||
import {Cloud} from './Cloud.js';
|
||||
/**
|
||||
* Creates a geometric object instance based on the provided annotations.
|
||||
*
|
||||
* @function createGeoObject
|
||||
* @param {Object} annotations - The annotations for the geometric object.
|
||||
* @param {Object} [annotations.props] - The properties of the annotations.
|
||||
* @param {String} [annotations.props.geo] - Which geometric object to create.
|
||||
* @return {Geo} The created geometric object.
|
||||
*/
|
||||
export function createGeoObject(annotations) {
|
||||
switch (annotations.props?.geo) {
|
||||
case 'rectangle':
|
||||
return new Rectangle(annotations);
|
||||
case 'ellipse':
|
||||
return new Ellipse(annotations);
|
||||
case 'diamond':
|
||||
return new Diamond(annotations);
|
||||
case 'triangle':
|
||||
return new Triangle(annotations);
|
||||
case 'trapezoid':
|
||||
return new Trapezoid(annotations);
|
||||
case 'rhombus':
|
||||
return new Rhombus(annotations);
|
||||
case 'hexagon':
|
||||
return new Hexagon(annotations);
|
||||
case 'oval':
|
||||
return new Oval(annotations);
|
||||
case 'star':
|
||||
return new Star(annotations);
|
||||
case 'arrow-right':
|
||||
return new ArrowRight(annotations);
|
||||
case 'arrow-left':
|
||||
return new ArrowLeft(annotations);
|
||||
case 'arrow-up':
|
||||
return new ArrowUp(annotations);
|
||||
case 'arrow-down':
|
||||
return new ArrowDown(annotations);
|
||||
case 'x-box':
|
||||
return new XBox(annotations);
|
||||
case 'check-box':
|
||||
return new Checkbox(annotations);
|
||||
case 'cloud':
|
||||
return new Cloud(annotations);
|
||||
default:
|
||||
return new Geo(annotations);
|
||||
}
|
||||
}
|
245
bbb-export-annotations/shapes/helpers.js
Normal file
245
bbb-export-annotations/shapes/helpers.js
Normal file
@ -0,0 +1,245 @@
|
||||
/**
|
||||
* Represents the constant TAU, which is equal to 2 * PI.
|
||||
*
|
||||
* TAU is often used in trigonometric calculations as it represents
|
||||
* one full turn in radians, making it more intuitive than using 2 * PI.
|
||||
* For example, half a circle is TAU / 2, a quarter is TAU / 4, etc.,
|
||||
* which makes the math easier to follow.
|
||||
*
|
||||
* @constant {number}
|
||||
*/
|
||||
export const TAU = Math.PI * 2;
|
||||
|
||||
/**
|
||||
* Sorts an array of objects lexicographically based on a nested key-value pair.
|
||||
*
|
||||
* @param {Array} array - The array to be sorted.
|
||||
* @param {string} key - The key in each object to be used for sorting.
|
||||
* @param {string} value - The nested key within the 'key' object to be used
|
||||
* for sorting.
|
||||
* @return {Array} - Returns a new array sorted lexicographically
|
||||
* by the specified nested key-value pair.
|
||||
*
|
||||
* @example
|
||||
* const data = [
|
||||
* {annotationInfo: {index: 'a1V'}, type: 'shape'},
|
||||
* {annotationInfo: {index: 'a2'}, type: 'shape'},
|
||||
* {annotationInfo: {index: 'a1'}, type: 'draw'}
|
||||
* ];
|
||||
* const sortedData = sortByKey(data, 'annotationInfo', 'index');
|
||||
* // Output: [{ annotationInfo: { index: 'a1' }, type: 'draw' },
|
||||
* { annotationInfo: { index: 'a1V' }, type: 'shape' },
|
||||
* { annotationInfo: { index: 'a2' }, type: 'shape' }]
|
||||
*/
|
||||
export function sortByKey(array, key, value) {
|
||||
return array.sort((a, b) => {
|
||||
const [x, y] = [a[key][value], b[key][value]];
|
||||
return x.localeCompare(y);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an angle from radians to degrees.
|
||||
*
|
||||
* @param {number} angle - The angle in radians.
|
||||
* @return {number} The angle in degrees, fixed to two decimal places.
|
||||
*/
|
||||
export function radToDegree(angle) {
|
||||
return parseFloat(angle * (360 / TAU)).toFixed(2) || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Random number generator based on a seed value.
|
||||
* This uses a variation of the xorshift algorithm to generate
|
||||
* pseudo-random numbers. The function returns a `next` function that,
|
||||
* when called, generates the next random number in sequence.
|
||||
*
|
||||
* @param {string} [seed=''] - The seed value for the random number generator.
|
||||
* Default is an empty string.
|
||||
* @return {Function} The `next` function to generate random numbers.
|
||||
* @see {@link https://github.com/tldraw/tldraw/blob/main/packages/utils/src/lib/number.ts} Adapted from Tldraw.
|
||||
*/
|
||||
export function rng(seed = '') {
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
let z = 0;
|
||||
let w = 0;
|
||||
|
||||
/**
|
||||
* Generates the next number in the pseudo-random sequence using bitwise
|
||||
* operations. This function uses a form of 'xorshift', a type of pseudo-
|
||||
* random number generator algorithm. It manipulates four state variables
|
||||
* \( x, y, z, w \) with bitwise operations to produce a new random number
|
||||
* upon each call. The returned value is scaled to the range [0, 2).
|
||||
* @return {number} The next pseudo-random number within [0, 2).
|
||||
*/
|
||||
function next() {
|
||||
const t = x ^ (x << 11);
|
||||
x = y;
|
||||
y = z;
|
||||
z = w;
|
||||
w ^= ((w >>> 19) ^ t ^ (t >>> 8)) >>> 0;
|
||||
return (w / 0x100000000) * 2;
|
||||
}
|
||||
|
||||
for (let k = 0; k < seed.length + 64; k++) {
|
||||
x ^= seed.charCodeAt(k) | 0;
|
||||
next();
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a point on the perimeter of a circle.
|
||||
*
|
||||
* @param {number} cx - The center x of the circle.
|
||||
* @param {number} cy - The center y of the circle.
|
||||
* @param {number} r - The radius of the circle.
|
||||
* @param {number} a - The angle in radians to get the point from.
|
||||
* @return {Object} A point object with 'x' and 'y' properties
|
||||
* @public
|
||||
*/
|
||||
export function getPointOnCircle(cx, cy, r, a) {
|
||||
return {
|
||||
x: cx + r * Math.cos(a),
|
||||
y: cy + r * Math.sin(a),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the angle (in radians) between a center point and another point
|
||||
* using the arctangent of the quotient of their coordinates.
|
||||
* The angle is measured in the coordinate system where x-axis points to the
|
||||
* right and y-axis points down. The angle is measured counterclockwise
|
||||
* from the positive x-axis.
|
||||
*
|
||||
* @param {Object} center - The center point with x and y coordinates.
|
||||
* @param {number} center.x - The x-coordinate of the center point.
|
||||
* @param {number} center.y - The y-coordinate of the center point.
|
||||
*
|
||||
* @param {Object} point - The other point with x and y coordinates.
|
||||
* @param {number} point.x - The x-coordinate of the other point.
|
||||
* @param {number} point.y - The y-coordinate of the other point.
|
||||
*
|
||||
* @return {number} The angle in radians between the line from the center
|
||||
* to the other point and the positive x-axis.
|
||||
*/
|
||||
export function angle(center, point) {
|
||||
const dy = point.y - center.y;
|
||||
const dx = point.x - center.x;
|
||||
return Math.atan2(dy, dx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the clockwise angular distance between two angles.
|
||||
*
|
||||
* This function takes two angles in radians and calculates the
|
||||
* shortest angular distance between them in the clockwise direction.
|
||||
* The result is also in radians and accounts for full circle rotation.
|
||||
*
|
||||
* @param {number} startAngle - The starting angle in radians.
|
||||
* @param {number} endAngle - The ending angle in radians.
|
||||
* @return {number} The Clockwise angular distance in radians
|
||||
* between the start and end angles.
|
||||
*/
|
||||
export function clockwiseAngleDist(startAngle, endAngle) {
|
||||
let l = endAngle - startAngle;
|
||||
if (l < 0) {
|
||||
l += TAU;
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the distance between two points.
|
||||
*
|
||||
* @param {Object} point1 - The first point, represented as an object {x, y}.
|
||||
* @param {Object} point2 - The second point, represented as an object {x, y}.
|
||||
* @return {number} - The calculated distance.
|
||||
*/
|
||||
export function calculateDistance(point1, point2) {
|
||||
const dx = point2.x - point1.x;
|
||||
const dy = point2.y - point1.y;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the circle that passes through three points A, B, and C.
|
||||
* Returns the circle's center as [x, y] and its radius.
|
||||
*
|
||||
* @param {number[]} A - Point A as [x1, y1].
|
||||
* @param {number[]} B - Point B as [x2, y2].
|
||||
* @param {number[]} C - Point C as [x3, y3].
|
||||
* @return {number[]|null} - The circle's center [x, y] and radius,
|
||||
* or null if the points are collinear.
|
||||
*/
|
||||
export function circleFromThreePoints(A, B, C) {
|
||||
const [x1, y1] = A;
|
||||
const [x2, y2] = B;
|
||||
const [x3, y3] = C;
|
||||
|
||||
const a = x1 * (y2 - y3) - y1 * (x2 - x3) + x2 * y3 - x3 * y2;
|
||||
|
||||
if (a === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const b =
|
||||
(x1 * x1 + y1 * y1) * (y3 - y2) +
|
||||
(x2 * x2 + y2 * y2) * (y1 - y3) +
|
||||
(x3 * x3 + y3 * y3) * (y2 - y1);
|
||||
|
||||
const c =
|
||||
(x1 * x1 + y1 * y1) * (x2 - x3) +
|
||||
(x2 * x2 + y2 * y2) * (x3 - x1) +
|
||||
(x3 * x3 + y3 * y3) * (x1 - x2);
|
||||
|
||||
const x = -b / (2 * a);
|
||||
const y = -c / (2 * a);
|
||||
|
||||
return [x, y, Math.hypot(x - x1, y - y1)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a 2D vector represented as an array [x, y].
|
||||
*
|
||||
* @param {Array<number>} A - The 2D vector to normalize.
|
||||
* @return {Array<number>} The normalized vector,
|
||||
*/
|
||||
export function normalize(A) {
|
||||
const length = Math.sqrt(A[0] * A[0] + A[1] * A[1]);
|
||||
return [A[0] / length, A[1] / length];
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates a vector [x,y] 90 degrees counter-clockwise.
|
||||
*
|
||||
* @param {Array<number>} vec - The 2D vector to rotate.
|
||||
* @return {Array<number>} The rotated vector.
|
||||
*/
|
||||
export function rotate(vec) {
|
||||
const [x, y] = vec;
|
||||
return [y, -x];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Escapes special characters in a string to their corresponding HTML entities
|
||||
* to prevent misinterpretation of HTML content. This function converts
|
||||
* ampersands, single quotes, double quotes, greater-than signs, and
|
||||
* less-than signs to their corresponding HTML entity codes, making it safe
|
||||
* to insert the string into HTML or XML content where these characters would
|
||||
* otherwise be mistaken for markup.
|
||||
*
|
||||
* @param {string} string - The string to be escaped.
|
||||
* @return {string} The escaped string with HTML entities.
|
||||
*/
|
||||
export function escapeSVGText(string) {
|
||||
return string
|
||||
.replace(/&/g, '\\&') // Escape ampersands.
|
||||
.replace(/'/g, '\\'') // Escape single quotes.
|
||||
.replace(/"/g, '\\"') // Escape double quotes.
|
||||
.replace(/>/g, '\\>') // Escape greater-than signs.
|
||||
.replace(/</g, '\\<'); // Escape less-than signs.
|
||||
}
|
@ -1,21 +1,20 @@
|
||||
const Logger = require('../lib/utils/logger');
|
||||
const axios = require('axios').default;
|
||||
const config = require('../config');
|
||||
const cp = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const redis = require('redis');
|
||||
const sanitize = require('sanitize-filename');
|
||||
const stream = require('stream');
|
||||
const WorkerStarter = require('../lib/utils/worker-starter');
|
||||
const {PresAnnStatusMsg} = require('../lib/utils/message-builder');
|
||||
const {workerData} = require('worker_threads');
|
||||
const {promisify} = require('util');
|
||||
import Logger from '../lib/utils/logger.js';
|
||||
import axios from 'axios';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import redis from 'redis';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import stream from 'stream';
|
||||
import WorkerStarter from '../lib/utils/worker-starter.js';
|
||||
import {PresAnnStatusMsg} from '../lib/utils/message-builder.js';
|
||||
import {workerData} from 'worker_threads';
|
||||
import {promisify} from 'util';
|
||||
|
||||
const jobId = workerData.jobId;
|
||||
const logger = new Logger('presAnn Collector');
|
||||
logger.info(`Collecting job ${jobId}`);
|
||||
|
||||
const config = JSON.parse(fs.readFileSync('./config/settings.json', 'utf8'));
|
||||
const dropbox = path.join(config.shared.presAnnDropboxDir, jobId);
|
||||
|
||||
// Takes the Job from the dropbox
|
||||
@ -23,11 +22,26 @@ const job = fs.readFileSync(path.join(dropbox, 'job'));
|
||||
const exportJob = JSON.parse(job);
|
||||
const jobType = exportJob.jobType;
|
||||
|
||||
/**
|
||||
* Asynchronously collects annotations from Redis, processes them,
|
||||
* and handles the collection of presentation page files. It removes
|
||||
* the annotations from Redis after collection, writes them to a file,
|
||||
* and manages the retrieval of SVGs, PNGs, or JPEGs. Errors during the
|
||||
* process are logged, and the status of the operation is published to
|
||||
* a Redis channel.
|
||||
*
|
||||
* @async
|
||||
* @function collectAnnotationsFromRedis
|
||||
* @throws Will log an error if an error occurs in connecting to Redis.
|
||||
* @return {Promise<void>} Resolves when the function has completed its task.
|
||||
*/
|
||||
async function collectAnnotationsFromRedis() {
|
||||
const client = redis.createClient({
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
password: config.redis.password,
|
||||
socket: {
|
||||
host: config.redis.host,
|
||||
port: config.redis.port
|
||||
}
|
||||
});
|
||||
|
||||
client.on('error', (err) => logger.info('Redis Client Error', err));
|
||||
@ -59,49 +73,46 @@ async function collectAnnotationsFromRedis() {
|
||||
const statusUpdate = new PresAnnStatusMsg(exportJob);
|
||||
|
||||
if (fs.existsSync(pdfFile)) {
|
||||
// If there's a PDF file, we leverage the existing converted SVG slides
|
||||
for (const p of pages) {
|
||||
const pageNumber = p.page;
|
||||
const outputFile = path.join(dropbox, `slide${pageNumber}`);
|
||||
const imageName = `slide${pageNumber}`;
|
||||
const convertedSVG = path.join(
|
||||
exportJob.presLocation,
|
||||
'svgs',
|
||||
`${imageName}.svg`);
|
||||
|
||||
// CairoSVG doesn't handle transparent SVG and PNG embeds properly,
|
||||
// e.g., in rasterized text. So textboxes may get a black background
|
||||
// when downloading/exporting repeatedly. To avoid that, we take slides
|
||||
// from the uploaded file, but later probe the dimensions from the SVG
|
||||
// so it matches what was shown in the browser.
|
||||
|
||||
const extract_png_from_pdf = [
|
||||
'-png',
|
||||
'-f', pageNumber,
|
||||
'-l', pageNumber,
|
||||
'-scale-to', config.collector.pngWidthRasterizedSlides,
|
||||
'-singlefile',
|
||||
'-cropbox',
|
||||
pdfFile, outputFile,
|
||||
];
|
||||
const outputFile = path.join(dropbox, `slide${pageNumber}.svg`);
|
||||
|
||||
try {
|
||||
cp.spawnSync(config.shared.pdftocairo, extract_png_from_pdf, {shell: false});
|
||||
fs.copyFileSync(convertedSVG, outputFile);
|
||||
} catch (error) {
|
||||
logger.error(`PDFtoCairo failed extracting slide ${pageNumber} in job ${jobId}: ${error.message}`);
|
||||
logger.error('Failed collecting slide ' + pageNumber +
|
||||
' in job ' + jobId + ': ' + error.message);
|
||||
statusUpdate.setError();
|
||||
}
|
||||
|
||||
await client.publish(config.redis.channels.publish, statusUpdate.build(pageNumber));
|
||||
await client.publish(
|
||||
config.redis.channels.publish,
|
||||
statusUpdate.build(pageNumber));
|
||||
}
|
||||
} else {
|
||||
const imageName = 'slide1';
|
||||
|
||||
if (fs.existsSync(`${presFile}.png`)) {
|
||||
fs.copyFileSync(`${presFile}.png`, path.join(dropbox, `${imageName}.png`));
|
||||
fs.copyFileSync(`${presFile}.png`,
|
||||
path.join(dropbox, `${imageName}.png`));
|
||||
} else if (fs.existsSync(`${presFile}.jpeg`)) {
|
||||
fs.copyFileSync(`${presFile}.jpeg`, path.join(dropbox, `${imageName}.jpeg`));
|
||||
fs.copyFileSync(`${presFile}.jpeg`,
|
||||
path.join(dropbox, `${imageName}.jpeg`));
|
||||
} else if (fs.existsSync(`${presFile}.jpg`)) {
|
||||
// JPG file available: copy changing extension to JPEG
|
||||
fs.copyFileSync(`${presFile}.jpg`, path.join(dropbox, `${imageName}.jpeg`));
|
||||
fs.copyFileSync(`${presFile}.jpg`,
|
||||
path.join(dropbox, `${imageName}.jpeg`));
|
||||
} else {
|
||||
await client.publish(config.redis.channels.publish, statusUpdate.build());
|
||||
client.disconnect();
|
||||
return logger.error(`No PDF, PNG, JPG or JPEG file available for job ${jobId}`);
|
||||
return logger.error(`PDF/PNG/JPG/JPEG file not found for job ${jobId}`);
|
||||
}
|
||||
|
||||
await client.publish(config.redis.channels.publish, statusUpdate.build());
|
||||
@ -113,6 +124,15 @@ async function collectAnnotationsFromRedis() {
|
||||
process.process();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a promise that resolves after a specified number of milliseconds,
|
||||
* effectively pausing execution for that duration. Used to delay operations
|
||||
* in an asynchronous function.
|
||||
* @async
|
||||
* @function sleep
|
||||
* @param {number} ms - The amount of time in milliseconds to sleep.
|
||||
* @return {Promise<void>} Resolves after the specified number of milliseconds.
|
||||
*/
|
||||
async function sleep(ms) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
@ -129,8 +149,10 @@ async function collectSharedNotes(retries = 3) {
|
||||
const padId = exportJob.presId;
|
||||
const notesFormat = 'pdf';
|
||||
|
||||
const serverSideFilename = `${sanitize(exportJob.serverSideFilename.replace(/\s/g, '_'))}.${notesFormat}`;
|
||||
const notes_endpoint = `${config.bbbPadsAPI}/p/${padId}/export/${notesFormat}`;
|
||||
const underscoredFilename = exportJob.serverSideFilename.replace(/\s/g, '_');
|
||||
const sanitizedFilename = sanitize(underscoredFilename);
|
||||
const serverSideFilename = `${sanitizedFilename}.${notesFormat}`;
|
||||
const notesEndpoint = `${config.bbbPadsAPI}/p/${padId}/export/${notesFormat}`;
|
||||
const filePath = path.join(dropbox, serverSideFilename);
|
||||
|
||||
const finishedDownload = promisify(stream.finished);
|
||||
@ -139,7 +161,7 @@ async function collectSharedNotes(retries = 3) {
|
||||
try {
|
||||
const response = await axios({
|
||||
method: 'GET',
|
||||
url: notes_endpoint,
|
||||
url: notesEndpoint,
|
||||
responseType: 'stream',
|
||||
});
|
||||
response.data.pipe(writer);
|
||||
@ -157,13 +179,21 @@ async function collectSharedNotes(retries = 3) {
|
||||
}
|
||||
}
|
||||
|
||||
const notifier = new WorkerStarter({jobType, jobId, serverSideFilename, filename: exportJob.filename});
|
||||
const notifier = new WorkerStarter({jobType, jobId,
|
||||
serverSideFilename, filename: exportJob.filename});
|
||||
notifier.notify();
|
||||
}
|
||||
|
||||
switch (jobType) {
|
||||
case 'PresentationWithAnnotationExportJob': return collectAnnotationsFromRedis();
|
||||
case 'PresentationWithAnnotationDownloadJob': return collectAnnotationsFromRedis();
|
||||
case 'PadCaptureJob': return collectSharedNotes();
|
||||
default: return logger.error(`Unknown job type ${jobType}`);
|
||||
case 'PresentationWithAnnotationExportJob':
|
||||
collectAnnotationsFromRedis();
|
||||
break;
|
||||
case 'PresentationWithAnnotationDownloadJob':
|
||||
collectAnnotationsFromRedis();
|
||||
break;
|
||||
case 'PadCaptureJob':
|
||||
collectSharedNotes();
|
||||
break;
|
||||
default:
|
||||
logger.error(`Unknown job type ${jobType}`);
|
||||
}
|
||||
|
@ -1,16 +1,17 @@
|
||||
const Logger = require('../lib/utils/logger');
|
||||
const config = require('../config');
|
||||
const fs = require('fs');
|
||||
const FormData = require('form-data');
|
||||
const redis = require('redis');
|
||||
const axios = require('axios').default;
|
||||
const path = require('path');
|
||||
const {NewPresFileAvailableMsg} = require('../lib/utils/message-builder');
|
||||
|
||||
const {workerData} = require('worker_threads');
|
||||
const [jobType, jobId, serverSideFilename] = [workerData.jobType, workerData.jobId, workerData.serverSideFilename];
|
||||
import Logger from '../lib/utils/logger.js';
|
||||
import fs from 'fs';
|
||||
import FormData from 'form-data';
|
||||
import redis from 'redis';
|
||||
import axios from 'axios';
|
||||
import path from 'path';
|
||||
import {NewPresFileAvailableMsg} from '../lib/utils/message-builder.js';
|
||||
import {workerData} from 'worker_threads';
|
||||
const [jobType, jobId, serverSideFilename] = [workerData.jobType,
|
||||
workerData.jobId,
|
||||
workerData.serverSideFilename];
|
||||
|
||||
const logger = new Logger('presAnn Notifier Worker');
|
||||
const config = JSON.parse(fs.readFileSync('./config/settings.json', 'utf8'));
|
||||
|
||||
const dropbox = `${config.shared.presAnnDropboxDir}/${jobId}`;
|
||||
const job = fs.readFileSync(path.join(dropbox, 'job'));
|
||||
@ -20,15 +21,18 @@ const exportJob = JSON.parse(job);
|
||||
* sending a message through Redis PubSub */
|
||||
async function notifyMeetingActor() {
|
||||
const client = redis.createClient({
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
password: config.redis.password,
|
||||
socket: {
|
||||
host: config.redis.host,
|
||||
port: config.redis.port
|
||||
}
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
client.on('error', (err) => logger.info('Redis Client Error', err));
|
||||
|
||||
const link = path.join('presentation',
|
||||
const link = path.join(
|
||||
'presentation',
|
||||
exportJob.parentMeetingId, exportJob.parentMeetingId,
|
||||
exportJob.presId, 'pdf', jobId, serverSideFilename);
|
||||
|
||||
@ -37,14 +41,17 @@ async function notifyMeetingActor() {
|
||||
logger.info(`Annotated PDF available at ${link}`);
|
||||
await client.publish(config.redis.channels.publish, notification.build());
|
||||
client.disconnect();
|
||||
|
||||
}
|
||||
|
||||
/** Upload PDF to a BBB room
|
||||
* @param {String} filePath - Absolute path to the file, including the extension
|
||||
*/
|
||||
async function upload(filePath) {
|
||||
const callbackUrl = `${config.bbbWebAPI}/bigbluebutton/presentation/${exportJob.presentationUploadToken}/upload`;
|
||||
const apiPath = '/bigbluebutton/presentation/';
|
||||
const uploadToken = exportJob.presentationUploadToken;
|
||||
const uploadAction = '/upload';
|
||||
const callbackUrl = config.bbbWebAPI + apiPath + uploadToken + uploadAction;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('conference', exportJob.parentMeetingId);
|
||||
formData.append('pod_id', config.notifier.pod_id);
|
||||
@ -64,7 +71,10 @@ async function upload(filePath) {
|
||||
if (jobType == 'PresentationWithAnnotationDownloadJob') {
|
||||
notifyMeetingActor();
|
||||
} else if (jobType == 'PresentationWithAnnotationExportJob') {
|
||||
const filePath = `${exportJob.presLocation}/pdfs/${jobId}/${serverSideFilename}`;
|
||||
const baseDirectory = exportJob.presLocation;
|
||||
const subDirectory = 'pdfs';
|
||||
const filePath = path.join(baseDirectory, subDirectory,
|
||||
jobId, serverSideFilename);
|
||||
upload(filePath);
|
||||
} else if (jobType == 'PadCaptureJob') {
|
||||
const filePath = `${dropbox}/${serverSideFilename}`;
|
||||
|
File diff suppressed because it is too large
Load Diff
190
bbb-graphql-actions/package-lock.json
generated
190
bbb-graphql-actions/package-lock.json
generated
@ -13,7 +13,7 @@
|
||||
"@types/node": "^20.7.0",
|
||||
"@types/redis": "^4.0.11",
|
||||
"axios": "^0.21.1",
|
||||
"express": "^4.17.1",
|
||||
"express": "^4.19.2",
|
||||
"redis": "^4.6.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -315,12 +315,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
||||
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
||||
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"content-type": "~1.0.4",
|
||||
"content-type": "~1.0.5",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
@ -328,7 +328,7 @@
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.11.0",
|
||||
"raw-body": "2.5.1",
|
||||
"raw-body": "2.5.2",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
@ -368,12 +368,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
|
||||
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
"es-define-property": "^1.0.0",
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"set-function-length": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@ -440,9 +446,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@ -466,6 +472,22 @@
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/define-data-property": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
@ -505,6 +527,25 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
||||
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
@ -519,16 +560,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.18.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
||||
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
|
||||
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.1",
|
||||
"body-parser": "1.20.2",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.5.0",
|
||||
"cookie": "0.6.0",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
@ -638,9 +679,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/generic-pool": {
|
||||
"version": "3.9.0",
|
||||
@ -651,14 +695,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
|
||||
"integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
||||
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"has-proto": "^1.0.1",
|
||||
"has-symbols": "^1.0.3"
|
||||
"has-symbols": "^1.0.3",
|
||||
"hasown": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@ -676,15 +724,15 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/has": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"node_modules/gopd": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
||||
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1"
|
||||
"get-intrinsic": "^1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
@ -696,10 +744,21 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/has-property-descriptors": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
|
||||
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@ -718,6 +777,17 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
@ -967,9 +1037,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.12.3",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
|
||||
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
|
||||
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
@ -1051,9 +1121,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
|
||||
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
|
||||
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
@ -1170,19 +1240,39 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
|
||||
"dependencies": {
|
||||
"define-data-property": "^1.1.4",
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"gopd": "^1.0.1",
|
||||
"has-property-descriptors": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
|
||||
"integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
"object-inspect": "^1.9.0"
|
||||
"call-bind": "^1.0.7",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"object-inspect": "^1.13.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
|
@ -28,7 +28,7 @@
|
||||
"@types/node": "^20.7.0",
|
||||
"@types/redis": "^4.0.11",
|
||||
"axios": "^0.21.1",
|
||||
"express": "^4.17.1",
|
||||
"express": "^4.19.2",
|
||||
"redis": "^4.6.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { RedisMessage } from '../types';
|
||||
import {throwErrorIfNotModerator} from "../imports/validation";
|
||||
import {throwErrorIfNotPresenter} from "../imports/validation";
|
||||
|
||||
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
|
||||
throwErrorIfNotModerator(sessionVariables);
|
||||
throwErrorIfNotPresenter(sessionVariables);
|
||||
const eventName = 'BroadcastLayoutMsg';
|
||||
|
||||
const routing = {
|
||||
|
@ -16,6 +16,7 @@ export default function buildRedisMessage(sessionVariables: Record<string, unkno
|
||||
|
||||
const body = {
|
||||
userId: routing.userId,
|
||||
networkRttInMs: input.networkRttInMs
|
||||
};
|
||||
|
||||
return { eventName, routing, header, body };
|
||||
|
@ -1,23 +0,0 @@
|
||||
import { RedisMessage } from '../types';
|
||||
|
||||
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
|
||||
const eventName = `UserConnectionUpdateRttReqMsg`;
|
||||
|
||||
const routing = {
|
||||
meetingId: sessionVariables['x-hasura-meetingid'] as String,
|
||||
userId: sessionVariables['x-hasura-userid'] as String
|
||||
};
|
||||
|
||||
const header = {
|
||||
name: eventName,
|
||||
meetingId: routing.meetingId,
|
||||
userId: routing.userId
|
||||
};
|
||||
|
||||
const body = {
|
||||
userId: routing.userId,
|
||||
networkRttInMs: input.networkRttInMs
|
||||
};
|
||||
|
||||
return { eventName, routing, header, body };
|
||||
}
|
@ -3,4 +3,5 @@ export const REDIS_HOST = process.env.BBB_REDIS_HOST || '127.0.0.1';
|
||||
export const REDIS_PORT = Number(process.env.BBB_REDIS_PORT) || 6379;
|
||||
export const SERVER_HOST = process.env.SERVER_HOST || '127.0.0.1';
|
||||
export const SERVER_PORT = Number(process.env.SERVER_PORT) || 8093;
|
||||
export const MAX_BODY_SIZE = Number(process.env.MAX_BODY_SIZE) || 10485760; // 10MB
|
||||
export const DEBUG = false;
|
||||
|
@ -1,13 +1,13 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
import util from 'util';
|
||||
import { redisMessageFactory } from './imports/redisMessageFactory';
|
||||
import { DEBUG, SERVER_HOST, SERVER_PORT } from './config';
|
||||
import { DEBUG, SERVER_HOST, SERVER_PORT, MAX_BODY_SIZE } from './config';
|
||||
import { createRedisClient } from './imports/redis';
|
||||
import { ValidationError } from './types/ValidationError';
|
||||
|
||||
// Initialize Express Application
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use(express.json({ limit: MAX_BODY_SIZE }));
|
||||
|
||||
// Create and configure Redis client
|
||||
const redisClient = createRedisClient();
|
||||
|
@ -25,6 +25,11 @@ func main() {
|
||||
log.SetFormatter(&log.JSONFormatter{})
|
||||
log := log.WithField("_routine", "main")
|
||||
|
||||
if activitiesOverviewEnabled := os.Getenv("BBB_GRAPHQL_MIDDLEWARE_ACTIVITIES_OVERVIEW_ENABLED"); activitiesOverviewEnabled == "true" {
|
||||
go common.ActivitiesOverviewLogRoutine()
|
||||
//go common.JsonPatchBenchmarkingLogRoutine()
|
||||
}
|
||||
|
||||
common.InitUniqueID()
|
||||
log = log.WithField("graphql-middleware-uid", common.GetUniqueID())
|
||||
|
||||
@ -36,6 +41,10 @@ func main() {
|
||||
// Listen msgs from akka (for example to invalidate connection)
|
||||
go websrv.StartRedisListener()
|
||||
|
||||
if jsonPatchDisabled := os.Getenv("BBB_GRAPHQL_MIDDLEWARE_JSON_PATCH_DISABLED"); jsonPatchDisabled != "" {
|
||||
log.Infof("Json Patch Disabled!")
|
||||
}
|
||||
|
||||
// Websocket listener
|
||||
|
||||
//Define IP to listen
|
||||
@ -65,6 +74,9 @@ func main() {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 120*time.Second)
|
||||
defer cancel()
|
||||
|
||||
common.ActivitiesOverviewStarted("__WebsocketConnection")
|
||||
defer common.ActivitiesOverviewCompleted("__WebsocketConnection")
|
||||
|
||||
if err := rateLimiter.Wait(ctx); err != nil {
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
http.Error(w, "Request cancelled or rate limit exceeded", http.StatusTooManyRequests)
|
||||
|
@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
./local-build.sh
|
||||
mv bbb-graphql-middleware /usr/local/bin/bbb-graphql-middleware
|
||||
systemctl restart bbb-graphql-middleware
|
||||
sudo mv bbb-graphql-middleware /usr/local/bin/bbb-graphql-middleware
|
||||
sudo systemctl restart bbb-graphql-middleware
|
||||
|
@ -3,6 +3,8 @@ module github.com/iMDT/bbb-graphql-middleware
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/evanphx/json-patch v0.5.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mattbaird/jsonpatch v0.0.0-20230413205102-771768614e91
|
||||
github.com/redis/go-redis/v9 v9.0.3
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
@ -13,8 +15,6 @@ require (
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/evanphx/json-patch v0.5.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user