Merge remote-tracking branch 'upstream/v3.0.x-release' into v3.0.x-release

This commit is contained in:
Gustavo Trott 2024-04-15 08:48:52 -03:00
commit c2474ae607
357 changed files with 10749 additions and 7009 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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("\\.")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import org.bigbluebutton.core.apps.groupchats.GroupChatApp
import org.bigbluebutton.core.apps.users.UsersApp
import org.bigbluebutton.core.apps.voice.VoiceApp
import org.bigbluebutton.core.bus.{BigBlueButtonEvent, InternalEventBus}
import org.bigbluebutton.core.db.{BreakoutRoomUserDAO, 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)) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,10 +9,8 @@ module.exports = {
],
'parserOptions': {
'ecmaVersion': 'latest',
'sourceType': 'module',
},
'rules': {
'require-jsdoc': 0,
'camelcase': 0,
'max-len': 0,
},
};

View File

@ -1,4 +0,0 @@
const settings = require('./settings');
const config = settings;
module.exports = config;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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',
});

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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);
}
}

View 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, '\\&amp;') // Escape ampersands.
.replace(/'/g, '\\&apos;') // Escape single quotes.
.replace(/"/g, '\\&quot;') // Escape double quotes.
.replace(/>/g, '\\&gt;') // Escape greater-than signs.
.replace(/</g, '\\&lt;'); // Escape less-than signs.
}

View File

@ -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}`);
}

View File

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

View File

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

View File

@ -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": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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