Merge remote-tracking branch 'upstream/v2.6.x-release' into demo-page-removal
This commit is contained in:
commit
bdb9c6857f
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -14,4 +14,4 @@ contact_links:
|
||||
about: Issue tracker for the Greenlight frontend
|
||||
- name: Commercial Support
|
||||
url: https://bigbluebutton.org/commercial-support
|
||||
abount: List of companies offering commercial BigBlueButton support
|
||||
about: List of companies offering commercial BigBlueButton support
|
||||
|
53
.github/workflows/automated-tests.yml
vendored
53
.github/workflows/automated-tests.yml
vendored
@ -16,9 +16,11 @@ jobs:
|
||||
- run: ./build/setup.sh bbb-apps-akka
|
||||
- run: ./build/setup.sh bbb-config
|
||||
- run: ./build/setup.sh bbb-etherpad
|
||||
- run: ./build/setup.sh bbb-export-annotations
|
||||
- run: ./build/setup.sh bbb-freeswitch-core
|
||||
- run: ./build/setup.sh bbb-freeswitch-sounds
|
||||
- run: ./build/setup.sh bbb-fsesl-akka
|
||||
- run: ./build/setup.sh bbb-html5-nodejs
|
||||
- run: ./build/setup.sh bbb-html5
|
||||
- run: ./build/setup.sh bbb-learning-dashboard
|
||||
- run: ./build/setup.sh bbb-libreoffice-docker
|
||||
@ -53,11 +55,9 @@ jobs:
|
||||
sudo sh -c '
|
||||
mkdir /root/bbb-ci-ssl/
|
||||
cd /root/bbb-ci-ssl/
|
||||
|
||||
openssl rand -base64 48 > /root/bbb-ci-ssl/bbb-dev-ca.pass ;
|
||||
chmod 600 /root/bbb-ci-ssl/bbb-dev-ca.pass ;
|
||||
openssl genrsa -des3 -out bbb-dev-ca.key -passout file:/root/bbb-ci-ssl/bbb-dev-ca.pass 2048 ;
|
||||
|
||||
openssl req -x509 -new -nodes -key bbb-dev-ca.key -sha256 -days 1460 -passin file:/root/bbb-ci-ssl/bbb-dev-ca.pass -out bbb-dev-ca.crt -subj "/C=CA/ST=BBB/L=BBB/O=BBB/OU=BBB/CN=BBB-DEV" ;
|
||||
'
|
||||
- name: Trust CA
|
||||
@ -67,7 +67,6 @@ jobs:
|
||||
sudo cp /root/bbb-ci-ssl/bbb-dev-ca.crt /usr/local/share/ca-certificates/bbb-dev/
|
||||
sudo chmod 644 /usr/local/share/ca-certificates/bbb-dev/bbb-dev-ca.crt
|
||||
sudo update-ca-certificates
|
||||
|
||||
'
|
||||
- name: Generate certificate
|
||||
run: |
|
||||
@ -84,9 +83,7 @@ jobs:
|
||||
[alt_names]
|
||||
DNS.1 = bbb-ci.test
|
||||
EOF
|
||||
|
||||
openssl req -nodes -newkey rsa:2048 -keyout bbb-ci.test.key -out bbb-ci.test.csr -subj "/C=CA/ST=BBB/L=BBB/O=BBB/OU=BBB/CN=bbb-ci.test" -addext "subjectAltName = DNS:bbb-ci.test"
|
||||
|
||||
openssl req -nodes -newkey rsa:2048 -keyout bbb-ci.test.key -out bbb-ci.test.csr -subj "/C=CA/ST=BBB/L=BBB/O=BBB/OU=BBB/CN=bbb-ci.test" -addext "subjectAltName = DNS:bbb-ci.test"
|
||||
openssl x509 -req -in bbb-ci.test.csr -CA bbb-dev-ca.crt -CAkey bbb-dev-ca.key -CAcreateserial -out bbb-ci.test.crt -days 825 -sha256 -passin file:/root/bbb-ci-ssl/bbb-dev-ca.pass -extfile bbb-ci.test.ext
|
||||
cd
|
||||
|
||||
@ -95,7 +92,6 @@ jobs:
|
||||
cat /root/bbb-ci-ssl/bbb-ci.test.crt > /local/certs/fullchain.pem
|
||||
cat /root/bbb-ci-ssl/bbb-dev-ca.crt >> /local/certs/fullchain.pem
|
||||
cat /root/bbb-ci-ssl/bbb-ci.test.key > /local/certs/privkey.pem
|
||||
|
||||
'
|
||||
- name: Setup local repository
|
||||
run: |
|
||||
@ -115,25 +111,32 @@ jobs:
|
||||
- name: Install BBB
|
||||
run: |
|
||||
sudo sh -c '
|
||||
cd /root/ && wget -q https://ubuntu.bigbluebutton.org/bbb-install-2.5.sh -O bbb-install.sh
|
||||
cat bbb-install.sh | sed "s|> /etc/apt/sources.list.d/bigbluebutton.list||g" | bash -s -- -v focal-25-dev -s bbb-ci.test -d /certs/
|
||||
cd /root/ && wget -q https://ubuntu.bigbluebutton.org/bbb-install-2.6.sh -O bbb-install.sh
|
||||
cat bbb-install.sh | sed "s|> /etc/apt/sources.list.d/bigbluebutton.list||g" | bash -s -- -v focal-26-dev -s bbb-ci.test -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
|
||||
bbb-conf --restart
|
||||
'
|
||||
- name: Run tests
|
||||
- name: Install test dependencies
|
||||
working-directory: ./bigbluebutton-tests/playwright
|
||||
run: |
|
||||
sh -c '
|
||||
echo "Teste"
|
||||
cd /home/runner/work/bigbluebutton/bigbluebutton/bigbluebutton-tests/playwright/
|
||||
echo "
|
||||
BBB_URL=\"https://bbb-ci.test/bigbluebutton/api\"
|
||||
BBB_SECRET=\"bbbci\"
|
||||
|
||||
DEBUG_MODE=\"\"
|
||||
" > .env
|
||||
npm install
|
||||
npx playwright install-deps
|
||||
npx playwright install
|
||||
export NODE_TLS_REJECT_UNAUTHORIZED='0'
|
||||
npx playwright test --project=chromium --grep @ci
|
||||
'
|
||||
sh -c '
|
||||
npm install
|
||||
npx playwright install-deps
|
||||
npx playwright install
|
||||
'
|
||||
- name: Run tests
|
||||
working-directory: ./bigbluebutton-tests/playwright
|
||||
env:
|
||||
NODE_EXTRA_CA_CERTS: /usr/local/share/ca-certificates/bbb-dev/bbb-dev-ca.crt
|
||||
ACTIONS_RUNNER_DEBUG: true
|
||||
BBB_URL: https://bbb-ci.test/bigbluebutton/api
|
||||
BBB_SECRET: bbbci
|
||||
run: npm run test-ci
|
||||
- if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: tests-report
|
||||
path: |
|
||||
bigbluebutton-tests/playwright/playwright-report
|
||||
bigbluebutton-tests/playwright/test-results
|
||||
|
@ -11,7 +11,7 @@ stages:
|
||||
|
||||
# define which docker image to use for builds
|
||||
default:
|
||||
image: gitlab.senfcall.de:5050/senfcall-public/docker-bbb-build:v2022-03-30
|
||||
image: gitlab.senfcall.de:5050/senfcall-public/docker-bbb-build:v2022-07-12
|
||||
|
||||
# This stage uses git to find out since when each package has been unmodified.
|
||||
# it then checks an API endpoint on the package server to find out for which of
|
||||
|
28
.travis.yml
28
.travis.yml
@ -1,28 +0,0 @@
|
||||
dist: focal
|
||||
language: node_js
|
||||
node_js: 16
|
||||
|
||||
env:
|
||||
- JOB_TYPE=linter
|
||||
- JOB_TYPE=acceptance_tests
|
||||
global:
|
||||
- BBB_SERVER_URL=http://localhost/bigbluebutton/api
|
||||
|
||||
jobs:
|
||||
allow_failures:
|
||||
- env: JOB_TYPE=linter
|
||||
include:
|
||||
- stage: "Linter"
|
||||
name: "ESLint"
|
||||
env: JOB_TYPE=linter
|
||||
- stage: "Tests"
|
||||
name: "Acceptance Tests"
|
||||
env: JOB_TYPE=acceptance_tests
|
||||
|
||||
script:
|
||||
- travis_wait 30 bash ./build_script.sh $JOB_TYPE
|
||||
|
||||
after_script:
|
||||
- docker stop $docker
|
||||
- docker rm $docker
|
||||
- docker rmi b2
|
@ -6,13 +6,14 @@ We actively support BigBlueButton through the community forums and through secur
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 2.2.x (or earlier) | :x: |
|
||||
| 2.3.x | :white_check_mark: |
|
||||
| 2.3.x (or earlier) | :x: |
|
||||
| 2.4.x | :white_check_mark: |
|
||||
| 2.5.x | :white_check_mark: |
|
||||
| 2.6.x | :x: |
|
||||
|
||||
We have released 2.4 to the community and are going to support both 2.4 and 2.3 together for the coming months (while we're actively developing the next release). Also, BigBlueButton 2.2 is running on Ubuntu 16.04 which is now end of life.
|
||||
We have released 2.5 to the community and are going to support both 2.4 and 2.5 together for the coming months (while we're actively developing the next release). Also, BigBlueButton 2.3 is now end of life.
|
||||
|
||||
As such, we recommend that all administrators deploy 2.4 going forward. You'll find [many improvements](https://docs.bigbluebutton.org/2.4/new.html) in this newer version.
|
||||
As such, we recommend that all administrators deploy 2.5 going forward. You'll find [many improvements](https://docs.bigbluebutton.org/2.5/new.html) in this newer version.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
@ -11,7 +11,7 @@ import org.bigbluebutton.core.pubsub.senders.ReceivedJsonMsgHandlerActor
|
||||
import org.bigbluebutton.core2.AnalyticsActor
|
||||
import org.bigbluebutton.core2.FromAkkaAppsMsgSenderActor
|
||||
import org.bigbluebutton.endpoint.redis.AppsRedisSubscriberActor
|
||||
import org.bigbluebutton.endpoint.redis.RedisRecorderActor
|
||||
import org.bigbluebutton.endpoint.redis.{ RedisRecorderActor, ExportAnnotationsActor }
|
||||
import org.bigbluebutton.endpoint.redis.LearningDashboardActor
|
||||
import org.bigbluebutton.common2.bus.IncomingJsonMessageBus
|
||||
import org.bigbluebutton.service.{ HealthzService, MeetingInfoActor, MeetingInfoService }
|
||||
@ -59,6 +59,11 @@ object Boot extends App with SystemConfiguration {
|
||||
"redisRecorderActor"
|
||||
)
|
||||
|
||||
val exportAnnotationsActor = system.actorOf(
|
||||
ExportAnnotationsActor.props(system, redisConfig, healthzService),
|
||||
"exportAnnotationsActor"
|
||||
)
|
||||
|
||||
val learningDashboardActor = system.actorOf(
|
||||
LearningDashboardActor.props(system, outGW),
|
||||
"LearningDashboardActor"
|
||||
@ -72,6 +77,7 @@ object Boot extends App with SystemConfiguration {
|
||||
val analyticsActorRef = system.actorOf(AnalyticsActor.props(analyticsIncludeChat))
|
||||
outBus2.subscribe(fromAkkaAppsMsgSenderActorRef, outBbbMsgMsgChannel)
|
||||
outBus2.subscribe(redisRecorderActor, recordServiceMessageChannel)
|
||||
outBus2.subscribe(exportAnnotationsActor, outBbbMsgMsgChannel)
|
||||
|
||||
outBus2.subscribe(analyticsActorRef, outBbbMsgMsgChannel)
|
||||
bbbMsgBus.subscribe(analyticsActorRef, analyticsChannel)
|
||||
|
@ -75,6 +75,12 @@ object LockSettingsUtil {
|
||||
}
|
||||
}
|
||||
|
||||
def isMicrophoneSharingLocked(user: UserState, liveMeeting: LiveMeeting): Boolean = {
|
||||
val permissions = MeetingStatus2x.getPermissions(liveMeeting.status)
|
||||
|
||||
user.role == Roles.VIEWER_ROLE && user.locked && permissions.disableMic
|
||||
}
|
||||
|
||||
def isCameraBroadcastLocked(user: UserState, liveMeeting: LiveMeeting): Boolean = {
|
||||
val permissions = MeetingStatus2x.getPermissions(liveMeeting.status)
|
||||
|
||||
|
@ -70,11 +70,6 @@ trait SystemConfiguration {
|
||||
|
||||
lazy val fromBbbWebRedisChannel = Try(config.getString("redis.fromBbbWebRedisChannel")).getOrElse("from-bbb-web-redis-channel")
|
||||
|
||||
lazy val toAkkaTranscodeRedisChannel = Try(config.getString("redis.toAkkaTranscodeRedisChannel")).getOrElse("bigbluebutton:to-bbb-transcode:system")
|
||||
lazy val fromAkkaTranscodeRedisChannel = Try(config.getString("redis.fromAkkaTranscodeRedisChannel")).getOrElse("bigbluebutton:from-bbb-transcode:system")
|
||||
lazy val toAkkaTranscodeJsonChannel = Try(config.getString("eventBus.toAkkaTranscodeJsonChannel")).getOrElse("to-akka-transcode-json-channel")
|
||||
lazy val fromAkkaTranscodeJsonChannel = Try(config.getString("eventBus.fromAkkaTranscodeJsonChannel")).getOrElse("from-akka-transcode-json-channel")
|
||||
|
||||
lazy val analyticsIncludeChat = Try(config.getBoolean("analytics.includeChat")).getOrElse(true)
|
||||
|
||||
// Grab the "interface" parameter from the http config
|
||||
|
@ -187,9 +187,6 @@ class BigBlueButtonActor(
|
||||
val disconnectEvnt = MsgBuilder.buildDisconnectAllClientsSysMsg(msg.meetingId, "meeting-destroyed")
|
||||
m2.outMsgRouter.send(disconnectEvnt)
|
||||
|
||||
val stopTranscodersCmd = MsgBuilder.buildStopMeetingTranscodersSysCmdMsg(msg.meetingId)
|
||||
m2.outMsgRouter.send(stopTranscodersCmd)
|
||||
|
||||
log.info("Destroyed meetingId={}", msg.meetingId)
|
||||
val destroyedEvent = MsgBuilder.buildMeetingDestroyedEvtMsg(msg.meetingId)
|
||||
m2.outMsgRouter.send(destroyedEvent)
|
||||
|
@ -20,7 +20,7 @@ trait CreateDefaultPublicGroupChat {
|
||||
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId)
|
||||
val envelope = BbbCoreEnvelope(GroupChatCreatedEvtMsg.NAME, routing)
|
||||
val header = BbbClientMsgHeader(GroupChatCreatedEvtMsg.NAME, meetingId, userId)
|
||||
val body = GroupChatCreatedEvtMsgBody(correlationId, gc.id, gc.createdBy, gc.name, gc.access, gc.users, msgs)
|
||||
val body = GroupChatCreatedEvtMsgBody(correlationId, gc.id, gc.createdBy, gc.access, gc.users, msgs)
|
||||
val event = GroupChatCreatedEvtMsg(header, body)
|
||||
BbbCommonEnvCoreMsg(envelope, event)
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ trait CreateGroupChatReqMsgHdlr extends SystemConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
val gc = GroupChatApp.createGroupChat(msg.body.name, msg.body.access, createdBy, users, msgs)
|
||||
val gc = GroupChatApp.createGroupChat(msg.body.access, createdBy, users, msgs)
|
||||
sendMessages(msg, gc, liveMeeting, bus)
|
||||
|
||||
val groupChats = state.groupChats.add(gc)
|
||||
@ -84,12 +84,12 @@ trait CreateGroupChatReqMsgHdlr extends SystemConfiguration {
|
||||
BbbCoreEnvelope(name, routing)
|
||||
}
|
||||
|
||||
def makeBody(chatId: String, name: String,
|
||||
def makeBody(chatId: String,
|
||||
access: String, correlationId: String,
|
||||
createdBy: GroupChatUser, users: Vector[GroupChatUser],
|
||||
msgs: Vector[GroupChatMsgToUser]): GroupChatCreatedEvtMsgBody = {
|
||||
GroupChatCreatedEvtMsgBody(correlationId, chatId, createdBy,
|
||||
name, access, users, msgs)
|
||||
access, users, msgs)
|
||||
}
|
||||
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
@ -102,7 +102,7 @@ trait CreateGroupChatReqMsgHdlr extends SystemConfiguration {
|
||||
val envelope = makeEnvelope(MessageTypes.DIRECT, GroupChatCreatedEvtMsg.NAME, meetingId, userId)
|
||||
val header = makeHeader(GroupChatCreatedEvtMsg.NAME, meetingId, userId)
|
||||
|
||||
val body = makeBody(gc.id, gc.name, gc.access, correlationId, gc.createdBy, users, msgs)
|
||||
val body = makeBody(gc.id, gc.access, correlationId, gc.createdBy, users, msgs)
|
||||
val event = GroupChatCreatedEvtMsg(header, body)
|
||||
val outEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
bus.outGW.send(outEvent)
|
||||
@ -117,7 +117,7 @@ trait CreateGroupChatReqMsgHdlr extends SystemConfiguration {
|
||||
meetingId, userId)
|
||||
val header = makeHeader(GroupChatCreatedEvtMsg.NAME, meetingId, userId)
|
||||
|
||||
val body = makeBody(gc.id, gc.name, gc.access, correlationId, gc.createdBy, users, msgs)
|
||||
val body = makeBody(gc.id, gc.access, correlationId, gc.createdBy, users, msgs)
|
||||
val event = GroupChatCreatedEvtMsg(header, body)
|
||||
|
||||
val outEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
|
@ -27,8 +27,8 @@ trait GetGroupChatsReqMsgHdlr {
|
||||
|
||||
val publicChats = state.groupChats.findAllPublicChats()
|
||||
val privateChats = state.groupChats.findAllPrivateChatsForUser(msg.header.userId)
|
||||
val pubChats = publicChats map (pc => GroupChatInfo(pc.id, pc.name, pc.access, pc.createdBy, pc.users))
|
||||
val privChats = privateChats map (pc => GroupChatInfo(pc.id, pc.name, pc.access, pc.createdBy, pc.users))
|
||||
val pubChats = publicChats map (pc => GroupChatInfo(pc.id, pc.access, pc.createdBy, pc.users))
|
||||
val privChats = privateChats map (pc => GroupChatInfo(pc.id, pc.access, pc.createdBy, pc.users))
|
||||
|
||||
val allChats = pubChats ++ privChats
|
||||
|
||||
|
@ -9,10 +9,10 @@ object GroupChatApp {
|
||||
|
||||
val MAIN_PUBLIC_CHAT = "MAIN-PUBLIC-GROUP-CHAT"
|
||||
|
||||
def createGroupChat(chatName: String, access: String, createBy: GroupChatUser,
|
||||
def createGroupChat(access: String, createBy: GroupChatUser,
|
||||
users: Vector[GroupChatUser], msgs: Vector[GroupChatMessage]): GroupChat = {
|
||||
val gcId = GroupChatFactory.genId()
|
||||
GroupChatFactory.create(gcId, chatName, access, createBy, users, msgs)
|
||||
GroupChatFactory.create(gcId, access, createBy, users, msgs)
|
||||
}
|
||||
|
||||
def toGroupChatMessage(sender: GroupChatUser, msg: GroupChatMsgFromUser): GroupChatMessage = {
|
||||
@ -46,13 +46,15 @@ object GroupChatApp {
|
||||
|
||||
def createDefaultPublicGroupChat(): GroupChat = {
|
||||
val createBy = GroupChatUser(SystemUser.ID)
|
||||
GroupChatFactory.create(MAIN_PUBLIC_CHAT, MAIN_PUBLIC_CHAT, GroupChatAccess.PUBLIC, createBy, Vector.empty, Vector.empty)
|
||||
GroupChatFactory.create(MAIN_PUBLIC_CHAT, GroupChatAccess.PUBLIC, createBy, Vector.empty, Vector.empty)
|
||||
}
|
||||
|
||||
def createTestPublicGroupChat(state: MeetingState2x): MeetingState2x = {
|
||||
val createBy = GroupChatUser(SystemUser.ID)
|
||||
val defaultPubGroupChat = GroupChatFactory.create("TEST_GROUP_CHAT", "TEST_GROUP_CHAT",
|
||||
GroupChatAccess.PUBLIC, createBy, Vector.empty, Vector.empty)
|
||||
val defaultPubGroupChat = GroupChatFactory.create(
|
||||
"TEST_GROUP_CHAT",
|
||||
GroupChatAccess.PUBLIC, createBy, Vector.empty, Vector.empty
|
||||
)
|
||||
val groupChats = state.groupChats.add(defaultPubGroupChat)
|
||||
state.update(groupChats)
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.bigbluebutton.core.apps.groupchats
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.apps.PermissionCheck
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.domain.MeetingState2x
|
||||
import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting }
|
||||
@ -43,17 +44,27 @@ trait SendGroupChatMessageMsgHdlr extends HandlerHelpers {
|
||||
sender <- GroupChatApp.findGroupChatUser(msg.header.userId, liveMeeting.users2x)
|
||||
chat <- state.groupChats.find(msg.body.chatId)
|
||||
} yield {
|
||||
val gcm = GroupChatApp.toGroupChatMessage(sender, msg.body.msg)
|
||||
val gcs = GroupChatApp.addGroupChatMessage(chat, state.groupChats, gcm)
|
||||
val chatIsPrivate = chat.access == GroupChatAccess.PRIVATE;
|
||||
val userIsAParticipant = chat.users.filter(u => u.id == sender.id).length > 0;
|
||||
|
||||
val event = buildGroupChatMessageBroadcastEvtMsg(
|
||||
liveMeeting.props.meetingProp.intId,
|
||||
msg.header.userId, msg.body.chatId, gcm
|
||||
)
|
||||
if ((chatIsPrivate && userIsAParticipant) || !chatIsPrivate) {
|
||||
val gcm = GroupChatApp.toGroupChatMessage(sender, msg.body.msg)
|
||||
val gcs = GroupChatApp.addGroupChatMessage(chat, state.groupChats, gcm)
|
||||
|
||||
bus.outGW.send(event)
|
||||
val event = buildGroupChatMessageBroadcastEvtMsg(
|
||||
liveMeeting.props.meetingProp.intId,
|
||||
msg.header.userId, msg.body.chatId, gcm
|
||||
)
|
||||
|
||||
bus.outGW.send(event)
|
||||
|
||||
state.update(gcs)
|
||||
} else {
|
||||
val reason = "User isn't a participant of the chat"
|
||||
PermissionCheck.ejectUserForFailedPermission(msg.header.meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
|
||||
state
|
||||
}
|
||||
|
||||
state.update(gcs)
|
||||
}
|
||||
|
||||
newState match {
|
||||
|
@ -41,7 +41,7 @@ trait SyncGetGroupChatsInfoMsgHdlr {
|
||||
val respMsg = buildSyncGetGroupChatMsgsRespMsg(msgs, pc.id)
|
||||
bus.outGW.send(respMsg)
|
||||
|
||||
GroupChatInfo(pc.id, pc.name, pc.access, pc.createdBy, pc.users)
|
||||
GroupChatInfo(pc.id, pc.access, pc.createdBy, pc.users)
|
||||
})
|
||||
|
||||
// publishing a message with the group chat info
|
||||
|
@ -19,6 +19,7 @@ class PresentationPodHdlrs(implicit val context: ActorContext)
|
||||
with PresentationPageCountErrorPubMsgHdlr
|
||||
with PresentationUploadedFileTooLargeErrorPubMsgHdlr
|
||||
with PresentationUploadTokenReqMsgHdlr
|
||||
with PresentationWithAnnotationsMsgHdlr
|
||||
with ResizeAndMovePagePubMsgHdlr
|
||||
with SyncGetPresentationPodsMsgHdlr
|
||||
with RemovePresentationPodPubMsgHdlr
|
||||
|
@ -0,0 +1,164 @@
|
||||
package org.bigbluebutton.core.apps.presentationpod
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.domain.MeetingState2x
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
import org.bigbluebutton.core.util.RandomStringGenerator
|
||||
import org.bigbluebutton.core.models.{ PresentationPod, PresentationPage, PresentationInPod }
|
||||
import java.io.File
|
||||
|
||||
trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
|
||||
this: PresentationPodHdlrs =>
|
||||
|
||||
def buildStoreAnnotationsInRedisSysMsg(annotations: StoredAnnotations, liveMeeting: LiveMeeting): BbbCommonEnvCoreMsg = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
|
||||
val envelope = BbbCoreEnvelope(StoreAnnotationsInRedisSysMsg.NAME, routing)
|
||||
val body = StoreAnnotationsInRedisSysMsgBody(annotations)
|
||||
val header = BbbCoreHeaderWithMeetingId(StoreAnnotationsInRedisSysMsg.NAME, liveMeeting.props.meetingProp.intId)
|
||||
val event = StoreAnnotationsInRedisSysMsg(header, body)
|
||||
|
||||
BbbCommonEnvCoreMsg(envelope, event)
|
||||
}
|
||||
|
||||
def buildStoreExportJobInRedisSysMsg(exportJob: ExportJob, liveMeeting: LiveMeeting): BbbCommonEnvCoreMsg = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
|
||||
val envelope = BbbCoreEnvelope(StoreExportJobInRedisSysMsg.NAME, routing)
|
||||
val body = StoreExportJobInRedisSysMsgBody(exportJob)
|
||||
val header = BbbCoreHeaderWithMeetingId(StoreExportJobInRedisSysMsg.NAME, liveMeeting.props.meetingProp.intId)
|
||||
val event = StoreExportJobInRedisSysMsg(header, body)
|
||||
|
||||
BbbCommonEnvCoreMsg(envelope, event)
|
||||
}
|
||||
|
||||
def buildPresentationUploadTokenSysPubMsg(parentId: String, userId: String, presentationUploadToken: String, filename: String): BbbCommonEnvCoreMsg = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
|
||||
val envelope = BbbCoreEnvelope(PresentationUploadTokenSysPubMsg.NAME, routing)
|
||||
val header = BbbClientMsgHeader(PresentationUploadTokenSysPubMsg.NAME, parentId, userId)
|
||||
val body = PresentationUploadTokenSysPubMsgBody("DEFAULT_PRESENTATION_POD", presentationUploadToken, filename, parentId)
|
||||
val event = PresentationUploadTokenSysPubMsg(header, body)
|
||||
BbbCommonEnvCoreMsg(envelope, event)
|
||||
}
|
||||
|
||||
def getPresentationPagesForExport(pagesRange: List[Int], pageCount: Int, presId: String, currentPres: Option[PresentationInPod], liveMeeting: LiveMeeting, storeAnnotationPages: List[PresentationPageForExport] = List()): List[PresentationPageForExport] = {
|
||||
|
||||
pagesRange match {
|
||||
case (pageNumber :: pages) => {
|
||||
|
||||
if (pageNumber >= 1 && pageNumber <= pageCount) {
|
||||
|
||||
val whiteboardId = s"${presId}/${pageNumber.toString}"
|
||||
val presentationPage: PresentationPage = currentPres.get.pages(whiteboardId)
|
||||
val xCamera: Double = presentationPage.xCamera
|
||||
val yCamera: Double = presentationPage.yCamera
|
||||
val zoom: Double = presentationPage.zoom
|
||||
val whiteboardHistory: Array[AnnotationVO] = liveMeeting.wbModel.getHistory(whiteboardId)
|
||||
|
||||
val page = new PresentationPageForExport(pageNumber, xCamera, yCamera, zoom, whiteboardHistory)
|
||||
getPresentationPagesForExport(pages, pageCount, presId, currentPres, liveMeeting, storeAnnotationPages :+ page)
|
||||
} else {
|
||||
getPresentationPagesForExport(pages, pageCount, presId, currentPres, liveMeeting, storeAnnotationPages)
|
||||
}
|
||||
}
|
||||
|
||||
case _ => storeAnnotationPages
|
||||
}
|
||||
}
|
||||
|
||||
def handle(m: MakePresentationWithAnnotationDownloadReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
|
||||
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
val userId = m.header.userId
|
||||
|
||||
val presentationPods: Vector[PresentationPod] = state.presentationPodManager.getAllPresentationPodsInMeeting()
|
||||
val currentPres: Option[PresentationInPod] = presentationPods.flatMap(_.getCurrentPresentation()).headOption
|
||||
|
||||
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, userId)) {
|
||||
val reason = "No permission to export presentation."
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, userId, reason, bus.outGW, liveMeeting)
|
||||
} else if (currentPres.isEmpty) {
|
||||
log.error(s"No presentation set in meeting ${meetingId}")
|
||||
} else {
|
||||
|
||||
val jobType: String = "PresentationWithAnnotationDownloadJob"
|
||||
val jobId: String = RandomStringGenerator.randomAlphanumericString(16);
|
||||
val allPages: Boolean = m.body.allPages
|
||||
val pageCount = currentPres.get.pages.size
|
||||
|
||||
val presId: String = m.body.presId match {
|
||||
case "" => PresentationPodsApp.getAllPresentationPodsInMeeting(state).flatMap(_.getCurrentPresentation.map(_.id)).mkString
|
||||
case _ => m.body.presId
|
||||
}
|
||||
|
||||
val presLocation = List("var", "bigbluebutton", meetingId, meetingId, presId).mkString(File.separator, File.separator, "");
|
||||
val pages: List[Int] = m.body.pages // Desired presentation pages for export
|
||||
val pagesRange: List[Int] = if (allPages) (1 to pageCount).toList else pages
|
||||
|
||||
val exportJob: ExportJob = new ExportJob(jobId, jobType, "annotated_slides", presId, presLocation, allPages, pagesRange, meetingId, "");
|
||||
val storeAnnotationPages: List[PresentationPageForExport] = getPresentationPagesForExport(pagesRange, pageCount, presId, currentPres, liveMeeting);
|
||||
|
||||
// Send Export Job to Redis
|
||||
val job = buildStoreExportJobInRedisSysMsg(exportJob, liveMeeting)
|
||||
bus.outGW.send(job)
|
||||
|
||||
// Send Annotations to Redis
|
||||
val annotations = new StoredAnnotations(jobId, presId, storeAnnotationPages)
|
||||
bus.outGW.send(buildStoreAnnotationsInRedisSysMsg(annotations, liveMeeting))
|
||||
}
|
||||
}
|
||||
|
||||
def handle(m: ExportPresentationWithAnnotationReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
|
||||
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
val userId = m.header.userId
|
||||
|
||||
val presentationPods: Vector[PresentationPod] = state.presentationPodManager.getAllPresentationPodsInMeeting()
|
||||
val currentPres: Option[PresentationInPod] = presentationPods.flatMap(_.getCurrentPresentation()).headOption
|
||||
|
||||
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, userId)) {
|
||||
val reason = "No permission to export presentation."
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, userId, reason, bus.outGW, liveMeeting)
|
||||
} else if (currentPres.isEmpty) {
|
||||
log.error(s"No presentation set in meeting ${meetingId}")
|
||||
} else {
|
||||
|
||||
val jobId: String = RandomStringGenerator.randomAlphanumericString(16);
|
||||
val jobType = "PresentationWithAnnotationExportJob"
|
||||
val allPages: Boolean = m.body.allPages
|
||||
val pageCount = currentPres.get.pages.size
|
||||
|
||||
val presId: String = PresentationPodsApp.getAllPresentationPodsInMeeting(state).flatMap(_.getCurrentPresentation.map(_.id)).mkString
|
||||
val presLocation = List("var", "bigbluebutton", meetingId, meetingId, presId).mkString(File.separator, File.separator, "");
|
||||
val parentMeetingId: String = m.body.parentMeetingId
|
||||
|
||||
val currentPage: PresentationPage = PresentationInPod.getCurrentPage(currentPres.get).get
|
||||
val pagesRange: List[Int] = if (allPages) (1 to pageCount).toList else List(currentPage.num)
|
||||
|
||||
val presentationUploadToken: String = PresentationPodsApp.generateToken("DEFAULT_PRESENTATION_POD", userId)
|
||||
|
||||
// Set filename, checking if it is already in use
|
||||
val meetingName: String = liveMeeting.props.meetingProp.name
|
||||
val duplicatedCount = presentationPods.flatMap(_.getPresentationsByFilename(meetingName)).size
|
||||
|
||||
val filename = duplicatedCount match {
|
||||
case 0 => meetingName
|
||||
case _ => s"${meetingName}(${duplicatedCount})"
|
||||
}
|
||||
|
||||
// Informs bbb-web about the token so that when we use it to upload the presentation, it is able to look it up in the list of tokens
|
||||
bus.outGW.send(buildPresentationUploadTokenSysPubMsg(parentMeetingId, userId, presentationUploadToken, filename))
|
||||
|
||||
val exportJob: ExportJob = new ExportJob(jobId, jobType, filename, presId, presLocation, allPages, pagesRange, parentMeetingId, presentationUploadToken)
|
||||
val storeAnnotationPages: List[PresentationPageForExport] = getPresentationPagesForExport(pagesRange, pageCount, presId, currentPres, liveMeeting);
|
||||
|
||||
// Send Export Job to Redis
|
||||
val job = buildStoreExportJobInRedisSysMsg(exportJob, liveMeeting)
|
||||
bus.outGW.send(job)
|
||||
|
||||
// Send Annotations to Redis
|
||||
val annotations = new StoredAnnotations(jobId, presId, storeAnnotationPages)
|
||||
bus.outGW.send(buildStoreAnnotationsInRedisSysMsg(annotations, liveMeeting))
|
||||
}
|
||||
}
|
||||
}
|
@ -25,8 +25,7 @@ trait GetScreenBroadcastPermissionReqMsgHdlr {
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
val reason = "No permission to share the screen."
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)
|
||||
} else if (!user.userLeftFlag.left
|
||||
&& liveMeeting.props.meetingProp.intId == msg.body.meetingId
|
||||
} else if (liveMeeting.props.meetingProp.intId == msg.body.meetingId
|
||||
&& liveMeeting.props.voiceProp.voiceConf == msg.body.voiceConf) {
|
||||
allowed = true
|
||||
}
|
||||
|
@ -17,8 +17,7 @@ trait GetScreenSubscribePermissionReqMsgHdlr {
|
||||
for {
|
||||
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId)
|
||||
} yield {
|
||||
if (!user.userLeftFlag.left
|
||||
&& liveMeeting.props.meetingProp.intId == msg.body.meetingId
|
||||
if (liveMeeting.props.meetingProp.intId == msg.body.meetingId
|
||||
&& liveMeeting.props.voiceProp.voiceConf == msg.body.voiceConf
|
||||
&& ScreenshareModel.getRTMPBroadcastingUrl(liveMeeting.screenshareModel) == msg.body.streamId) {
|
||||
allowed = true
|
||||
|
@ -51,7 +51,7 @@ trait EjectUserFromMeetingCmdMsgHdlr extends RightsManagementTrait {
|
||||
breakoutModel <- state.breakout
|
||||
} yield {
|
||||
breakoutModel.rooms.values.foreach { room =>
|
||||
room.users.filter(u => u.id == ru.externId + "-" + room.sequence).foreach(user => {
|
||||
room.users.filter(u => u.id == ru.id + "-" + room.sequence).foreach(user => {
|
||||
eventBus.publish(BigBlueButtonEvent(room.id, EjectUserFromBreakoutInternalMsg(meetingId, room.id, user.id, ejectedBy, reason, EjectReasonCode.EJECT_USER, ban)))
|
||||
})
|
||||
}
|
||||
|
@ -1,35 +1,51 @@
|
||||
package org.bigbluebutton.core2.message.handlers
|
||||
package org.bigbluebutton.core.apps.voice
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter }
|
||||
import org.bigbluebutton.core.models.Users2x
|
||||
import org.bigbluebutton.core2.message.senders.MsgBuilder
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting, MeetingActor, OutMsgRouter }
|
||||
|
||||
trait GetGlobalAudioPermissionReqMsgHdlr {
|
||||
this: MeetingActor =>
|
||||
|
||||
val outGW: OutMsgRouter
|
||||
|
||||
def handleGetGlobalAudioPermissionReqMsg(msg: GetGlobalAudioPermissionReqMsg) {
|
||||
var allowed = false
|
||||
def handleGetGlobalAudioPermissionReqMsg(msg: GetGlobalAudioPermissionReqMsg): Unit = {
|
||||
|
||||
for {
|
||||
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId)
|
||||
} yield {
|
||||
if (!user.userLeftFlag.left
|
||||
&& liveMeeting.props.meetingProp.intId == msg.body.meetingId
|
||||
&& liveMeeting.props.voiceProp.voiceConf == msg.body.voiceConf) {
|
||||
allowed = true
|
||||
}
|
||||
def broadcastEvent(
|
||||
meetingId: String,
|
||||
voiceConf: String,
|
||||
userId: String,
|
||||
sfuSessionId: String,
|
||||
allowed: Boolean
|
||||
): Unit = {
|
||||
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, userId)
|
||||
val envelope = BbbCoreEnvelope(GetGlobalAudioPermissionRespMsg.NAME, routing)
|
||||
val header = BbbClientMsgHeader(GetGlobalAudioPermissionRespMsg.NAME, meetingId, userId)
|
||||
val body = GetGlobalAudioPermissionRespMsgBody(
|
||||
meetingId,
|
||||
voiceConf,
|
||||
userId,
|
||||
sfuSessionId,
|
||||
allowed
|
||||
)
|
||||
val event = GetGlobalAudioPermissionRespMsg(header, body)
|
||||
val eventMsg = BbbCommonEnvCoreMsg(envelope, event)
|
||||
|
||||
outGW.send(eventMsg)
|
||||
}
|
||||
|
||||
val event = MsgBuilder.buildGetGlobalAudioPermissionRespMsg(
|
||||
val allowed = VoiceHdlrHelpers.isGlobalAudioSubscribeAllowed(
|
||||
liveMeeting,
|
||||
msg.body.meetingId,
|
||||
msg.body.userId,
|
||||
msg.body.voiceConf
|
||||
)
|
||||
|
||||
broadcastEvent(
|
||||
liveMeeting.props.meetingProp.intId,
|
||||
liveMeeting.props.voiceProp.voiceConf,
|
||||
msg.body.userId,
|
||||
msg.body.sfuSessionId,
|
||||
allowed
|
||||
)
|
||||
outGW.send(event)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,53 @@
|
||||
package org.bigbluebutton.core.apps.voice
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting, MeetingActor, OutMsgRouter }
|
||||
|
||||
trait GetMicrophonePermissionReqMsgHdlr {
|
||||
this: MeetingActor =>
|
||||
|
||||
val liveMeeting: LiveMeeting
|
||||
val outGW: OutMsgRouter
|
||||
|
||||
def handleGetMicrophonePermissionReqMsg(msg: GetMicrophonePermissionReqMsg): Unit = {
|
||||
|
||||
def broadcastEvent(
|
||||
meetingId: String,
|
||||
voiceConf: String,
|
||||
userId: String,
|
||||
sfuSessionId: String,
|
||||
allowed: Boolean
|
||||
): Unit = {
|
||||
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, userId)
|
||||
val envelope = BbbCoreEnvelope(GetMicrophonePermissionRespMsg.NAME, routing)
|
||||
val header = BbbClientMsgHeader(GetMicrophonePermissionRespMsg.NAME, meetingId, userId)
|
||||
val body = GetMicrophonePermissionRespMsgBody(
|
||||
meetingId,
|
||||
voiceConf,
|
||||
userId,
|
||||
sfuSessionId,
|
||||
allowed
|
||||
)
|
||||
val event = GetMicrophonePermissionRespMsg(header, body)
|
||||
val eventMsg = BbbCommonEnvCoreMsg(envelope, event)
|
||||
|
||||
outGW.send(eventMsg)
|
||||
}
|
||||
|
||||
val allowed = VoiceHdlrHelpers.isMicrophoneSharingAllowed(
|
||||
liveMeeting,
|
||||
msg.body.meetingId,
|
||||
msg.body.userId,
|
||||
msg.body.voiceConf,
|
||||
msg.body.callerIdNum
|
||||
)
|
||||
|
||||
broadcastEvent(
|
||||
liveMeeting.props.meetingProp.intId,
|
||||
liveMeeting.props.voiceProp.voiceConf,
|
||||
msg.body.userId,
|
||||
msg.body.sfuSessionId,
|
||||
allowed
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package org.bigbluebutton.core.apps.voice
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.models.{ Users2x, VoiceUsers }
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting }
|
||||
import org.bigbluebutton.LockSettingsUtil
|
||||
import org.bigbluebutton.SystemConfiguration
|
||||
|
||||
object VoiceHdlrHelpers extends SystemConfiguration {
|
||||
def isGlobalAudioSubscribeAllowed(
|
||||
liveMeeting: LiveMeeting,
|
||||
meetingId: String,
|
||||
userId: String,
|
||||
voiceConf: String
|
||||
): Boolean = {
|
||||
Users2x.findWithIntId(liveMeeting.users2x, userId) match {
|
||||
case Some(user) => (
|
||||
applyPermissionCheck &&
|
||||
liveMeeting.props.meetingProp.intId == meetingId &&
|
||||
liveMeeting.props.voiceProp.voiceConf == voiceConf
|
||||
)
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
def isMicrophoneSharingAllowed(
|
||||
liveMeeting: LiveMeeting,
|
||||
meetingId: String,
|
||||
userId: String,
|
||||
voiceConf: String,
|
||||
callerIdNum: String
|
||||
): Boolean = {
|
||||
Users2x.findWithIntId(liveMeeting.users2x, userId) match {
|
||||
case Some(user) => {
|
||||
val microphoneSharingLocked = LockSettingsUtil.isMicrophoneSharingLocked(
|
||||
user,
|
||||
liveMeeting
|
||||
)
|
||||
val isCallerBanned = VoiceUsers.isCallerBanned(
|
||||
callerIdNum,
|
||||
liveMeeting.voiceUsers
|
||||
)
|
||||
|
||||
(applyPermissionCheck &&
|
||||
!isCallerBanned &&
|
||||
!microphoneSharingLocked &&
|
||||
liveMeeting.props.meetingProp.intId == meetingId &&
|
||||
liveMeeting.props.voiceProp.voiceConf == voiceConf)
|
||||
}
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
}
|
@ -23,7 +23,6 @@ object CameraHdlrHelpers extends SystemConfiguration with RightsManagementTrait
|
||||
(applyPermissionCheck &&
|
||||
!camBroadcastLocked &&
|
||||
!camCapReached &&
|
||||
!user.userLeftFlag.left &&
|
||||
streamId.startsWith(user.intId) &&
|
||||
liveMeeting.props.meetingProp.intId == meetingId)
|
||||
}
|
||||
@ -43,7 +42,6 @@ object CameraHdlrHelpers extends SystemConfiguration with RightsManagementTrait
|
||||
|
||||
(applyPermissionCheck &&
|
||||
!camSubscribeLocked &&
|
||||
!user.userLeftFlag.left &&
|
||||
liveMeeting.props.meetingProp.intId == meetingId)
|
||||
}
|
||||
case _ => false
|
||||
|
@ -5,9 +5,9 @@ import org.bigbluebutton.core.util.RandomStringGenerator
|
||||
|
||||
object GroupChatFactory {
|
||||
def genId(): String = System.currentTimeMillis() + "-" + RandomStringGenerator.randomAlphanumericString(8)
|
||||
def create(id: String, name: String, access: String, createdBy: GroupChatUser,
|
||||
def create(id: String, access: String, createdBy: GroupChatUser,
|
||||
users: Vector[GroupChatUser], msgs: Vector[GroupChatMessage]): GroupChat = {
|
||||
new GroupChat(id, name, access, createdBy, users, msgs)
|
||||
new GroupChat(id, access, createdBy, users, msgs)
|
||||
}
|
||||
|
||||
}
|
||||
@ -23,7 +23,7 @@ case class GroupChats(chats: collection.immutable.Map[String, GroupChat]) {
|
||||
def getAllGroupChatsInMeeting(): Vector[GroupChat] = chats.values.toVector
|
||||
}
|
||||
|
||||
case class GroupChat(id: String, name: String, access: String, createdBy: GroupChatUser,
|
||||
case class GroupChat(id: String, access: String, createdBy: GroupChatUser,
|
||||
users: Vector[GroupChatUser],
|
||||
msgs: Vector[GroupChatMessage]) {
|
||||
def findMsgWithId(id: String): Option[GroupChatMessage] = msgs.find(m => m.id == id)
|
||||
|
@ -3,6 +3,7 @@ package org.bigbluebutton.core.models
|
||||
import org.bigbluebutton.common2.domain.PageVO
|
||||
import org.bigbluebutton.core.models.PresentationInPod
|
||||
import org.bigbluebutton.core.util.RandomStringGenerator
|
||||
import org.bigbluebutton.common2.msgs.AnnotationVO
|
||||
|
||||
object PresentationPodFactory {
|
||||
private def genId(): String = System.currentTimeMillis() + "-" + RandomStringGenerator.randomAlphanumericString(8)
|
||||
@ -81,6 +82,9 @@ case class PresentationPod(id: String, currentPresenter: String,
|
||||
def getPresentation(presentationId: String): Option[PresentationInPod] =
|
||||
presentations.values find (p => p.id == presentationId)
|
||||
|
||||
def getPresentationsByFilename(filename: String): Iterable[PresentationInPod] =
|
||||
presentations.values filter (p => p.name.startsWith(filename))
|
||||
|
||||
def setCurrentPresentation(presId: String): Option[PresentationPod] = {
|
||||
var tempPod: PresentationPod = this
|
||||
presentations.values foreach (curPres => { // unset previous current presentation
|
||||
|
@ -215,6 +215,8 @@ class ReceivedJsonMsgHandlerActor(
|
||||
routeVoiceMsg[VoiceConfCallStateEvtMsg](envelope, jsonNode)
|
||||
case GetGlobalAudioPermissionReqMsg.NAME =>
|
||||
routeGenericMsg[GetGlobalAudioPermissionReqMsg](envelope, jsonNode)
|
||||
case GetMicrophonePermissionReqMsg.NAME =>
|
||||
routeGenericMsg[GetMicrophonePermissionReqMsg](envelope, jsonNode)
|
||||
|
||||
// Breakout rooms
|
||||
case BreakoutRoomsListMsg.NAME =>
|
||||
@ -304,6 +306,12 @@ class ReceivedJsonMsgHandlerActor(
|
||||
routeGenericMsg[PdfConversionInvalidErrorSysPubMsg](envelope, jsonNode)
|
||||
case AssignPresenterReqMsg.NAME =>
|
||||
routeGenericMsg[AssignPresenterReqMsg](envelope, jsonNode)
|
||||
case MakePresentationWithAnnotationDownloadReqMsg.NAME =>
|
||||
routeGenericMsg[MakePresentationWithAnnotationDownloadReqMsg](envelope, jsonNode)
|
||||
case ExportPresentationWithAnnotationReqMsg.NAME =>
|
||||
routeGenericMsg[ExportPresentationWithAnnotationReqMsg](envelope, jsonNode)
|
||||
case NewPresAnnFileAvailableMsg.NAME =>
|
||||
routeGenericMsg[NewPresAnnFileAvailableMsg](envelope, jsonNode)
|
||||
|
||||
// Presentation Pods
|
||||
case CreateNewPresentationPodPubMsg.NAME =>
|
||||
@ -384,7 +392,6 @@ class ReceivedJsonMsgHandlerActor(
|
||||
routeGenericMsg[UpdateExternalVideoPubMsg](envelope, jsonNode)
|
||||
case StopExternalVideoPubMsg.NAME =>
|
||||
routeGenericMsg[StopExternalVideoPubMsg](envelope, jsonNode)
|
||||
|
||||
case _ =>
|
||||
log.error("Cannot route envelope name " + envelope.name)
|
||||
// do nothing
|
||||
|
@ -31,4 +31,4 @@ trait AbstractPresentationRecordEvent extends RecordEvent {
|
||||
|
||||
object AbstractPresentationRecordEvent {
|
||||
protected final val POD_ID = "podId"
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.core.record.events
|
||||
|
||||
trait AbstractPresentationWithAnnotations extends RecordEvent {
|
||||
setModule("PRES-ANN")
|
||||
}
|
@ -45,4 +45,4 @@ object AbstractWhiteboardRecordEvent {
|
||||
protected final val PRESENTATION = "presentation"
|
||||
protected final val PAGE_NUM = "pageNumber"
|
||||
protected final val WHITEBOARD_ID = "whiteboardId"
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,65 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.core.record.events
|
||||
|
||||
import org.bigbluebutton.common2.domain.SimpleVoteOutVO
|
||||
import scala.collection.immutable.List
|
||||
import scala.collection.Map
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
import spray.json._
|
||||
import DefaultJsonProtocol._
|
||||
|
||||
class AddTldrawShapeWhiteboardRecordEvent extends AbstractWhiteboardRecordEvent {
|
||||
import AddTldrawShapeWhiteboardRecordEvent._
|
||||
|
||||
implicit object AnyJsonFormat extends JsonFormat[Any] {
|
||||
def write(x: Any) = x match {
|
||||
case n: Int => JsNumber(n)
|
||||
case s: String => JsString(s)
|
||||
case d: Double => JsNumber(d)
|
||||
case m: scala.collection.immutable.Map[String, _] => mapFormat[String, Any].write(m)
|
||||
case l: List[_] => listFormat[Any].write(l)
|
||||
case b: Boolean if b == true => JsTrue
|
||||
case b: Boolean if b == false => JsFalse
|
||||
}
|
||||
|
||||
def read(value: JsValue) = {}
|
||||
}
|
||||
|
||||
setEvent("AddTldrawShapeEvent")
|
||||
|
||||
def setUserId(id: String) {
|
||||
eventMap.put(USER_ID, id)
|
||||
}
|
||||
|
||||
def setAnnotationId(id: String) {
|
||||
eventMap.put(SHAPE_ID, id)
|
||||
}
|
||||
|
||||
def addAnnotation(annotation: scala.collection.immutable.Map[String, Any]) {
|
||||
eventMap.put(SHAPE_DATA, annotation.toJson.compactPrint)
|
||||
}
|
||||
}
|
||||
|
||||
object AddTldrawShapeWhiteboardRecordEvent {
|
||||
protected final val USER_ID = "userId"
|
||||
protected final val SHAPE_ID = "shapeId"
|
||||
protected final val SHAPE_DATA = "shapeData"
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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 DeleteTldrawShapeRecordEvent extends AbstractWhiteboardRecordEvent {
|
||||
import DeleteTldrawShapeRecordEvent._
|
||||
|
||||
setEvent("DeleteTldrawShapeEvent")
|
||||
|
||||
def setUserId(userId: String) {
|
||||
eventMap.put(USER_ID, userId)
|
||||
}
|
||||
|
||||
def setShapeId(shapeId: String) {
|
||||
eventMap.put(SHAPE_ID, shapeId)
|
||||
}
|
||||
}
|
||||
|
||||
object DeleteTldrawShapeRecordEvent {
|
||||
protected final val USER_ID = "userId"
|
||||
protected final val SHAPE_ID = "shapeId"
|
||||
}
|
@ -32,23 +32,28 @@ class ResizeAndMoveSlideRecordEvent extends AbstractPresentationRecordEvent {
|
||||
eventMap.put(ID, id)
|
||||
}
|
||||
|
||||
def setXCamera(xCamera: Double) {
|
||||
eventMap.put(X_CAMERA, xCamera.toString)
|
||||
def setXOffset(offset: Double) {
|
||||
eventMap.put(X_OFFSET, offset.toString)
|
||||
}
|
||||
|
||||
def setYCamera(yCamera: Double) {
|
||||
eventMap.put(Y_CAMERA, yCamera.toString)
|
||||
def setYOffset(offset: Double) {
|
||||
eventMap.put(Y_OFFSET, offset.toString)
|
||||
}
|
||||
|
||||
def setZoom(zoom: Double) {
|
||||
eventMap.put(ZOOM, zoom.toString)
|
||||
def setWidthRatio(ratio: Double) {
|
||||
eventMap.put(WIDTH_RATIO, ratio.toString)
|
||||
}
|
||||
|
||||
def setHeightRatio(ratio: Double) {
|
||||
eventMap.put(HEIGHT_RATIO, ratio.toString)
|
||||
}
|
||||
}
|
||||
|
||||
object ResizeAndMoveSlideRecordEvent {
|
||||
protected final val PRES_NAME = "presentationName"
|
||||
protected final val ID = "id"
|
||||
protected final val X_CAMERA = "xCamera"
|
||||
protected final val Y_CAMERA = "yCamera"
|
||||
protected final val ZOOM = "zoom"
|
||||
protected final val X_OFFSET = "xOffset"
|
||||
protected final val Y_OFFSET = "yOffset"
|
||||
protected final val WIDTH_RATIO = "widthRatio"
|
||||
protected final val HEIGHT_RATIO = "heightRatio"
|
||||
}
|
||||
|
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.core.record.events
|
||||
|
||||
import org.bigbluebutton.common2.msgs.{ AnnotationVO, ExportJob, StoredAnnotations, PresentationPageForExport }
|
||||
import org.bigbluebutton.common2.util.JsonUtil
|
||||
|
||||
class StoreAnnotationsInRedisPresAnnEvent extends AbstractPresentationWithAnnotations {
|
||||
import StoreAnnotationsInRedisPresAnnEvent._
|
||||
|
||||
setEvent("StoreAnnotationsInRedisPresAnnEvent")
|
||||
|
||||
def setJobId(jobId: String) {
|
||||
eventMap.put(JOB_ID, jobId)
|
||||
}
|
||||
|
||||
def setPresId(presId: String) {
|
||||
eventMap.put(PRES_ID, presId)
|
||||
}
|
||||
|
||||
def setPages(pages: List[PresentationPageForExport]) {
|
||||
eventMap.put(PAGES, JsonUtil.toJson(pages))
|
||||
}
|
||||
}
|
||||
|
||||
object StoreAnnotationsInRedisPresAnnEvent {
|
||||
protected final val JOB_ID = "jobId"
|
||||
protected final val PRES_ID = "presId"
|
||||
protected final val PAGES = "pages"
|
||||
}
|
||||
|
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.core.record.events
|
||||
|
||||
import org.bigbluebutton.common2.msgs.{ AnnotationVO, ExportJob, StoredAnnotations, PresentationPageForExport }
|
||||
import org.bigbluebutton.common2.util.JsonUtil
|
||||
|
||||
class StoreExportJobInRedisPresAnnEvent extends AbstractPresentationWithAnnotations {
|
||||
import StoreExportJobInRedisPresAnnEvent._
|
||||
|
||||
setEvent("StoreExportJobInRedisPresAnnEvent")
|
||||
|
||||
def setJobId(jobId: String) {
|
||||
eventMap.put(JOB_ID, jobId)
|
||||
}
|
||||
|
||||
def setJobType(jobType: String) {
|
||||
eventMap.put(JOB_TYPE, jobType)
|
||||
}
|
||||
|
||||
def setFilename(filename: String) {
|
||||
eventMap.put(FILENAME, filename)
|
||||
}
|
||||
|
||||
def setPresId(presId: String) {
|
||||
eventMap.put(PRES_ID, presId)
|
||||
}
|
||||
|
||||
def setPresLocation(presLocation: String) {
|
||||
eventMap.put(PRES_LOCATION, presLocation)
|
||||
}
|
||||
|
||||
def setAllPages(allPages: String) {
|
||||
eventMap.put(ALL_PAGES, allPages)
|
||||
}
|
||||
|
||||
def setPages(pages: List[Int]) {
|
||||
eventMap.put(PAGES, JsonUtil.toJson(pages))
|
||||
}
|
||||
|
||||
def setParentMeetingId(parentMeetingId: String) {
|
||||
eventMap.put(PARENT_MEETING_ID, parentMeetingId)
|
||||
}
|
||||
|
||||
def setPresentationUploadToken(presentationUploadToken: String) {
|
||||
eventMap.put(PRESENTATION_UPLOAD_TOKEN, presentationUploadToken)
|
||||
}
|
||||
}
|
||||
|
||||
object StoreExportJobInRedisPresAnnEvent {
|
||||
protected final val JOB_ID = "jobId"
|
||||
protected final val JOB_TYPE = "jobType"
|
||||
protected final val FILENAME = "filename"
|
||||
protected final val PRES_ID = "presId"
|
||||
protected final val PRES_LOCATION = "presLocation"
|
||||
protected final val ALL_PAGES = "allPages"
|
||||
protected final val PAGES = "pages"
|
||||
protected final val PARENT_MEETING_ID = "parentMeetingId"
|
||||
protected final val PRESENTATION_UPLOAD_TOKEN = "presentationUploadToken"
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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 TldrawCameraChangedRecordEvent extends AbstractPresentationRecordEvent {
|
||||
import TldrawCameraChangedRecordEvent._
|
||||
|
||||
setEvent("TldrawCameraChangedEvent")
|
||||
|
||||
def setPresentationName(name: String) {
|
||||
eventMap.put(PRES_NAME, name)
|
||||
}
|
||||
|
||||
def setId(id: String) {
|
||||
eventMap.put(ID, id)
|
||||
}
|
||||
|
||||
def setXCamera(xCamera: Double) {
|
||||
eventMap.put(X_CAMERA, xCamera.toString)
|
||||
}
|
||||
|
||||
def setYCamera(yCamera: Double) {
|
||||
eventMap.put(Y_CAMERA, yCamera.toString)
|
||||
}
|
||||
|
||||
def setZoom(zoom: Double) {
|
||||
eventMap.put(ZOOM, zoom.toString)
|
||||
}
|
||||
}
|
||||
|
||||
object TldrawCameraChangedRecordEvent {
|
||||
protected final val PRES_NAME = "presentationName"
|
||||
protected final val ID = "id"
|
||||
protected final val X_CAMERA = "xCamera"
|
||||
protected final val Y_CAMERA = "yCamera"
|
||||
protected final val ZOOM = "zoom"
|
||||
}
|
@ -80,6 +80,7 @@ class MeetingActor(
|
||||
with MuteMeetingCmdMsgHdlr
|
||||
with IsMeetingMutedReqMsgHdlr
|
||||
with GetGlobalAudioPermissionReqMsgHdlr
|
||||
with GetMicrophonePermissionReqMsgHdlr
|
||||
with GetScreenBroadcastPermissionReqMsgHdlr
|
||||
with GetScreenSubscribePermissionReqMsgHdlr
|
||||
|
||||
@ -467,6 +468,8 @@ class MeetingActor(
|
||||
handleUserStatusVoiceConfEvtMsg(m)
|
||||
case m: GetGlobalAudioPermissionReqMsg =>
|
||||
handleGetGlobalAudioPermissionReqMsg(m)
|
||||
case m: GetMicrophonePermissionReqMsg =>
|
||||
handleGetMicrophonePermissionReqMsg(m)
|
||||
|
||||
// Layout
|
||||
case m: GetCurrentLayoutReqMsg => handleGetCurrentLayoutReqMsg(m)
|
||||
@ -497,6 +500,9 @@ class MeetingActor(
|
||||
// Presentation
|
||||
case m: PreuploadedPresentationsSysPubMsg => presentationApp2x.handle(m, liveMeeting, msgBus)
|
||||
case m: AssignPresenterReqMsg => state = handlePresenterChange(m, state)
|
||||
case m: MakePresentationWithAnnotationDownloadReqMsg => presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
case m: ExportPresentationWithAnnotationReqMsg => presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
case m: NewPresAnnFileAvailableMsg => log.info("***** New PDF with annotations available.")
|
||||
|
||||
// Presentation Pods
|
||||
case m: CreateNewPresentationPodPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
|
@ -110,6 +110,11 @@ class AnalyticsActor(val includeChat: Boolean) extends Actor with ActorLogging {
|
||||
case m: SetPresentationDownloadableEvtMsg => logMessage(msg)
|
||||
//case m: PresentationPageConvertedSysMsg => logMessage(msg)
|
||||
//case m: PresentationPageConvertedEventMsg => logMessage(msg)
|
||||
// case m: StoreAnnotationsInRedisSysMsg => logMessage(msg)
|
||||
// case m: StoreExportJobInRedisSysMsg => logMessage(msg)
|
||||
case m: MakePresentationWithAnnotationDownloadReqMsg => logMessage(msg)
|
||||
case m: ExportPresentationWithAnnotationReqMsg => logMessage(msg)
|
||||
case m: NewPresAnnFileAvailableMsg => logMessage(msg)
|
||||
case m: PresentationPageConversionStartedSysMsg => logMessage(msg)
|
||||
case m: PresentationConversionEndedSysMsg => logMessage(msg)
|
||||
case m: PresentationConversionRequestReceivedSysMsg => logMessage(msg)
|
||||
|
@ -347,16 +347,6 @@ object MsgBuilder {
|
||||
BbbCommonEnvCoreMsg(envelope, event)
|
||||
}
|
||||
|
||||
def buildStopMeetingTranscodersSysCmdMsg(meetingId: String): BbbCommonEnvCoreMsg = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
|
||||
val envelope = BbbCoreEnvelope(StopMeetingTranscodersSysCmdMsg.NAME, routing)
|
||||
val body = StopMeetingTranscodersSysCmdMsgBody()
|
||||
val header = BbbCoreHeaderWithMeetingId(StopMeetingTranscodersSysCmdMsg.NAME, meetingId)
|
||||
val event = StopMeetingTranscodersSysCmdMsg(header, body)
|
||||
|
||||
BbbCommonEnvCoreMsg(envelope, event)
|
||||
}
|
||||
|
||||
def buildRecordingChapterBreakSysMsg(meetingId: String, timestamp: Long): BbbCommonEnvCoreMsg = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
|
||||
val envelope = BbbCoreEnvelope(RecordingChapterBreakSysMsg.NAME, routing)
|
||||
@ -461,23 +451,6 @@ object MsgBuilder {
|
||||
BbbCommonEnvCoreMsg(envelope, event)
|
||||
}
|
||||
|
||||
def buildGetGlobalAudioPermissionRespMsg(
|
||||
meetingId: String,
|
||||
voiceConf: String,
|
||||
userId: String,
|
||||
sfuSessionId: String,
|
||||
allowed: Boolean
|
||||
): BbbCommonEnvCoreMsg = {
|
||||
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, userId)
|
||||
val envelope = BbbCoreEnvelope(GetGlobalAudioPermissionRespMsg.NAME, routing)
|
||||
val header = BbbClientMsgHeader(GetGlobalAudioPermissionRespMsg.NAME, meetingId, userId)
|
||||
|
||||
val body = GetGlobalAudioPermissionRespMsgBody(meetingId, voiceConf, userId, sfuSessionId, allowed)
|
||||
val event = GetGlobalAudioPermissionRespMsg(header, body)
|
||||
|
||||
BbbCommonEnvCoreMsg(envelope, event)
|
||||
}
|
||||
|
||||
def buildMeetingTimeRemainingUpdateEvtMsg(meetingId: String, timeLeftInSec: Long, timeUpdatedInMinutes: Int = 0): BbbCommonEnvCoreMsg = {
|
||||
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, "not-used")
|
||||
val envelope = BbbCoreEnvelope(MeetingTimeRemainingUpdateEvtMsg.NAME, routing)
|
||||
|
@ -0,0 +1,88 @@
|
||||
package org.bigbluebutton.endpoint.redis
|
||||
|
||||
import scala.collection.immutable.StringOps
|
||||
import scala.collection.JavaConverters._
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.common2.redis.{ RedisConfig, RedisStorageProvider }
|
||||
import org.bigbluebutton.core.record.events.{ AbstractPresentationWithAnnotations, StoreAnnotationsInRedisPresAnnEvent, StoreExportJobInRedisPresAnnEvent }
|
||||
import akka.actor.Actor
|
||||
import akka.actor.ActorLogging
|
||||
import akka.actor.ActorSystem
|
||||
import akka.actor.Props
|
||||
import org.bigbluebutton.service.HealthzService
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent._
|
||||
import ExecutionContext.Implicits.global
|
||||
|
||||
object ExportAnnotationsActor {
|
||||
def props(
|
||||
system: ActorSystem,
|
||||
redisConfig: RedisConfig,
|
||||
healthzService: HealthzService
|
||||
): Props =
|
||||
Props(
|
||||
classOf[ExportAnnotationsActor],
|
||||
system,
|
||||
redisConfig,
|
||||
healthzService
|
||||
)
|
||||
}
|
||||
|
||||
class ExportAnnotationsActor(
|
||||
system: ActorSystem,
|
||||
redisConfig: RedisConfig,
|
||||
healthzService: HealthzService
|
||||
)
|
||||
extends RedisStorageProvider(
|
||||
system,
|
||||
"BbbAppsAkkaRecorder",
|
||||
redisConfig
|
||||
) with Actor with ActorLogging {
|
||||
|
||||
private def storePresentationAnnotations(session: String, message: java.util.Map[java.lang.String, java.lang.String], messageType: String): Unit = {
|
||||
redis.storePresentationAnnotations(session, message, messageType)
|
||||
}
|
||||
|
||||
def receive = {
|
||||
//=============================
|
||||
// 2x messages
|
||||
case msg: BbbCommonEnvCoreMsg => handleBbbCommonEnvCoreMsg(msg)
|
||||
case _ => // do nothing
|
||||
}
|
||||
|
||||
private def handleBbbCommonEnvCoreMsg(msg: BbbCommonEnvCoreMsg): Unit = {
|
||||
msg.core match {
|
||||
case m: StoreAnnotationsInRedisSysMsg => handleStoreAnnotationsInRedisSysMsg(m)
|
||||
case m: StoreExportJobInRedisSysMsg => handleStoreExportJobInRedisSysMsg(m)
|
||||
|
||||
case _ => // message not to be stored.
|
||||
}
|
||||
}
|
||||
|
||||
private def handleStoreAnnotationsInRedisSysMsg(msg: StoreAnnotationsInRedisSysMsg) {
|
||||
val ev = new StoreAnnotationsInRedisPresAnnEvent()
|
||||
|
||||
ev.setJobId(msg.body.annotations.jobId)
|
||||
ev.setPresId(msg.body.annotations.presId)
|
||||
ev.setPages(msg.body.annotations.pages)
|
||||
|
||||
storePresentationAnnotations(msg.header.meetingId, ev.toMap.asJava, "PresAnn")
|
||||
}
|
||||
|
||||
private def handleStoreExportJobInRedisSysMsg(msg: StoreExportJobInRedisSysMsg) {
|
||||
val ev = new StoreExportJobInRedisPresAnnEvent()
|
||||
|
||||
ev.setJobId(msg.body.exportJob.jobId)
|
||||
ev.setJobType(msg.body.exportJob.jobType)
|
||||
ev.setFilename(msg.body.exportJob.filename)
|
||||
ev.setPresId(msg.body.exportJob.presId)
|
||||
ev.setPresLocation(msg.body.exportJob.presLocation)
|
||||
ev.setAllPages(msg.body.exportJob.allPages.toString)
|
||||
ev.setPages(msg.body.exportJob.pages)
|
||||
ev.setParentMeetingId(msg.body.exportJob.parentMeetingId)
|
||||
ev.setPresentationUploadToken(msg.body.exportJob.presUploadToken)
|
||||
|
||||
storePresentationAnnotations(msg.header.meetingId, ev.toMap.asJava, "ExportJob")
|
||||
}
|
||||
}
|
@ -184,7 +184,7 @@ class RedisRecorderActor(
|
||||
}
|
||||
|
||||
private def handleResizeAndMovePageEvtMsg(msg: ResizeAndMovePageEvtMsg) {
|
||||
val ev = new ResizeAndMoveSlideRecordEvent()
|
||||
val ev = new TldrawCameraChangedRecordEvent()
|
||||
ev.setMeetingId(msg.header.meetingId)
|
||||
ev.setPodId(msg.body.podId)
|
||||
ev.setPresentationName(msg.body.presentationId)
|
||||
@ -280,7 +280,7 @@ class RedisRecorderActor(
|
||||
|
||||
private def handleSendWhiteboardAnnotationsEvtMsg(msg: SendWhiteboardAnnotationsEvtMsg) {
|
||||
msg.body.annotations.foreach(annotation => {
|
||||
val ev = new AddShapeWhiteboardRecordEvent()
|
||||
val ev = new AddTldrawShapeWhiteboardRecordEvent()
|
||||
ev.setMeetingId(msg.header.meetingId)
|
||||
ev.setPresentation(getPresentationId(annotation.wbId))
|
||||
ev.setPageNumber(getPageNum(annotation.wbId))
|
||||
@ -320,7 +320,7 @@ class RedisRecorderActor(
|
||||
|
||||
private def handleDeleteWhiteboardAnnotationsEvtMsg(msg: DeleteWhiteboardAnnotationsEvtMsg) {
|
||||
msg.body.annotationsIds.foreach(annotationId => {
|
||||
val ev = new UndoAnnotationRecordEvent()
|
||||
val ev = new DeleteTldrawShapeRecordEvent()
|
||||
ev.setMeetingId(msg.header.meetingId)
|
||||
ev.setPresentation(getPresentationId(msg.body.whiteboardId))
|
||||
ev.setPageNumber(getPageNum(msg.body.whiteboardId))
|
||||
|
@ -3,6 +3,7 @@ Description=BigBlueButton Apps (Akka)
|
||||
Requires=network.target
|
||||
Wants=redis-server.service
|
||||
After=redis-server.service
|
||||
PartOf=bigbluebutton.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
@ -22,5 +23,4 @@ PermissionsStartOnly=true
|
||||
LimitNOFILE=1024
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
WantedBy=multi-user.target bigbluebutton.target
|
||||
|
@ -7,10 +7,9 @@ class GroupsChatTests extends UnitSpec {
|
||||
|
||||
"A GroupChat" should "be able to add and remove user" in {
|
||||
val gcId = "gc-id"
|
||||
val chatName = "Public"
|
||||
val userId = "uid-1"
|
||||
val createBy = GroupChatUser("groupId")
|
||||
val gc = GroupChatFactory.create(gcId, chatName, GroupChatAccess.PUBLIC, createBy, Vector.empty, Vector.empty)
|
||||
val gc = GroupChatFactory.create(gcId, GroupChatAccess.PUBLIC, createBy, Vector.empty, Vector.empty)
|
||||
val user = GroupChatUser(userId)
|
||||
val gc2 = gc.add(user)
|
||||
assert(gc2.users.size == 1)
|
||||
@ -26,8 +25,7 @@ class GroupsChatTests extends UnitSpec {
|
||||
"A GroupChat" should "be able to add, update, and remove msg" in {
|
||||
val createBy = GroupChatUser("groupId")
|
||||
val gcId = "gc-id"
|
||||
val chatName = "Public"
|
||||
val gc = GroupChatFactory.create(gcId, chatName, GroupChatAccess.PUBLIC, createBy, Vector.empty, Vector.empty)
|
||||
val gc = GroupChatFactory.create(gcId, GroupChatAccess.PUBLIC, createBy, Vector.empty, Vector.empty)
|
||||
val msgId1 = "msgid-1"
|
||||
val ts = System.currentTimeMillis()
|
||||
val hello = "Hello World!"
|
||||
|
@ -18,7 +18,7 @@ val compileSettings = Seq(
|
||||
"-Xlint",
|
||||
"-Ywarn-dead-code",
|
||||
"-language:_",
|
||||
"-target:jvm-1.8",
|
||||
"-target:jvm-1.11",
|
||||
"-encoding", "UTF-8"
|
||||
),
|
||||
javacOptions ++= List(
|
||||
|
@ -46,8 +46,8 @@ object Dependencies {
|
||||
|
||||
val apacheLang = "org.apache.commons" % "commons-lang3" % Versions.lang
|
||||
|
||||
val bbbCommons = "org.bigbluebutton" % "bbb-common-message_2.13" % Versions.bbbCommons excludeAll (
|
||||
ExclusionRule(organization = "org.red5"))
|
||||
val bbbCommons = "org.bigbluebutton" % "bbb-common-message_2.13" % Versions.bbbCommons
|
||||
|
||||
val bbbFseslClient = "org.bigbluebutton" % "bbb-fsesl-client" % Versions.bbbFsesl
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ Description=BigBlueButton FS-ESL (Akka)
|
||||
Requires=network.target
|
||||
Wants=redis-server.service
|
||||
After=redis-server.service
|
||||
PartOf= bigbluebutton.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
@ -22,5 +23,5 @@ PermissionsStartOnly=true
|
||||
LimitNOFILE=1024
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
WantedBy=multi-user.target bigbluebutton.target
|
||||
|
||||
|
@ -21,6 +21,8 @@ package org.bigbluebutton.common2.redis;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import io.lettuce.core.api.sync.BaseRedisCommands;
|
||||
import org.slf4j.Logger;
|
||||
@ -112,6 +114,33 @@ public class RedisStorageService extends RedisAwareCommunicator {
|
||||
commands.exec();
|
||||
}
|
||||
|
||||
public void storePresentationAnnotations(String meetingId, Map<String, String> event, String msgType) {
|
||||
RedisCommands<String, String> commands = connection.sync();
|
||||
|
||||
commands.multi();
|
||||
|
||||
switch (msgType) {
|
||||
case "PresAnn": {
|
||||
commands.hmset(event.get("jobId"), event);
|
||||
break;
|
||||
}
|
||||
|
||||
case "ExportJob": {
|
||||
Gson gson = new Gson();
|
||||
String exportJobAsJson = gson.toJson(event);
|
||||
commands.rpush("exportJobs", exportJobAsJson.toString());
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
log.error("Attempted to store PresentationAnnotations message of type: {}", clientName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
commands.exec();
|
||||
}
|
||||
|
||||
// @fixme: not used anywhere
|
||||
public void removeMeeting(String meetingId) {
|
||||
RedisCommands<String, String> commands = connection.sync();
|
||||
|
@ -49,7 +49,6 @@ public class MessageSender extends RedisAwareCommunicator {
|
||||
connectionPool = ConnectionPoolSupport.createGenericObjectPool(() -> redisClient.connectPubSub(),
|
||||
createPoolingConfig());
|
||||
|
||||
log.info("Redis org.bigbluebutton.red5.pubsub.message publisher starting!");
|
||||
try {
|
||||
sendMessage = true;
|
||||
|
||||
@ -84,7 +83,7 @@ public class MessageSender extends RedisAwareCommunicator {
|
||||
RedisAsyncCommands<String, String> async = connection.async();
|
||||
RedisFuture<Long> future = async.publish(channel, message);
|
||||
} catch (Exception e) {
|
||||
log.warn("Cannot publish the org.bigbluebutton.red5.pubsub.message to redis", e);
|
||||
log.warn("Cannot publish the message to redis", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -49,8 +49,6 @@ case class UsersProp(
|
||||
|
||||
case class MetadataProp(metadata: collection.immutable.Map[String, String])
|
||||
|
||||
case class ScreenshareProps(screenshareConf: String, red5ScreenshareIp: String, red5ScreenshareApp: String)
|
||||
|
||||
case class LockSettingsProps(
|
||||
disableCam: Boolean,
|
||||
disableMic: Boolean,
|
||||
|
@ -8,7 +8,7 @@ object GroupChatAccess {
|
||||
case class GroupChatUser(id: String, name: String = "", role: String = "VIEWER")
|
||||
case class GroupChatMsgFromUser(correlationId: String, sender: GroupChatUser, chatEmphasizedText: Boolean = false, message: String)
|
||||
case class GroupChatMsgToUser(id: String, timestamp: Long, correlationId: String, sender: GroupChatUser, chatEmphasizedText: Boolean = false, message: String)
|
||||
case class GroupChatInfo(id: String, name: String, access: String, createdBy: GroupChatUser, users: Vector[GroupChatUser])
|
||||
case class GroupChatInfo(id: String, access: String, createdBy: GroupChatUser, users: Vector[GroupChatUser])
|
||||
|
||||
object OpenGroupChatWindowReqMsg { val NAME = "OpenGroupChatWindowReqMsg" }
|
||||
case class OpenGroupChatWindowReqMsg(header: BbbClientMsgHeader, body: OpenGroupChatWindowReqMsgBody) extends StandardMsg
|
||||
@ -36,14 +36,14 @@ case class GetGroupChatMsgsRespMsgBody(chatId: String, msgs: Vector[GroupChatMsg
|
||||
|
||||
object CreateGroupChatReqMsg { val NAME = "CreateGroupChatReqMsg" }
|
||||
case class CreateGroupChatReqMsg(header: BbbClientMsgHeader, body: CreateGroupChatReqMsgBody) extends StandardMsg
|
||||
case class CreateGroupChatReqMsgBody(correlationId: String, name: String, access: String,
|
||||
case class CreateGroupChatReqMsgBody(correlationId: String, access: String,
|
||||
users: Vector[String], msg: Vector[GroupChatMsgFromUser])
|
||||
|
||||
object GroupChatCreatedEvtMsg { val NAME = "GroupChatCreatedEvtMsg" }
|
||||
case class GroupChatCreatedEvtMsg(header: BbbClientMsgHeader, body: GroupChatCreatedEvtMsgBody) extends BbbCoreMsg
|
||||
case class GroupChatCreatedEvtMsgBody(correlationId: String, chatId: String, createdBy: GroupChatUser,
|
||||
name: String, access: String,
|
||||
users: Vector[GroupChatUser], msg: Vector[GroupChatMsgToUser])
|
||||
access: String,
|
||||
users: Vector[GroupChatUser], msg: Vector[GroupChatMsgToUser])
|
||||
|
||||
object DestroyGroupChatReqMsg { val NAME = "DestroyGroupChatReqMsg" }
|
||||
case class DestroyGroupChatReqMsg(header: BbbClientMsgHeader, body: DestroyGroupChatReqMsgBody) extends StandardMsg
|
||||
|
@ -3,13 +3,25 @@ package org.bigbluebutton.common2.msgs
|
||||
import org.bigbluebutton.common2.domain.PresentationVO
|
||||
|
||||
// ------------ client to akka-apps ------------
|
||||
|
||||
// ------------ client to akka-apps ------------
|
||||
|
||||
// ------------ bbb-common-web to akka-apps ------------
|
||||
object PreuploadedPresentationsSysPubMsg { val NAME = "PreuploadedPresentationsSysPubMsg" }
|
||||
case class PreuploadedPresentationsSysPubMsg(header: BbbClientMsgHeader, body: PreuploadedPresentationsSysPubMsgBody) extends StandardMsg
|
||||
case class PreuploadedPresentationsSysPubMsgBody(presentations: Vector[PresentationVO])
|
||||
|
||||
object MakePresentationWithAnnotationDownloadReqMsg { val NAME = "MakePresentationWithAnnotationDownloadReqMsg" }
|
||||
case class MakePresentationWithAnnotationDownloadReqMsg(header: BbbClientMsgHeader, body: MakePresentationWithAnnotationDownloadReqMsgBody) extends StandardMsg
|
||||
case class MakePresentationWithAnnotationDownloadReqMsgBody(presId: String, allPages: Boolean, pages: List[Int])
|
||||
|
||||
object ExportPresentationWithAnnotationReqMsg { val NAME = "ExportPresentationWithAnnotationReqMsg" }
|
||||
case class ExportPresentationWithAnnotationReqMsg(header: BbbClientMsgHeader, body: ExportPresentationWithAnnotationReqMsgBody) extends StandardMsg
|
||||
case class ExportPresentationWithAnnotationReqMsgBody(parentMeetingId: String, allPages: Boolean)
|
||||
|
||||
object NewPresAnnFileAvailableMsg { val NAME = "NewPresAnnFileAvailableMsg" }
|
||||
case class NewPresAnnFileAvailableMsg(header: BbbClientMsgHeader, body: NewPresAnnFileAvailableMsgBody) extends StandardMsg
|
||||
case class NewPresAnnFileAvailableMsgBody(fileURI: String)
|
||||
|
||||
// ------------ bbb-common-web to akka-apps ------------
|
||||
|
||||
// ------------ akka-apps to client ------------
|
||||
|
@ -1,43 +0,0 @@
|
||||
package org.bigbluebutton.common2.msgs
|
||||
|
||||
/* In Messages */
|
||||
object StartProbingSysReqMsg { val NAME = "StartProbingSysReqMsg" }
|
||||
case class StartProbingSysReqMsg(header: BbbCoreHeaderWithMeetingId, body: StartProbingSysReqMsgBody) extends BbbCoreMsg
|
||||
case class StartProbingSysReqMsgBody(transcoderId: String, params: Map[String, String])
|
||||
|
||||
object StartTranscoderSysReqMsg { val NAME = "StartTranscoderSysReqMsg" }
|
||||
case class StartTranscoderSysReqMsg(header: BbbCoreHeaderWithMeetingId, body: StartTranscoderSysReqMsgBody) extends BbbCoreMsg
|
||||
case class StartTranscoderSysReqMsgBody(transcoderId: String, params: Map[String, String])
|
||||
|
||||
object StopTranscoderSysReqMsg { val NAME = "StopTranscoderSysReqMsg" }
|
||||
case class StopTranscoderSysReqMsg(header: BbbCoreHeaderWithMeetingId, body: StopTranscoderSysReqMsgBody) extends BbbCoreMsg
|
||||
case class StopTranscoderSysReqMsgBody(transcoderId: String)
|
||||
|
||||
object UpdateTranscoderSysReqMsg { val NAME = "UpdateTranscoderSysReqMsg" }
|
||||
case class UpdateTranscoderSysReqMsg(header: BbbCoreHeaderWithMeetingId, body: UpdateTranscoderSysReqMsgBody) extends BbbCoreMsg
|
||||
case class UpdateTranscoderSysReqMsgBody(transcoderId: String, params: Map[String, String])
|
||||
|
||||
object TranscoderStatusUpdateSysCmdMsg { val NAME = "TranscoderStatusUpdateSysCmdMsg" }
|
||||
case class TranscoderStatusUpdateSysCmdMsg(header: BbbCoreHeaderWithMeetingId, body: TranscoderStatusUpdateSysCmdMsgBody) extends BbbCoreMsg
|
||||
case class TranscoderStatusUpdateSysCmdMsgBody(transcoderId: String, params: Map[String, String])
|
||||
|
||||
object StopMeetingTranscodersSysCmdMsg { val NAME = "StopMeetingTranscodersSysCmdMsg" }
|
||||
case class StopMeetingTranscodersSysCmdMsg(header: BbbCoreHeaderWithMeetingId, body: StopMeetingTranscodersSysCmdMsgBody) extends BbbCoreMsg
|
||||
case class StopMeetingTranscodersSysCmdMsgBody()
|
||||
|
||||
/* Out Messages */
|
||||
object StartProbingSysRespMsg { val NAME = "StartProbingSysRespMsg" }
|
||||
case class StartProbingSysRespMsg(header: BbbCoreHeaderWithMeetingId, body: StartProbingSysRespMsgBody) extends BbbCoreMsg
|
||||
case class StartProbingSysRespMsgBody(transcoderId: String, params: Map[String, String])
|
||||
|
||||
object StartTranscoderSysRespMsg { val NAME = "StartTranscoderSysRespMsg" }
|
||||
case class StartTranscoderSysRespMsg(header: BbbCoreHeaderWithMeetingId, body: StartTranscoderSysRespMsgBody) extends BbbCoreMsg
|
||||
case class StartTranscoderSysRespMsgBody(transcoderId: String, params: Map[String, String])
|
||||
|
||||
object StopTranscoderSysRespMsg { val NAME = "StopTranscoderSysRespMsg" }
|
||||
case class StopTranscoderSysRespMsg(header: BbbCoreHeaderWithMeetingId, body: StopTranscoderSysRespMsgBody) extends BbbCoreMsg
|
||||
case class StopTranscoderSysRespMsgBody(transcoderId: String)
|
||||
|
||||
object UpdateTranscoderSysRespMsg { val NAME = "UpdateTranscoderSysRespMsg" }
|
||||
case class UpdateTranscoderSysRespMsg(header: BbbCoreHeaderWithMeetingId, body: UpdateTranscoderSysRespMsgBody) extends BbbCoreMsg
|
||||
case class UpdateTranscoderSysRespMsgBody(transcoderId: String, params: Map[String, String])
|
@ -310,7 +310,7 @@ case class UserJoinMeetingAfterReconnectReqMsg(header: BbbClientMsgHeader, body:
|
||||
case class UserJoinMeetingAfterReconnectReqMsgBody(userId: String, authToken: String, clientType: String)
|
||||
|
||||
/**
|
||||
* Sent from bbb-apps when user disconnects from Red5.
|
||||
* Sent from client to bbb-akka to notify that a user is leaving
|
||||
*/
|
||||
object UserLeaveReqMsg { val NAME = "UserLeaveReqMsg" }
|
||||
case class UserLeaveReqMsg(header: BbbClientMsgHeader, body: UserLeaveReqMsgBody) extends StandardMsg
|
||||
|
@ -561,3 +561,40 @@ case class GetGlobalAudioPermissionRespMsgBody(
|
||||
sfuSessionId: String,
|
||||
allowed: Boolean
|
||||
)
|
||||
|
||||
/* Sent by bbb-webrtc-sfu to ask permission for a new microphone/full audio
|
||||
* connection
|
||||
* - callerIdNum: the session's callerId as assembled by the requester
|
||||
* - sfuSessionId: the UUID for this request's session in bbb-webrtc-sfu.
|
||||
* Used for response matching.
|
||||
*/
|
||||
object GetMicrophonePermissionReqMsg { val NAME = "GetMicrophonePermissionReqMsg" }
|
||||
case class GetMicrophonePermissionReqMsg(
|
||||
header: BbbClientMsgHeader,
|
||||
body: GetMicrophonePermissionReqMsgBody
|
||||
) extends StandardMsg
|
||||
case class GetMicrophonePermissionReqMsgBody(
|
||||
meetingId: String,
|
||||
voiceConf: String,
|
||||
userId: String,
|
||||
callerIdNum: String,
|
||||
sfuSessionId: String
|
||||
)
|
||||
|
||||
/* Sent to bbb-webrtc-sfu as a response to GetMicrophonePermissionReqMsg
|
||||
* - sfuSessionId: the UUID for this request's session in bbb-webrtc-sfu.
|
||||
* Used for response matching.
|
||||
* - allowed: whether session creation should be allowed.
|
||||
*/
|
||||
object GetMicrophonePermissionRespMsg { val NAME = "GetMicrophonePermissionRespMsg" }
|
||||
case class GetMicrophonePermissionRespMsg(
|
||||
header: BbbClientMsgHeader,
|
||||
body: GetMicrophonePermissionRespMsgBody
|
||||
) extends StandardMsg
|
||||
case class GetMicrophonePermissionRespMsgBody(
|
||||
meetingId: String,
|
||||
voiceConf: String,
|
||||
userId: String,
|
||||
sfuSessionId: String,
|
||||
allowed: Boolean
|
||||
)
|
||||
|
@ -3,6 +3,32 @@ package org.bigbluebutton.common2.msgs
|
||||
case class AnnotationVO(id: String, annotationInfo: scala.collection.immutable.Map[String, Any],
|
||||
wbId: String, userId: String)
|
||||
|
||||
case class PresentationPageForExport(
|
||||
page: Int,
|
||||
xCamera: Double,
|
||||
yCamera: Double,
|
||||
zoom: Double,
|
||||
annotations: Array[AnnotationVO],
|
||||
)
|
||||
|
||||
case class StoredAnnotations(
|
||||
jobId: String,
|
||||
presId: String,
|
||||
pages: List[PresentationPageForExport],
|
||||
)
|
||||
|
||||
case class ExportJob(
|
||||
jobId: String,
|
||||
jobType: String,
|
||||
filename: String,
|
||||
presId: String,
|
||||
presLocation: String,
|
||||
allPages: Boolean,
|
||||
pages: List[Int],
|
||||
parentMeetingId: String,
|
||||
presUploadToken: String,
|
||||
)
|
||||
|
||||
// ------------ client to akka-apps ------------
|
||||
object ClientToServerLatencyTracerMsg { val NAME = "ClientToServerLatencyTracerMsg" }
|
||||
case class ClientToServerLatencyTracerMsg(header: BbbClientMsgHeader, body: ClientToServerLatencyTracerMsgBody) extends StandardMsg
|
||||
@ -65,4 +91,12 @@ case class SendWhiteboardAnnotationsEvtMsgBody(whiteboardId: String, annotations
|
||||
object DeleteWhiteboardAnnotationsEvtMsg { val NAME = "DeleteWhiteboardAnnotationsEvtMsg" }
|
||||
case class DeleteWhiteboardAnnotationsEvtMsg(header: BbbClientMsgHeader, body: DeleteWhiteboardAnnotationsEvtMsgBody) extends BbbCoreMsg
|
||||
case class DeleteWhiteboardAnnotationsEvtMsgBody(whiteboardId: String, annotationsIds: Array[String])
|
||||
|
||||
// ------------ akka-apps to client ------------
|
||||
object StoreAnnotationsInRedisSysMsg { val NAME = "StoreAnnotationsInRedisSysMsg" }
|
||||
case class StoreAnnotationsInRedisSysMsg(header: BbbCoreHeaderWithMeetingId, body: StoreAnnotationsInRedisSysMsgBody) extends BbbCoreMsg
|
||||
case class StoreAnnotationsInRedisSysMsgBody(annotations: StoredAnnotations)
|
||||
|
||||
object StoreExportJobInRedisSysMsg { val NAME = "StoreExportJobInRedisSysMsg" }
|
||||
case class StoreExportJobInRedisSysMsg(header: BbbCoreHeaderWithMeetingId, body: StoreExportJobInRedisSysMsgBody) extends BbbCoreMsg
|
||||
case class StoreExportJobInRedisSysMsgBody(exportJob: ExportJob)
|
||||
|
5
bbb-common-web/.env
Normal file
5
bbb-common-web/.env
Normal file
@ -0,0 +1,5 @@
|
||||
POSTGRES_VERSION=14.1
|
||||
POSTGRES_USER=bbb
|
||||
POSTGRES_PASSWORD=bbb123
|
||||
HOST_PORT=5432
|
||||
CONTAINER_PORT=5432
|
@ -105,3 +105,15 @@ libraryDependencies += "javax.validation" % "validation-api" % "2.0.1.Final"
|
||||
libraryDependencies += "org.springframework.boot" % "spring-boot-starter-validation" % "2.5.1"
|
||||
libraryDependencies += "org.glassfish" % "javax.el" % "3.0.1-b12"
|
||||
libraryDependencies += "org.apache.httpcomponents" % "httpclient" % "4.5.13"
|
||||
|
||||
libraryDependencies ++= Seq(
|
||||
"javax.validation" % "validation-api" % "2.0.1.Final",
|
||||
"org.springframework.boot" % "spring-boot-starter-validation" % "2.6.1",
|
||||
"org.springframework.data" % "spring-data-commons" % "2.6.1",
|
||||
"org.glassfish" % "javax.el" % "3.0.1-b12",
|
||||
"org.apache.httpcomponents" % "httpclient" % "4.5.13",
|
||||
"org.postgresql" % "postgresql" % "42.2.16",
|
||||
"org.hibernate" % "hibernate-core" % "5.6.1.Final",
|
||||
"org.flywaydb" % "flyway-core" % "7.8.2",
|
||||
"com.zaxxer" % "HikariCP" % "4.0.3"
|
||||
)
|
||||
|
2
bbb-common-web/docker-clean.sh
Normal file
2
bbb-common-web/docker-clean.sh
Normal file
@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
docker rm -f $(docker ps -aq)
|
14
bbb-common-web/docker-compose.yml
Normal file
14
bbb-common-web/docker-compose.yml
Normal file
@ -0,0 +1,14 @@
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:${POSTGRES_VERSION}
|
||||
container_name: postgres
|
||||
environment:
|
||||
- "TZ=UTC"
|
||||
- "POSTGRES_USER=${POSTGRES_USER}"
|
||||
- "POSTGRES_PASSWORD=${POSTGRES_PASSWORD}"
|
||||
ports:
|
||||
- "${HOST_PORT}:${CONTAINER_PORT}"
|
||||
volumes:
|
||||
- "./src/main/java/db/migration:/docker-entrypoint-initdb.d"
|
31
bbb-common-web/hibernate-cfg.sh
Executable file
31
bbb-common-web/hibernate-cfg.sh
Executable file
@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
. .env
|
||||
mkdir -p ./src/main/resources
|
||||
echo '<!DOCTYPE hibernate-configuration PUBLIC
|
||||
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
|
||||
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
|
||||
<hibernate-configuration>
|
||||
<session-factory>
|
||||
<!-- JDBC Database connection settings -->
|
||||
<property name="connection.driver_class">org.postgresql.Driver</property>
|
||||
<property name="connection.url">jdbc:postgresql://localhost:'"$HOST_PORT"'/bbb</property>
|
||||
<property name="connection.username">'"$POSTGRES_USER"'</property>
|
||||
<property name="connection.password">'"$POSTGRES_PASSWORD"'</property>
|
||||
<!-- JDBC connection pool settings -->
|
||||
<property name="hibernate.connection.provider_class">com.zaxxer.hikari.hibernate.HikariConnectionProvider</property>
|
||||
<property name="hibernate.hikari.minimumIdle">5</property>
|
||||
<property name="hibernate.hikari.maximumPoolSize">10</property>
|
||||
<property name="hibernate.hikari.idleTimeout">30000</property>
|
||||
<!-- Select our SQL dialect -->
|
||||
<property name="dialect">org.hibernate.dialect.PostgreSQL10Dialect</property>
|
||||
<!-- Echo the SQL to stdout -->
|
||||
<property name="show_sql">true</property>
|
||||
<!-- Set the current session context -->
|
||||
<property name="current_session_context_class">thread</property>
|
||||
<property name="hibernate.show_sql">false</property>
|
||||
<!-- format the sql nice -->
|
||||
<property name="hibernate.format_sql">false</property>
|
||||
<!-- show the hql as comment -->
|
||||
<property name="use_sql_comments">false</property>
|
||||
</session-factory>
|
||||
</hibernate-configuration>' > ./src/main/resources/hibernate.cfg.xml
|
12
bbb-common-web/psql.sh
Normal file
12
bbb-common-web/psql.sh
Normal file
@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
. .env
|
||||
echo "================== Help for psql ========================="
|
||||
echo "\\dt : Describe the current database"
|
||||
echo "\\d [table] : Describe a table"
|
||||
echo "\\c : Connect to a database"
|
||||
echo "\\h : help with SQL commands"
|
||||
echo "\\? : help with psql commands"
|
||||
echo "\\q : quit"
|
||||
echo "Reset the database using the truncate_tables('$POSTGRES_USER') function"
|
||||
echo "=================================================================="
|
||||
docker exec -it postgres psql -U $POSTGRES_USER -d bbb
|
75
bbb-common-web/src/main/java/db/migration/V1__Initial_create.sql
Executable file
75
bbb-common-web/src/main/java/db/migration/V1__Initial_create.sql
Executable file
@ -0,0 +1,75 @@
|
||||
CREATE DATABASE bbb;
|
||||
\c bbb;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS recordings (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
record_id VARCHAR(64),
|
||||
meeting_id VARCHAR(256),
|
||||
name VARCHAR(256),
|
||||
published BOOLEAN,
|
||||
participants INT,
|
||||
state VARCHAR(256),
|
||||
start_time timestamp,
|
||||
end_time timestamp,
|
||||
deleted_at timestamp,
|
||||
publish_updated BOOLEAN DEFAULT FALSE,
|
||||
protected BOOLEAN
|
||||
);
|
||||
CREATE UNIQUE INDEX index_recording_on_recording_id ON recordings (record_id);
|
||||
CREATE INDEX index_recordings_on_meeting_id ON recordings(meeting_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS metadata (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
recording_id BIGINT,
|
||||
key VARCHAR(256),
|
||||
value VARCHAR(256),
|
||||
CONSTRAINT fk_metadata_recording FOREIGN KEY(recording_id) REFERENCES recordings(id)
|
||||
);
|
||||
CREATE UNIQUE INDEX index_metadata_on_recording_id_and_key ON metadata(recording_id, key);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS playback_formats (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
recording_id BIGINT,
|
||||
format VARCHAR(64),
|
||||
url VARCHAR(256),
|
||||
length INT,
|
||||
processing_time INT,
|
||||
CONSTRAINT fk_playback_formats_recording FOREIGN KEY (recording_id) REFERENCES recordings(id)
|
||||
);
|
||||
CREATE UNIQUE INDEX index_playback_formats_on_recording_id_and_format ON playback_formats(recording_id, format);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS thumbnails (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
playback_format_id BIGINT,
|
||||
height INT,
|
||||
width INT,
|
||||
alt VARCHAR(256),
|
||||
url VARCHAR(256),
|
||||
sequence INT,
|
||||
CONSTRAINT fk_thumbnails_playback_formats FOREIGN KEY (playback_format_id) REFERENCES playback_formats(id)
|
||||
);
|
||||
CREATE INDEX index_thumbnails_on_playback_format_id ON thumbnails(playback_format_id);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS callback_data (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
recording_id BIGINT,
|
||||
meeting_id VARCHAR(256),
|
||||
callback_attributes TEXT,
|
||||
created_at timestamp NOT NULL,
|
||||
updated_at timestamp NOT NULL,
|
||||
CONSTRAINT fk_callback_data_recordings FOREIGN KEY (recording_id) REFERENCES recordings(id)
|
||||
);
|
||||
|
||||
CREATE OR REPLACE FUNCTION truncate_tables(username IN VARCHAR) RETURNS void AS $$
|
||||
DECLARE
|
||||
statements CURSOR FOR
|
||||
SELECT tablename FROM pg_tables
|
||||
WHERE tableowner = username AND schemaname = 'public';
|
||||
BEGIN
|
||||
FOR stmt IN statements LOOP
|
||||
EXECUTE 'TRUNCATE TABLE ' || quote_ident(stmt.tablename) || ' CASCADE;';
|
||||
END LOOP;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
@ -47,8 +47,8 @@ public class HTML5LoadBalancingService {
|
||||
|
||||
// Find nodejs processes associated with processing meeting events
|
||||
// $ ps -u meteor -o pcpu,cmd= | grep NODEJS_BACKEND_INSTANCE_ID
|
||||
// 1.1 /usr/share/node-v12.16.1-linux-x64/bin/node --max-old-space-size=2048 --max_semi_space_size=128 main.js NODEJS_BACKEND_INSTANCE_ID=1
|
||||
// 1.0 /usr/share/node-v12.16.1-linux-x64/bin/node --max-old-space-size=2048 --max_semi_space_size=128 main.js NODEJS_BACKEND_INSTANCE_ID=2
|
||||
// 1.1 /usr/lib/bbb-html5/node/bin/node --max-old-space-size=2048 --max_semi_space_size=128 main.js NODEJS_BACKEND_INSTANCE_ID=1
|
||||
// 1.0 /usr/lib/bbb-html5/node/bin/node --max-old-space-size=2048 --max_semi_space_size=128 main.js NODEJS_BACKEND_INSTANCE_ID=2
|
||||
public void scanHTML5processes() {
|
||||
try {
|
||||
this.list = new ArrayList<HTML5ProcessLine>();
|
||||
|
@ -79,6 +79,8 @@ import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.springframework.data.domain.*;
|
||||
|
||||
public class MeetingService implements MessageListener {
|
||||
private static Logger log = LoggerFactory.getLogger(MeetingService.class);
|
||||
|
||||
@ -574,8 +576,26 @@ public class MeetingService implements MessageListener {
|
||||
return recordingService.isRecordingExist(recordId);
|
||||
}
|
||||
|
||||
public String getRecordings2x(List<String> idList, List<String> states, Map<String, String> metadataFilters) {
|
||||
return recordingService.getRecordings2x(idList, states, metadataFilters);
|
||||
public String getRecordings2x(List<String> idList, List<String> states, Map<String, String> metadataFilters, String page, String size) {
|
||||
int p;
|
||||
int s;
|
||||
|
||||
try {
|
||||
p = Integer.parseInt(page);
|
||||
} catch(NumberFormatException e) {
|
||||
p = 0;
|
||||
}
|
||||
|
||||
try {
|
||||
s = Integer.parseInt(size);
|
||||
} catch(NumberFormatException e) {
|
||||
s = 25;
|
||||
}
|
||||
|
||||
log.info("{} {}", p, s);
|
||||
|
||||
Pageable pageable = PageRequest.of(p, s);
|
||||
return recordingService.getRecordings2x(idList, states, metadataFilters, pageable);
|
||||
}
|
||||
|
||||
public boolean existsAnyRecording(List<String> idList) {
|
||||
|
@ -19,696 +19,37 @@
|
||||
|
||||
package org.bigbluebutton.api;
|
||||
|
||||
import org.bigbluebutton.api.messaging.messages.MakePresentationDownloadableMsg;
|
||||
import org.bigbluebutton.api.model.entity.Recording;
|
||||
import org.bigbluebutton.api2.domain.UploadedTrack;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.bigbluebutton.api.domain.Recording;
|
||||
import org.bigbluebutton.api.domain.RecordingMetadata;
|
||||
import org.bigbluebutton.api.messaging.messages.MakePresentationDownloadableMsg;
|
||||
import org.bigbluebutton.api.util.RecordingMetadataReaderHelper;
|
||||
import org.bigbluebutton.api2.domain.UploadedTrack;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.domain.*;
|
||||
|
||||
public class RecordingService {
|
||||
private static Logger log = LoggerFactory.getLogger(RecordingService.class);
|
||||
public interface RecordingService {
|
||||
|
||||
private static final Pattern PRESENTATION_ID_PATTERN = Pattern.compile("^[a-z0-9]{40}-[0-9]{13}\\.[0-9a-zA-Z]{3,4}$");
|
||||
|
||||
private static String processDir = "/var/bigbluebutton/recording/process";
|
||||
private static String publishedDir = "/var/bigbluebutton/published";
|
||||
private static String unpublishedDir = "/var/bigbluebutton/unpublished";
|
||||
private static String deletedDir = "/var/bigbluebutton/deleted";
|
||||
private RecordingMetadataReaderHelper recordingServiceHelper;
|
||||
private String recordStatusDir;
|
||||
private String captionsDir;
|
||||
private String presentationBaseDir;
|
||||
private String defaultServerUrl;
|
||||
private String defaultTextTrackUrl;
|
||||
Boolean validateTextTrackSingleUseToken(String recordId, String caption, String token);
|
||||
String getRecordingTextTracks(String recordId);
|
||||
String putRecordingTextTrack(UploadedTrack track);
|
||||
String getCaptionTrackInboxDir();
|
||||
String getCaptionsDir();
|
||||
boolean isRecordingExist(String recordId);
|
||||
String getRecordings2x(List<String> idList, List<String> states, Map<String, String> metadataFilters, Pageable pageable);
|
||||
boolean existAnyRecording(List<String> idList);
|
||||
boolean changeState(String recordingId, String state);
|
||||
void updateMetaParams(List<String> recordIDs, Map<String,String> metaParams);
|
||||
void startIngestAndProcessing(String meetingId);
|
||||
void markAsEnded(String meetingId);
|
||||
void kickOffRecordingChapterBreak(String meetingId, Long timestamp);
|
||||
void processMakePresentationDownloadableMsg(MakePresentationDownloadableMsg msg);
|
||||
File getDownloadablePresentationFile(String meetingId, String presId, String presFilename);
|
||||
|
||||
private void copyPresentationFile(File presFile, File dlownloadableFile) {
|
||||
try {
|
||||
FileUtils.copyFile(presFile, dlownloadableFile);
|
||||
} catch (IOException ex) {
|
||||
log.error("Failed to copy file: {}", ex);
|
||||
}
|
||||
default <T> Page<T> listToPage(List<T> list, Pageable pageable) {
|
||||
int start = (int) pageable.getOffset();
|
||||
int end = (int) (Math.min((start + pageable.getPageSize()), list.size()));
|
||||
return new PageImpl<>(list.subList(start, end), pageable, list.size());
|
||||
}
|
||||
|
||||
public void processMakePresentationDownloadableMsg(MakePresentationDownloadableMsg msg) {
|
||||
try {
|
||||
File presDir = Util.getPresentationDir(presentationBaseDir, msg.meetingId, msg.presId);
|
||||
Util.makePresentationDownloadable(presDir, msg.presId, msg.downloadable);
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to make presentation downloadable: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
public File getDownloadablePresentationFile(String meetingId, String presId, String presFilename) {
|
||||
log.info("Find downloadable presentation for meetingId={} presId={} filename={}", meetingId, presId,
|
||||
presFilename);
|
||||
|
||||
if (! Util.isPresFileIdValidFormat(presFilename)) {
|
||||
log.error("Invalid presentation filename for meetingId={} presId={} filename={}", meetingId, presId,
|
||||
presFilename);
|
||||
return null;
|
||||
}
|
||||
|
||||
String presFilenameExt = FilenameUtils.getExtension(presFilename);
|
||||
File presDir = Util.getPresentationDir(presentationBaseDir, meetingId, presId);
|
||||
File downloadMarker = Util.getPresFileDownloadMarker(presDir, presId);
|
||||
if (presDir != null && downloadMarker != null && downloadMarker.exists()) {
|
||||
String safePresFilename = presId.concat(".").concat(presFilenameExt);
|
||||
File presFile = new File(presDir.getAbsolutePath() + File.separatorChar + safePresFilename);
|
||||
if (presFile.exists()) {
|
||||
return presFile;
|
||||
}
|
||||
|
||||
log.error("Presentation file missing for meetingId={} presId={} filename={}", meetingId, presId,
|
||||
presFilename);
|
||||
return null;
|
||||
}
|
||||
|
||||
log.error("Invalid presentation directory for meetingId={} presId={} filename={}", meetingId, presId,
|
||||
presFilename);
|
||||
return null;
|
||||
}
|
||||
|
||||
public void kickOffRecordingChapterBreak(String meetingId, Long timestamp) {
|
||||
String done = recordStatusDir + File.separatorChar + meetingId + "-" + timestamp + ".done";
|
||||
|
||||
File doneFile = new File(done);
|
||||
if (!doneFile.exists()) {
|
||||
try {
|
||||
doneFile.createNewFile();
|
||||
if (!doneFile.exists())
|
||||
log.error("Failed to create {} file.", done);
|
||||
} catch (IOException e) {
|
||||
log.error("Exception occured when trying to create {} file", done);
|
||||
}
|
||||
} else {
|
||||
log.error("{} file already exists.", done);
|
||||
}
|
||||
}
|
||||
|
||||
public void startIngestAndProcessing(String meetingId) {
|
||||
String done = recordStatusDir + File.separatorChar + meetingId + ".done";
|
||||
|
||||
File doneFile = new File(done);
|
||||
if (!doneFile.exists()) {
|
||||
try {
|
||||
doneFile.createNewFile();
|
||||
if (!doneFile.exists())
|
||||
log.error("Failed to create {} file.", done);
|
||||
} catch (IOException e) {
|
||||
log.error("Exception occured when trying to create {} file.", done);
|
||||
}
|
||||
} else {
|
||||
log.error("{} file already exists.", done);
|
||||
}
|
||||
}
|
||||
|
||||
public void markAsEnded(String meetingId) {
|
||||
String done = recordStatusDir + "/../ended/" + meetingId + ".done";
|
||||
|
||||
File doneFile = new File(done);
|
||||
if (!doneFile.exists()) {
|
||||
try {
|
||||
doneFile.createNewFile();
|
||||
if (!doneFile.exists())
|
||||
log.error("Failed to create " + done + " file.");
|
||||
} catch (IOException e) {
|
||||
log.error("Exception occured when trying to create {} file.", done);
|
||||
}
|
||||
} else {
|
||||
log.error(done + " file already exists.");
|
||||
}
|
||||
}
|
||||
|
||||
public List<RecordingMetadata> getRecordingsMetadata(List<String> recordIDs, List<String> states) {
|
||||
List<RecordingMetadata> recs = new ArrayList<>();
|
||||
|
||||
Map<String, List<File>> allDirectories = getAllDirectories(states);
|
||||
if (recordIDs.isEmpty()) {
|
||||
for (Map.Entry<String, List<File>> entry : allDirectories.entrySet()) {
|
||||
recordIDs.addAll(getAllRecordingIds(entry.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
for (String recordID : recordIDs) {
|
||||
for (Map.Entry<String, List<File>> entry : allDirectories.entrySet()) {
|
||||
List<File> _recs = getRecordingsForPath(recordID, entry.getValue());
|
||||
for (File _rec : _recs) {
|
||||
RecordingMetadata r = getRecordingMetadata(_rec);
|
||||
if (r != null) {
|
||||
recs.add(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return recs;
|
||||
}
|
||||
|
||||
public Boolean validateTextTrackSingleUseToken(String recordId, String caption, String token) {
|
||||
return recordingServiceHelper.validateTextTrackSingleUseToken(recordId, caption, token);
|
||||
}
|
||||
|
||||
public String getRecordingTextTracks(String recordId) {
|
||||
return recordingServiceHelper.getRecordingTextTracks(recordId, captionsDir, getCaptionFileUrlDirectory());
|
||||
}
|
||||
|
||||
public String putRecordingTextTrack(UploadedTrack track) {
|
||||
return recordingServiceHelper.putRecordingTextTrack(track);
|
||||
}
|
||||
|
||||
public String getRecordings2x(List<String> idList, List<String> states, Map<String, String> metadataFilters) {
|
||||
List<RecordingMetadata> recsList = getRecordingsMetadata(idList, states);
|
||||
ArrayList<RecordingMetadata> recs = filterRecordingsByMetadata(recsList, metadataFilters);
|
||||
return recordingServiceHelper.getRecordings2x(recs);
|
||||
}
|
||||
|
||||
private RecordingMetadata getRecordingMetadata(File dir) {
|
||||
File file = new File(dir.getPath() + File.separatorChar + "metadata.xml");
|
||||
return recordingServiceHelper.getRecordingMetadata(file);
|
||||
}
|
||||
|
||||
public boolean recordingMatchesMetadata(RecordingMetadata recording, Map<String, String> metadataFilters) {
|
||||
boolean matchesMetadata = true;
|
||||
Map<String, String> recMeta = recording.getMeta();
|
||||
for (Map.Entry<String, String> filter : metadataFilters.entrySet()) {
|
||||
String metadataValue = recMeta.get(filter.getKey());
|
||||
if ( metadataValue == null ) {
|
||||
// The recording doesn't have metadata specified
|
||||
matchesMetadata = false;
|
||||
} else {
|
||||
String filterValue = filter.getValue();
|
||||
if( filterValue.charAt(0) == '%' && filterValue.charAt(filterValue.length()-1) == '%' && metadataValue.contains(filterValue.substring(1, filterValue.length()-1)) ){
|
||||
// Filter value embraced by two wild cards
|
||||
// AND the filter value is part of the metadata value
|
||||
} else if( filterValue.charAt(0) == '%' && metadataValue.endsWith(filterValue.substring(1, filterValue.length())) ) {
|
||||
// Filter value starts with a wild cards
|
||||
// AND the filter value ends with the metadata value
|
||||
} else if( filterValue.charAt(filterValue.length()-1) == '%' && metadataValue.startsWith(filterValue.substring(0, filterValue.length()-1)) ) {
|
||||
// Filter value ends with a wild cards
|
||||
// AND the filter value starts with the metadata value
|
||||
} else if( metadataValue.equals(filterValue) ) {
|
||||
// Filter value doesnt have wildcards
|
||||
// AND the filter value is the same as metadata value
|
||||
} else {
|
||||
matchesMetadata = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return matchesMetadata;
|
||||
}
|
||||
|
||||
|
||||
public ArrayList<RecordingMetadata> filterRecordingsByMetadata(List<RecordingMetadata> recordings, Map<String, String> metadataFilters) {
|
||||
ArrayList<RecordingMetadata> resultRecordings = new ArrayList<>();
|
||||
for (RecordingMetadata entry : recordings) {
|
||||
if (recordingMatchesMetadata(entry, metadataFilters))
|
||||
resultRecordings.add(entry);
|
||||
}
|
||||
return resultRecordings;
|
||||
}
|
||||
|
||||
private ArrayList<File> getAllRecordingsFor(String recordId) {
|
||||
String[] format = getPlaybackFormats(publishedDir);
|
||||
ArrayList<File> ids = new ArrayList<File>();
|
||||
|
||||
for (int i = 0; i < format.length; i++) {
|
||||
List<File> recordings = getDirectories(publishedDir + File.separatorChar + format[i]);
|
||||
for (int f = 0; f < recordings.size(); f++) {
|
||||
if (recordId.equals(recordings.get(f).getName()))
|
||||
ids.add(recordings.get(f));
|
||||
}
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
public boolean isRecordingExist(String recordId) {
|
||||
List<String> publishList = getAllRecordingIds(publishedDir);
|
||||
List<String> unpublishList = getAllRecordingIds(unpublishedDir);
|
||||
if (publishList.contains(recordId) || unpublishList.contains(recordId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean existAnyRecording(List<String> idList) {
|
||||
List<String> publishList = getAllRecordingIds(publishedDir);
|
||||
List<String> unpublishList = getAllRecordingIds(unpublishedDir);
|
||||
|
||||
for (String id : idList) {
|
||||
if (publishList.contains(id) || unpublishList.contains(id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<String> getAllRecordingIds(String path) {
|
||||
String[] format = getPlaybackFormats(path);
|
||||
|
||||
return getAllRecordingIds(path, format);
|
||||
}
|
||||
|
||||
private List<String> getAllRecordingIds(String path, String[] format) {
|
||||
List<String> ids = new ArrayList<>();
|
||||
|
||||
for (String aFormat : format) {
|
||||
List<File> recordings = getDirectories(path + File.separatorChar + aFormat);
|
||||
for (File recording : recordings) {
|
||||
if (!ids.contains(recording.getName())) {
|
||||
ids.add(recording.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
private Set<String> getAllRecordingIds(List<File> recs) {
|
||||
Set<String> ids = new HashSet<>();
|
||||
|
||||
Iterator<File> iterator = recs.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
ids.add(iterator.next().getName());
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
private List<File> getRecordingsForPath(String id, List<File> recordings) {
|
||||
List<File> recs = new ArrayList<>();
|
||||
|
||||
Iterator<File> iterator = recordings.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
File rec = iterator.next();
|
||||
if (rec.getName().startsWith(id)) {
|
||||
recs.add(rec);
|
||||
}
|
||||
}
|
||||
return recs;
|
||||
}
|
||||
|
||||
private static void deleteRecording(String id, String path) {
|
||||
String[] format = getPlaybackFormats(path);
|
||||
for (String aFormat : format) {
|
||||
List<File> recordings = getDirectories(path + File.separatorChar + aFormat);
|
||||
for (File recording : recordings) {
|
||||
if (recording.getName().equals(id)) {
|
||||
deleteDirectory(recording);
|
||||
createDirectory(recording);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void createDirectory(File directory) {
|
||||
if (!directory.exists())
|
||||
directory.mkdirs();
|
||||
}
|
||||
|
||||
private static void deleteDirectory(File directory) {
|
||||
/**
|
||||
* Go through each directory and check if it's not empty. We need to
|
||||
* delete files inside a directory before a directory can be deleted.
|
||||
**/
|
||||
File[] files = directory.listFiles();
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
deleteDirectory(file);
|
||||
} else {
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
// Now that the directory is empty. Delete it.
|
||||
directory.delete();
|
||||
}
|
||||
|
||||
private static List<File> getDirectories(String path) {
|
||||
List<File> files = new ArrayList<>();
|
||||
try {
|
||||
DirectoryStream<Path> stream = Files.newDirectoryStream(FileSystems.getDefault().getPath(path));
|
||||
Iterator<Path> iter = stream.iterator();
|
||||
while (iter.hasNext()) {
|
||||
Path next = iter.next();
|
||||
files.add(next.toFile());
|
||||
}
|
||||
stream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
private static String[] getPlaybackFormats(String path) {
|
||||
System.out.println("Getting playback formats at " + path);
|
||||
List<File> dirs = getDirectories(path);
|
||||
String[] formats = new String[dirs.size()];
|
||||
|
||||
for (int i = 0; i < dirs.size(); i++) {
|
||||
System.out.println("Playback format = " + dirs.get(i).getName());
|
||||
formats[i] = dirs.get(i).getName();
|
||||
}
|
||||
return formats;
|
||||
}
|
||||
|
||||
public void setRecordingStatusDir(String dir) {
|
||||
recordStatusDir = dir;
|
||||
}
|
||||
|
||||
public void setUnpublishedDir(String dir) {
|
||||
unpublishedDir = dir;
|
||||
}
|
||||
|
||||
public void setPresentationBaseDir(String dir) {
|
||||
presentationBaseDir = dir;
|
||||
}
|
||||
|
||||
public void setDefaultServerUrl(String url) {
|
||||
defaultServerUrl = url;
|
||||
}
|
||||
|
||||
public void setDefaultTextTrackUrl(String url) {
|
||||
defaultTextTrackUrl = url;
|
||||
}
|
||||
|
||||
public void setPublishedDir(String dir) {
|
||||
publishedDir = dir;
|
||||
}
|
||||
|
||||
public void setCaptionsDir(String dir) {
|
||||
captionsDir = dir;
|
||||
}
|
||||
|
||||
public void setRecordingServiceHelper(RecordingMetadataReaderHelper r) {
|
||||
recordingServiceHelper = r;
|
||||
}
|
||||
|
||||
private boolean shouldIncludeState(List<String> states, String type) {
|
||||
boolean r = false;
|
||||
|
||||
if (!states.isEmpty()) {
|
||||
if (states.contains("any")) {
|
||||
r = true;
|
||||
} else {
|
||||
if (type.equals(Recording.STATE_PUBLISHED) && states.contains(Recording.STATE_PUBLISHED)) {
|
||||
r = true;
|
||||
} else if (type.equals(Recording.STATE_UNPUBLISHED) && states.contains(Recording.STATE_UNPUBLISHED)) {
|
||||
r = true;
|
||||
} else if (type.equals(Recording.STATE_DELETED) && states.contains(Recording.STATE_DELETED)) {
|
||||
r = true;
|
||||
} else if (type.equals(Recording.STATE_PROCESSING) && states.contains(Recording.STATE_PROCESSING)) {
|
||||
r = true;
|
||||
} else if (type.equals(Recording.STATE_PROCESSED) && states.contains(Recording.STATE_PROCESSED)) {
|
||||
r = true;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
if (type.equals(Recording.STATE_PUBLISHED) || type.equals(Recording.STATE_UNPUBLISHED)) {
|
||||
r = true;
|
||||
}
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
public boolean changeState(String recordingId, String state) {
|
||||
boolean succeeded = false;
|
||||
if (state.equals(Recording.STATE_PUBLISHED)) {
|
||||
// It can only be published if it is unpublished
|
||||
succeeded |= changeState(unpublishedDir, recordingId, state);
|
||||
} else if (state.equals(Recording.STATE_UNPUBLISHED)) {
|
||||
// It can only be unpublished if it is published
|
||||
succeeded |= changeState(publishedDir, recordingId, state);
|
||||
} else if (state.equals(Recording.STATE_DELETED)) {
|
||||
// It can be deleted from any state
|
||||
succeeded |= changeState(publishedDir, recordingId, state);
|
||||
succeeded |= changeState(unpublishedDir, recordingId, state);
|
||||
}
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
private boolean changeState(String path, String recordingId, String state) {
|
||||
boolean exists = false;
|
||||
boolean succeeded = true;
|
||||
String[] format = getPlaybackFormats(path);
|
||||
for (String aFormat : format) {
|
||||
List<File> recordings = getDirectories(path + File.separatorChar + aFormat);
|
||||
for (File recording : recordings) {
|
||||
if (recording.getName().equalsIgnoreCase(recordingId)) {
|
||||
exists = true;
|
||||
File dest;
|
||||
if (state.equals(Recording.STATE_PUBLISHED)) {
|
||||
dest = new File(publishedDir + File.separatorChar + aFormat);
|
||||
succeeded &= publishRecording(dest, recordingId, recording, aFormat);
|
||||
} else if (state.equals(Recording.STATE_UNPUBLISHED)) {
|
||||
dest = new File(unpublishedDir + File.separatorChar + aFormat);
|
||||
succeeded &= unpublishRecording(dest, recordingId, recording, aFormat);
|
||||
} else if (state.equals(Recording.STATE_DELETED)) {
|
||||
dest = new File(deletedDir + File.separatorChar + aFormat);
|
||||
succeeded &= deleteRecording(dest, recordingId, recording, aFormat);
|
||||
} else {
|
||||
log.debug(String.format("State: %s, is not supported", state));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return exists && succeeded;
|
||||
}
|
||||
|
||||
public boolean publishRecording(File destDir, String recordingId, File recordingDir, String format) {
|
||||
File metadataXml = recordingServiceHelper.getMetadataXmlLocation(recordingDir.getPath());
|
||||
RecordingMetadata r = recordingServiceHelper.getRecordingMetadata(metadataXml);
|
||||
if (r != null) {
|
||||
if (!destDir.exists()) destDir.mkdirs();
|
||||
|
||||
try {
|
||||
FileUtils.moveDirectory(recordingDir, new File(destDir.getPath() + File.separatorChar + recordingId));
|
||||
|
||||
r.setState(Recording.STATE_PUBLISHED);
|
||||
r.setPublished(true);
|
||||
|
||||
File medataXmlFile = recordingServiceHelper.getMetadataXmlLocation(
|
||||
destDir.getAbsolutePath() + File.separatorChar + recordingId);
|
||||
|
||||
// Process the changes by saving the recording into metadata.xml
|
||||
return recordingServiceHelper.saveRecordingMetadata(medataXmlFile, r);
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to publish recording : " + recordingId, e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean unpublishRecording(File destDir, String recordingId, File recordingDir, String format) {
|
||||
File metadataXml = recordingServiceHelper.getMetadataXmlLocation(recordingDir.getPath());
|
||||
|
||||
RecordingMetadata r = recordingServiceHelper.getRecordingMetadata(metadataXml);
|
||||
if (r != null) {
|
||||
if (!destDir.exists()) destDir.mkdirs();
|
||||
|
||||
try {
|
||||
FileUtils.moveDirectory(recordingDir, new File(destDir.getPath() + File.separatorChar + recordingId));
|
||||
r.setState(Recording.STATE_UNPUBLISHED);
|
||||
r.setPublished(false);
|
||||
|
||||
File medataXmlFile = recordingServiceHelper.getMetadataXmlLocation(
|
||||
destDir.getAbsolutePath() + File.separatorChar + recordingId);
|
||||
|
||||
// Process the changes by saving the recording into metadata.xml
|
||||
return recordingServiceHelper.saveRecordingMetadata(medataXmlFile, r);
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to unpublish recording : " + recordingId, e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean deleteRecording(File destDir, String recordingId, File recordingDir, String format) {
|
||||
File metadataXml = recordingServiceHelper.getMetadataXmlLocation(recordingDir.getPath());
|
||||
|
||||
RecordingMetadata r = recordingServiceHelper.getRecordingMetadata(metadataXml);
|
||||
if (r != null) {
|
||||
if (!destDir.exists()) destDir.mkdirs();
|
||||
|
||||
try {
|
||||
FileUtils.moveDirectory(recordingDir, new File(destDir.getPath() + File.separatorChar + recordingId));
|
||||
r.setState(Recording.STATE_DELETED);
|
||||
r.setPublished(false);
|
||||
|
||||
File medataXmlFile = recordingServiceHelper.getMetadataXmlLocation(
|
||||
destDir.getAbsolutePath() + File.separatorChar + recordingId);
|
||||
|
||||
// Process the changes by saving the recording into metadata.xml
|
||||
return recordingServiceHelper.saveRecordingMetadata(medataXmlFile, r);
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to delete recording : " + recordingId, e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private List<File> getAllDirectories(String state) {
|
||||
List<File> allDirectories = new ArrayList<>();
|
||||
|
||||
String dir = getDestinationBaseDirectoryName(state);
|
||||
|
||||
if ( dir != null ) {
|
||||
String[] formats = getPlaybackFormats(dir);
|
||||
for (String format : formats) {
|
||||
allDirectories.addAll(getDirectories(dir + File.separatorChar + format));
|
||||
}
|
||||
}
|
||||
|
||||
return allDirectories;
|
||||
}
|
||||
|
||||
private Map<String, List<File>> getAllDirectories(List<String> states) {
|
||||
Map<String, List<File>> allDirectories = new HashMap<>();
|
||||
|
||||
if ( shouldIncludeState(states, Recording.STATE_PUBLISHED) ) {
|
||||
List<File> listedDirectories = getAllDirectories(Recording.STATE_PUBLISHED);
|
||||
allDirectories.put(Recording.STATE_PUBLISHED, listedDirectories);
|
||||
}
|
||||
|
||||
if ( shouldIncludeState(states, Recording.STATE_UNPUBLISHED) ) {
|
||||
List<File> listedDirectories = getAllDirectories(Recording.STATE_UNPUBLISHED);
|
||||
allDirectories.put(Recording.STATE_UNPUBLISHED, listedDirectories);
|
||||
}
|
||||
|
||||
if ( shouldIncludeState(states, Recording.STATE_DELETED) ) {
|
||||
List<File> listedDirectories = getAllDirectories(Recording.STATE_DELETED);
|
||||
allDirectories.put(Recording.STATE_DELETED, listedDirectories);
|
||||
}
|
||||
|
||||
if ( shouldIncludeState(states, Recording.STATE_PROCESSING) ) {
|
||||
List<File> listedDirectories = getAllDirectories(Recording.STATE_PROCESSING);
|
||||
allDirectories.put(Recording.STATE_PROCESSING, listedDirectories);
|
||||
}
|
||||
|
||||
if ( shouldIncludeState(states, Recording.STATE_PROCESSED) ) {
|
||||
List<File> listedDirectories = getAllDirectories(Recording.STATE_PROCESSED);
|
||||
allDirectories.put(Recording.STATE_PROCESSED, listedDirectories);
|
||||
}
|
||||
|
||||
return allDirectories;
|
||||
}
|
||||
|
||||
public void updateMetaParams(List<String> recordIDs, Map<String,String> metaParams) {
|
||||
// Define the directories used to lookup the recording
|
||||
List<String> states = new ArrayList<>();
|
||||
states.add(Recording.STATE_PUBLISHED);
|
||||
states.add(Recording.STATE_UNPUBLISHED);
|
||||
states.add(Recording.STATE_DELETED);
|
||||
|
||||
// Gather all the existent directories based on the states defined for the lookup
|
||||
Map<String, List<File>> allDirectories = getAllDirectories(states);
|
||||
|
||||
// Retrieve the actual recording from the directories gathered for the lookup
|
||||
for (String recordID : recordIDs) {
|
||||
for (Map.Entry<String, List<File>> entry : allDirectories.entrySet()) {
|
||||
List<File> recs = getRecordingsForPath(recordID, entry.getValue());
|
||||
|
||||
// Go through all recordings of all formats
|
||||
for (File rec : recs) {
|
||||
File metadataXml = recordingServiceHelper.getMetadataXmlLocation(rec.getPath());
|
||||
updateRecordingMetadata(metadataXml, metaParams, metadataXml);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updateRecordingMetadata(File srxMetadataXml, Map<String,String> metaParams, File destMetadataXml) {
|
||||
RecordingMetadata rec = recordingServiceHelper.getRecordingMetadata(srxMetadataXml);
|
||||
|
||||
Map<String, String> recMeta = rec.getMeta();
|
||||
|
||||
if (rec != null && !recMeta.isEmpty()) {
|
||||
for (Map.Entry<String,String> meta : metaParams.entrySet()) {
|
||||
if ( !"".equals(meta.getValue()) ) {
|
||||
// As it has a value, if the meta parameter exists update it, otherwise add it
|
||||
recMeta.put(meta.getKey(), meta.getValue());
|
||||
} else {
|
||||
// As it doesn't have a value, if it exists delete it
|
||||
if ( recMeta.containsKey(meta.getKey()) ) {
|
||||
recMeta.remove(meta.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rec.setMeta(recMeta);
|
||||
|
||||
// Process the changes by saving the recording into metadata.xml
|
||||
recordingServiceHelper.saveRecordingMetadata(destMetadataXml, rec);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private Map<String,File> indexRecordings(List<File> recs) {
|
||||
Map<String,File> indexedRecs = new HashMap<>();
|
||||
|
||||
Iterator<File> iterator = recs.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
File rec = iterator.next();
|
||||
indexedRecs.put(rec.getName(), rec);
|
||||
}
|
||||
|
||||
return indexedRecs;
|
||||
}
|
||||
|
||||
private String getDestinationBaseDirectoryName(String state) {
|
||||
return getDestinationBaseDirectoryName(state, false);
|
||||
}
|
||||
|
||||
private String getDestinationBaseDirectoryName(String state, boolean forceDefault) {
|
||||
String baseDir = null;
|
||||
|
||||
if ( state.equals(Recording.STATE_PROCESSING) || state.equals(Recording.STATE_PROCESSED) )
|
||||
baseDir = processDir;
|
||||
else if ( state.equals(Recording.STATE_PUBLISHED) )
|
||||
baseDir = publishedDir;
|
||||
else if ( state.equals(Recording.STATE_UNPUBLISHED) )
|
||||
baseDir = unpublishedDir;
|
||||
else if ( state.equals(Recording.STATE_DELETED) )
|
||||
baseDir = deletedDir;
|
||||
else if ( forceDefault )
|
||||
baseDir = publishedDir;
|
||||
|
||||
return baseDir;
|
||||
}
|
||||
|
||||
public String getCaptionTrackInboxDir() {
|
||||
return captionsDir + File.separatorChar + "inbox";
|
||||
}
|
||||
|
||||
public String getCaptionsDir() {
|
||||
return captionsDir;
|
||||
}
|
||||
|
||||
public String getCaptionFileUrlDirectory() {
|
||||
return defaultTextTrackUrl + "/textTrack/";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
package org.bigbluebutton.api.model.entity;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Table(name = "callback_data")
|
||||
public class CallbackData {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id")
|
||||
private Long id;
|
||||
|
||||
@Column(name = "meeting_id")
|
||||
private String meetingId;
|
||||
|
||||
@Column(name = "callback_attributes")
|
||||
private String callbackAttributes;
|
||||
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@OneToOne(fetch = FetchType.EAGER)
|
||||
@JoinColumn(name = "recording_id", referencedColumnName = "id")
|
||||
private Recording recording;
|
||||
|
||||
public Long getId() { return id; }
|
||||
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getMeetingId() {
|
||||
return meetingId;
|
||||
}
|
||||
|
||||
public void setMeetingId(String meetingId) {
|
||||
this.meetingId = meetingId;
|
||||
}
|
||||
|
||||
public String getCallbackAttributes() {
|
||||
return callbackAttributes;
|
||||
}
|
||||
|
||||
public void setCallbackAttributes(String callbackAttributes) {
|
||||
this.callbackAttributes = callbackAttributes;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public Recording getRecording() { return recording; }
|
||||
|
||||
public void setRecording(Recording recording) { this.recording = recording; }
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CallbackData callbackData = (CallbackData) o;
|
||||
return Objects.equals(this.id, callbackData.id) &&
|
||||
Objects.equals(this.meetingId, callbackData.meetingId) &&
|
||||
Objects.equals(this.callbackAttributes, callbackData.callbackAttributes) &&
|
||||
Objects.equals(this.createdAt, callbackData.createdAt) &&
|
||||
Objects.equals(this.updatedAt, callbackData.updatedAt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, meetingId, callbackAttributes, createdAt, updatedAt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("class CallbackData {\n");
|
||||
sb.append(" id: ").append(toIndentedString(id)).append("\n");
|
||||
sb.append(" meetingId: ").append(toIndentedString(meetingId)).append("\n");
|
||||
sb.append(" callbackAttributes: ").append(toIndentedString(callbackAttributes)).append("\n");
|
||||
sb.append(" createdAt: ").append(toIndentedString(createdAt)).append("\n");
|
||||
sb.append(" updatedAt: ").append(toIndentedString(updatedAt)).append("\n");
|
||||
sb.append("}");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given object to string with each line indented by 4 spaces
|
||||
* (except the first line).
|
||||
*/
|
||||
private String toIndentedString(Object o) {
|
||||
if (o == null) {
|
||||
return "null";
|
||||
}
|
||||
return o.toString().replace("\n", "\n ");
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
package org.bigbluebutton.api.model.entity;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Table(name = "metadata")
|
||||
public class Metadata {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id")
|
||||
private Long id;
|
||||
|
||||
@Column(name = "key")
|
||||
private String key;
|
||||
|
||||
@Column(name = "value")
|
||||
private String value;
|
||||
|
||||
@ManyToOne(fetch = FetchType.EAGER)
|
||||
@JoinColumn(name = "recording_id", referencedColumnName = "id")
|
||||
private Recording recording;
|
||||
|
||||
public Long getId() { return id; }
|
||||
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public Recording getRecording() { return recording; }
|
||||
|
||||
public void setRecording(Recording recording) { this.recording = recording; }
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Metadata metadata = (Metadata) o;
|
||||
return Objects.equals(this.id, metadata.id) &&
|
||||
Objects.equals(this.key, metadata.key) &&
|
||||
Objects.equals(this.value, metadata.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("class Metadata {\n");
|
||||
sb.append(" id: ").append(toIndentedString(id)).append("\n");
|
||||
sb.append(" key: ").append(toIndentedString(key)).append("\n");
|
||||
sb.append(" value: ").append(toIndentedString(value)).append("\n");
|
||||
sb.append("}");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given object to string with each line indented by 4 spaces
|
||||
* (except the first line).
|
||||
*/
|
||||
private String toIndentedString(Object o) {
|
||||
if (o == null) {
|
||||
return "null";
|
||||
}
|
||||
return o.toString().replace("\n", "\n ");
|
||||
}
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
package org.bigbluebutton.api.model.entity;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
@Entity
|
||||
@Table(name = "playback_formats")
|
||||
public class PlaybackFormat {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id")
|
||||
private Long id;
|
||||
|
||||
@Column(name = "format")
|
||||
private String format;
|
||||
|
||||
@Column(name = "url")
|
||||
private String url;
|
||||
|
||||
@Column(name = "length")
|
||||
private Integer length;
|
||||
|
||||
@Column(name = "processing_time")
|
||||
private Integer processingTime;
|
||||
|
||||
@OneToOne(fetch = FetchType.EAGER)
|
||||
@JoinColumn(name = "recording_id", referencedColumnName = "id")
|
||||
private Recording recording;
|
||||
|
||||
@OneToMany(cascade = CascadeType.ALL, mappedBy = "playbackFormat", fetch = FetchType.EAGER)
|
||||
private Set<Thumbnail> thumbnails;
|
||||
|
||||
public Long getId() { return id; }
|
||||
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
public void setFormat(String format) {
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public Integer getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
public void setLength(Integer length) {
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public Integer getProcessingTime() {
|
||||
return processingTime;
|
||||
}
|
||||
|
||||
public void setProcessingTime(Integer processingTime) {
|
||||
this.processingTime = processingTime;
|
||||
}
|
||||
|
||||
public Recording getRecording() {
|
||||
return recording;
|
||||
}
|
||||
|
||||
public void setRecording(Recording recording) {
|
||||
this.recording = recording;
|
||||
}
|
||||
|
||||
public Set<Thumbnail> getThumbnails() { return thumbnails; }
|
||||
|
||||
public void setThumbnails(Set<Thumbnail> thumbnails) { this.thumbnails = thumbnails; }
|
||||
|
||||
public void addThumbnail(Thumbnail thumbnail) {
|
||||
if(thumbnails == null) {
|
||||
thumbnails = new HashSet<>();
|
||||
}
|
||||
|
||||
thumbnail.setPlaybackFormat(this);
|
||||
thumbnails.add(thumbnail);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
PlaybackFormat format = (PlaybackFormat) o;
|
||||
return Objects.equals(this.id, format.id) &&
|
||||
Objects.equals(this.format, format.format) &&
|
||||
Objects.equals(this.url, format.url) &&
|
||||
Objects.equals(this.length, format.length) &&
|
||||
Objects.equals(this.processingTime, format.processingTime) &&
|
||||
Objects.equals(this.thumbnails, format.thumbnails);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, format, url, length, processingTime, thumbnails);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("class PlaybackFormat {\n");
|
||||
sb.append(" id: ").append(toIndentedString(id)).append("\n");
|
||||
sb.append(" format: ").append(toIndentedString(format)).append("\n");
|
||||
sb.append(" url: ").append(toIndentedString(url)).append("\n");
|
||||
sb.append(" length: ").append(toIndentedString(length)).append("\n");
|
||||
sb.append(" processingTime: ").append(toIndentedString(processingTime)).append("\n");
|
||||
sb.append(" thumbnails: ").append(toIndentedString(thumbnails)).append("\n");
|
||||
sb.append("}");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given object to string with each line indented by 4 spaces
|
||||
* (except the first line).
|
||||
*/
|
||||
private String toIndentedString(Object o) {
|
||||
if (o == null) {
|
||||
return "null";
|
||||
}
|
||||
return o.toString().replace("\n", "\n ");
|
||||
}
|
||||
}
|
268
bbb-common-web/src/main/java/org/bigbluebutton/api/model/entity/Recording.java
Executable file
268
bbb-common-web/src/main/java/org/bigbluebutton/api/model/entity/Recording.java
Executable file
@ -0,0 +1,268 @@
|
||||
package org.bigbluebutton.api.model.entity;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
@Entity
|
||||
@Table(name = "recordings")
|
||||
public class Recording {
|
||||
|
||||
public enum State {
|
||||
STATE_PROCESSING("processing"),
|
||||
STATE_PROCESSED("processed"),
|
||||
STATE_PUBLISING("publishing"),
|
||||
STATE_PUBLISHED("published"),
|
||||
STATE_UNPUBLISING("unpublishing"),
|
||||
STATE_UNPUBLISHED("unpublished"),
|
||||
STATE_DELETING("deleting"),
|
||||
STATE_DELETED("deleted");
|
||||
|
||||
private String value;
|
||||
|
||||
State(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id")
|
||||
private Long id;
|
||||
|
||||
@Column(name = "record_id")
|
||||
private String recordId;
|
||||
|
||||
@Column(name = "meeting_id")
|
||||
private String meetingId;
|
||||
|
||||
@Column(name = "name")
|
||||
private String name;
|
||||
|
||||
@Column(name = "published")
|
||||
private Boolean published;
|
||||
|
||||
@Column(name = "participants")
|
||||
private Integer participants;
|
||||
|
||||
@Column(name = "state")
|
||||
private String state;
|
||||
|
||||
@Column(name = "start_time")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@Column(name = "end_time")
|
||||
private LocalDateTime endTime;
|
||||
|
||||
@Column(name = "deleted_at")
|
||||
private LocalDateTime deletedAt;
|
||||
|
||||
@Column(name = "publish_updated")
|
||||
private Boolean publishUpdated;
|
||||
|
||||
@Column(name = "protected")
|
||||
private Boolean isProtected;
|
||||
|
||||
@OneToMany(mappedBy = "recording", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
|
||||
private Set<Metadata> metadata;
|
||||
|
||||
@OneToOne(mappedBy = "recording", cascade = CascadeType.ALL)
|
||||
private PlaybackFormat format;
|
||||
|
||||
@OneToOne(mappedBy = "recording", cascade = CascadeType.ALL)
|
||||
private CallbackData callbackData;
|
||||
|
||||
public Long getId() { return id; }
|
||||
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getRecordId() {
|
||||
return recordId;
|
||||
}
|
||||
|
||||
public void setRecordId(String recordId) {
|
||||
this.recordId = recordId;
|
||||
}
|
||||
|
||||
public String getMeetingId() {
|
||||
return meetingId;
|
||||
}
|
||||
|
||||
public void setMeetingId(String meetingId) {
|
||||
this.meetingId = meetingId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Boolean getPublished() {
|
||||
return published;
|
||||
}
|
||||
|
||||
public void setPublished(Boolean published) {
|
||||
this.published = published;
|
||||
}
|
||||
|
||||
public Integer getParticipants() {
|
||||
return participants;
|
||||
}
|
||||
|
||||
public void setParticipants(Integer participants) {
|
||||
this.participants = participants;
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public void setState(String state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public LocalDateTime getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public void setStartTime(LocalDateTime startTime) {
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public LocalDateTime getEndTime() {
|
||||
return endTime;
|
||||
}
|
||||
|
||||
public void setEndTime(LocalDateTime endTime) {
|
||||
this.endTime = endTime;
|
||||
}
|
||||
|
||||
public LocalDateTime getDeletedAt() {
|
||||
return deletedAt;
|
||||
}
|
||||
|
||||
public void setDeletedAt(LocalDateTime deletedAt) {
|
||||
this.deletedAt = deletedAt;
|
||||
}
|
||||
|
||||
public Boolean getPublishUpdated() {
|
||||
return publishUpdated;
|
||||
}
|
||||
|
||||
public void setPublishUpdated(Boolean publishUpdated) {
|
||||
this.publishUpdated = publishUpdated;
|
||||
}
|
||||
|
||||
public Boolean getProtected() {
|
||||
return isProtected;
|
||||
}
|
||||
|
||||
public void setProtected(Boolean aProtected) {
|
||||
isProtected = aProtected;
|
||||
}
|
||||
|
||||
public Set<Metadata> getMetadata() { return metadata; }
|
||||
|
||||
public void setMetadata(Set<Metadata> metadata) { this.metadata = metadata; }
|
||||
|
||||
public void addMetadata(Metadata metadata) {
|
||||
if(this.metadata == null) {
|
||||
this.metadata = new HashSet<>();
|
||||
}
|
||||
|
||||
metadata.setRecording(this);
|
||||
this.metadata.add(metadata);
|
||||
}
|
||||
|
||||
public PlaybackFormat getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
public void setFormat(PlaybackFormat format) {
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
public CallbackData getCallbackData() {
|
||||
return callbackData;
|
||||
}
|
||||
|
||||
public void setCallbackData(CallbackData callbackData) {
|
||||
this.callbackData = callbackData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Recording recording = (Recording) o;
|
||||
return Objects.equals(this.id, recording.id) &&
|
||||
Objects.equals(this.recordId, recording.recordId) &&
|
||||
Objects.equals(this.meetingId, recording.meetingId) &&
|
||||
Objects.equals(this.name, recording.name) &&
|
||||
Objects.equals(this.published, recording.published) &&
|
||||
Objects.equals(this.participants, recording.participants) &&
|
||||
Objects.equals(this.state, recording.state) &&
|
||||
Objects.equals(this.startTime, recording.startTime) &&
|
||||
Objects.equals(this.endTime, recording.endTime) &&
|
||||
Objects.equals(this.deletedAt, recording.deletedAt) &&
|
||||
Objects.equals(this.publishUpdated, recording.publishUpdated) &&
|
||||
Objects.equals(this.isProtected, recording.isProtected) &&
|
||||
Objects.equals(this.metadata, recording.metadata) &&
|
||||
Objects.equals(this.format, recording.format) &&
|
||||
Objects.equals(this.callbackData, recording.callbackData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, recordId, meetingId, name, published, participants, state, startTime, endTime,
|
||||
deletedAt, publishUpdated, isProtected, metadata, format, callbackData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("class Recording {\n");
|
||||
sb.append(" id: ").append(toIndentedString(id)).append("\n");
|
||||
sb.append(" recordId: ").append(toIndentedString(recordId)).append("\n");
|
||||
sb.append(" meetingId: ").append(toIndentedString(meetingId)).append("\n");
|
||||
sb.append(" name: ").append(toIndentedString(name)).append("\n");
|
||||
sb.append(" published: ").append(toIndentedString(published)).append("\n");
|
||||
sb.append(" participants: ").append(toIndentedString(participants)).append("\n");
|
||||
sb.append(" state: ").append(toIndentedString(state)).append("\n");
|
||||
sb.append(" startTime: ").append(toIndentedString(startTime)).append("\n");
|
||||
sb.append(" endTime: ").append(toIndentedString(endTime)).append("\n");
|
||||
sb.append(" deletedAt: ").append(toIndentedString(deletedAt)).append("\n");
|
||||
sb.append(" publishUpdated: ").append(toIndentedString(publishUpdated)).append("\n");
|
||||
sb.append(" protected: ").append(toIndentedString(isProtected)).append("\n");
|
||||
sb.append(" metadata: ").append(toIndentedString(metadata)).append("\n");
|
||||
sb.append(" format: ").append(toIndentedString(format)).append("\n");
|
||||
sb.append(" callBackData: ").append(toIndentedString(callbackData)).append("\n");
|
||||
sb.append("}");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given object to string with each line indented by 4 spaces
|
||||
* (except the first line).
|
||||
*/
|
||||
private String toIndentedString(Object o) {
|
||||
if (o == null) {
|
||||
return "null";
|
||||
}
|
||||
return o.toString().replace("\n", "\n ");
|
||||
}
|
||||
}
|
132
bbb-common-web/src/main/java/org/bigbluebutton/api/model/entity/Thumbnail.java
Executable file
132
bbb-common-web/src/main/java/org/bigbluebutton/api/model/entity/Thumbnail.java
Executable file
@ -0,0 +1,132 @@
|
||||
package org.bigbluebutton.api.model.entity;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Table(name = "thumbnails")
|
||||
public class Thumbnail implements Comparable<Thumbnail> {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id")
|
||||
private Long id;
|
||||
|
||||
@Column(name = "height")
|
||||
private Integer height;
|
||||
|
||||
@Column(name = "width")
|
||||
private Integer width;
|
||||
|
||||
@Column(name = "alt")
|
||||
private String alt;
|
||||
|
||||
@Column(name = "url")
|
||||
private String url;
|
||||
|
||||
@Column(name = "sequence")
|
||||
private Integer sequence;
|
||||
|
||||
@ManyToOne(fetch = FetchType.EAGER)
|
||||
@JoinColumn(name = "playback_format_id", referencedColumnName = "id")
|
||||
private PlaybackFormat playbackFormat;
|
||||
|
||||
public Long getId() { return id; }
|
||||
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public PlaybackFormat getPlaybackFormat() { return playbackFormat; }
|
||||
|
||||
public void setPlaybackFormat(PlaybackFormat playbackFormat) { this.playbackFormat = playbackFormat; }
|
||||
|
||||
public Integer getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setHeight(Integer height) {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public Integer getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public void setWidth(Integer width) {
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
public String getAlt() {
|
||||
return alt;
|
||||
}
|
||||
|
||||
public void setAlt(String alt) {
|
||||
this.alt = alt;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public Integer getSequence() {
|
||||
return sequence;
|
||||
}
|
||||
|
||||
public void setSequence(Integer sequence) {
|
||||
this.sequence = sequence;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Thumbnail thumbnail = (Thumbnail) o;
|
||||
return Objects.equals(this.id, thumbnail.id) &&
|
||||
Objects.equals(this.height, thumbnail.height) &&
|
||||
Objects.equals(this.width, thumbnail.width) &&
|
||||
Objects.equals(this.alt, thumbnail.alt) &&
|
||||
Objects.equals(this.url, thumbnail.url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, height, width, alt, url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Thumbnail t) {
|
||||
return this.getSequence().compareTo(t.getSequence());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("class Thumbnail {\n");
|
||||
sb.append(" Id: ").append(toIndentedString(id)).append("\n");
|
||||
sb.append(" height: ").append(toIndentedString(height)).append("\n");
|
||||
sb.append(" width: ").append(toIndentedString(width)).append("\n");
|
||||
sb.append(" alt: ").append(toIndentedString(alt)).append("\n");
|
||||
sb.append(" url: ").append(toIndentedString(url)).append("\n");
|
||||
sb.append("}");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given object to string with each line indented by 4 spaces
|
||||
* (except the first line).
|
||||
*/
|
||||
private String toIndentedString(Object o) {
|
||||
if (o == null) {
|
||||
return "null";
|
||||
}
|
||||
return o.toString().replace("\n", "\n ");
|
||||
}
|
||||
}
|
19
bbb-common-web/src/main/java/org/bigbluebutton/api/service/XmlService.java
Executable file
19
bbb-common-web/src/main/java/org/bigbluebutton/api/service/XmlService.java
Executable file
@ -0,0 +1,19 @@
|
||||
package org.bigbluebutton.api.service;
|
||||
|
||||
import org.bigbluebutton.api.model.entity.*;
|
||||
|
||||
import java.util.Collection;
|
||||
import org.springframework.data.domain.*;
|
||||
|
||||
public interface XmlService {
|
||||
|
||||
String recordingsToXml(Collection<Recording> recordings);
|
||||
String recordingToXml(Recording recording);
|
||||
String metadataToXml(Metadata metadata);
|
||||
String playbackFormatToXml(PlaybackFormat playbackFormat);
|
||||
String thumbnailToXml(Thumbnail thumbnail);
|
||||
String callbackDataToXml(CallbackData callbackData);
|
||||
String constructResponseFromRecordingsXml(String xml);
|
||||
String constructPaginatedResponse(Page<?> page, String response);
|
||||
Recording xmlToRecording(String recordId, String xml);
|
||||
}
|
@ -0,0 +1,248 @@
|
||||
package org.bigbluebutton.api.service.impl;
|
||||
|
||||
import org.bigbluebutton.api.RecordingService;
|
||||
import org.bigbluebutton.api.messaging.messages.MakePresentationDownloadableMsg;
|
||||
import org.bigbluebutton.api.model.entity.Metadata;
|
||||
import org.bigbluebutton.api.model.entity.Recording;
|
||||
import org.bigbluebutton.api.service.XmlService;
|
||||
import org.bigbluebutton.api.util.DataStore;
|
||||
import org.bigbluebutton.api.util.RecordingMetadataReaderHelper;
|
||||
import org.bigbluebutton.api2.domain.UploadedTrack;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.data.domain.*;
|
||||
|
||||
public class RecordingServiceDbImpl implements RecordingService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(RecordingServiceDbImpl.class);
|
||||
|
||||
private String processDir = "/var/bigbluebutton/recording/process";
|
||||
private String publishedDir = "/var/bigbluebutton/published";
|
||||
private String unpublishedDir = "/var/bigbluebutton/unpublished";
|
||||
private String deletedDir = "/var/bigbluebutton/deleted";
|
||||
|
||||
private RecordingMetadataReaderHelper recordingServiceHelper;
|
||||
private String recordStatusDir;
|
||||
private String captionsDir;
|
||||
private String presentationBaseDir;
|
||||
private String defaultServerUrl;
|
||||
private String defaultTextTrackUrl;
|
||||
|
||||
private DataStore dataStore;
|
||||
private XmlService xmlService;
|
||||
|
||||
public RecordingServiceDbImpl() {
|
||||
dataStore = DataStore.getInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean validateTextTrackSingleUseToken(String recordId, String caption, String token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRecordingTextTracks(String recordId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String putRecordingTextTrack(UploadedTrack track) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCaptionTrackInboxDir() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCaptionsDir() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRecordingExist(String recordId) {
|
||||
return dataStore.findRecordingByRecordId(recordId) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRecordings2x(List<String> idList, List<String> states, Map<String, String> metadataFilters, Pageable pageable) {
|
||||
logger.info("Retrieving all recordings");
|
||||
Set<Recording> recordings = new HashSet<>();
|
||||
recordings.addAll(dataStore.findAll(Recording.class));
|
||||
|
||||
Set<Recording> recordingsById = new HashSet<>();
|
||||
for(String id: idList) {
|
||||
List<Recording> r = dataStore.findRecordingsByMeetingId(id);
|
||||
|
||||
if(r == null || r.size() == 0) {
|
||||
Recording recording = dataStore.findRecordingByRecordId(id);
|
||||
if(recording != null) {
|
||||
r = new ArrayList<>();
|
||||
r.add(recording);
|
||||
}
|
||||
}
|
||||
|
||||
if(r != null) recordingsById.addAll(r);
|
||||
}
|
||||
|
||||
logger.info("Filtering recordings by meeting ID");
|
||||
if(recordingsById.size() > 0) {
|
||||
recordings.retainAll(recordingsById);
|
||||
}
|
||||
logger.info("{} recordings remaining", recordings.size());
|
||||
|
||||
Set<Recording> recordingsByState = new HashSet<>();
|
||||
for(String state: states) {
|
||||
List<Recording> r = dataStore.findRecordingsByState(state);
|
||||
if(r != null) recordingsByState.addAll(r);
|
||||
}
|
||||
|
||||
logger.info("Filtering recordings by state");
|
||||
if(recordingsByState.size() > 0) {
|
||||
recordings.retainAll(recordingsByState);
|
||||
}
|
||||
logger.info("{} recordings remaining", recordings.size());
|
||||
|
||||
List<Metadata> metadata = new ArrayList<>();
|
||||
for(Map.Entry<String, String> metadataFilter: metadataFilters.entrySet()) {
|
||||
List<Metadata> m = dataStore.findMetadataByFilter(metadataFilter.getKey(), metadataFilter.getValue());
|
||||
if(m != null) metadata.addAll(m);
|
||||
}
|
||||
|
||||
Set<Recording> recordingsByMetadata = new HashSet<>();
|
||||
for(Metadata m: metadata) {
|
||||
recordingsByMetadata.add(m.getRecording());
|
||||
}
|
||||
|
||||
logger.info("Filtering recordings by metadata");
|
||||
if(recordingsByMetadata.size() > 0) {
|
||||
recordings.retainAll(recordingsByMetadata);
|
||||
}
|
||||
logger.info("{} recordings remaining", recordings.size());
|
||||
|
||||
Page<Recording> recordingsPage = listToPage(new ArrayList<>(recordings), pageable);
|
||||
String recordingsXml = xmlService.recordingsToXml(recordingsPage.getContent());
|
||||
String response = xmlService.constructResponseFromRecordingsXml(recordingsXml);
|
||||
return xmlService.constructPaginatedResponse(recordingsPage, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existAnyRecording(List<String> idList) {
|
||||
for(String id: idList) {
|
||||
if(dataStore.findRecordingByRecordId(id) != null) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean changeState(String recordingId, String state) {
|
||||
if(Stream.of(Recording.State.values()).anyMatch(x -> x.getValue().equals(state))) {
|
||||
Recording recording = dataStore.findRecordingByRecordId(recordingId);
|
||||
if(recording != null) {
|
||||
recording.setState(state);
|
||||
dataStore.save(recording);
|
||||
return true;
|
||||
} else {
|
||||
logger.error("A recording with ID {} does not exist", recordingId);
|
||||
}
|
||||
} else {
|
||||
logger.error("State [{}] is not a valid state", state);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMetaParams(List<String> recordIDs, Map<String, String> metaParams) {
|
||||
Set<Recording> recordings = new HashSet<>();
|
||||
for(String id: recordIDs) {
|
||||
Recording recording = dataStore.findRecordingByRecordId(id);
|
||||
if(recording != null) recordings.add(recording);
|
||||
}
|
||||
|
||||
for(Recording recording: recordings) {
|
||||
Set<Metadata> metadata = recording.getMetadata();
|
||||
|
||||
for(Map.Entry<String, String> entry: metaParams.entrySet()) {
|
||||
for(Metadata m: metadata) {
|
||||
if(m.getKey().equals(entry.getKey())) {
|
||||
m.setValue(entry.getValue());
|
||||
} else {
|
||||
Metadata newParam = new Metadata();
|
||||
newParam.setKey(entry.getKey());
|
||||
newParam.setValue(entry.getValue());
|
||||
newParam.setRecording(recording);
|
||||
recording.addMetadata(newParam);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dataStore.save(recording);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startIngestAndProcessing(String meetingId) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markAsEnded(String meetingId) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void kickOffRecordingChapterBreak(String meetingId, Long timestamp) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processMakePresentationDownloadableMsg(MakePresentationDownloadableMsg msg) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getDownloadablePresentationFile(String meetingId, String presId, String presFilename) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setRecordingStatusDir(String dir) {
|
||||
recordStatusDir = dir;
|
||||
}
|
||||
|
||||
public void setUnpublishedDir(String dir) {
|
||||
unpublishedDir = dir;
|
||||
}
|
||||
|
||||
public void setPresentationBaseDir(String dir) {
|
||||
presentationBaseDir = dir;
|
||||
}
|
||||
|
||||
public void setDefaultServerUrl(String url) {
|
||||
defaultServerUrl = url;
|
||||
}
|
||||
|
||||
public void setDefaultTextTrackUrl(String url) {
|
||||
defaultTextTrackUrl = url;
|
||||
}
|
||||
|
||||
public void setPublishedDir(String dir) {
|
||||
publishedDir = dir;
|
||||
}
|
||||
|
||||
public void setCaptionsDir(String dir) {
|
||||
captionsDir = dir;
|
||||
}
|
||||
|
||||
public void setRecordingServiceHelper(RecordingMetadataReaderHelper r) {
|
||||
recordingServiceHelper = r;
|
||||
}
|
||||
|
||||
public void setXmlService(XmlService xmlService) { this.xmlService = xmlService; }
|
||||
}
|
@ -0,0 +1,725 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.api.service.impl;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.bigbluebutton.api.Util;
|
||||
import org.bigbluebutton.api.RecordingService;
|
||||
import org.bigbluebutton.api.domain.Recording;
|
||||
import org.bigbluebutton.api.domain.RecordingMetadata;
|
||||
import org.bigbluebutton.api.messaging.messages.MakePresentationDownloadableMsg;
|
||||
import org.bigbluebutton.api.service.XmlService;
|
||||
import org.bigbluebutton.api.util.RecordingMetadataReaderHelper;
|
||||
import org.bigbluebutton.api2.domain.UploadedTrack;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.data.domain.*;
|
||||
|
||||
public class RecordingServiceFileImpl implements RecordingService {
|
||||
private static Logger log = LoggerFactory.getLogger(RecordingServiceFileImpl.class);
|
||||
|
||||
private static final Pattern PRESENTATION_ID_PATTERN = Pattern.compile("^[a-z0-9]{40}-[0-9]{13}\\.[0-9a-zA-Z]{3,4}$");
|
||||
|
||||
private static String processDir = "/var/bigbluebutton/recording/process";
|
||||
private static String publishedDir = "/var/bigbluebutton/published";
|
||||
private static String unpublishedDir = "/var/bigbluebutton/unpublished";
|
||||
private static String deletedDir = "/var/bigbluebutton/deleted";
|
||||
private RecordingMetadataReaderHelper recordingServiceHelper;
|
||||
private XmlService xmlService;
|
||||
private String recordStatusDir;
|
||||
private String captionsDir;
|
||||
private String presentationBaseDir;
|
||||
private String defaultServerUrl;
|
||||
private String defaultTextTrackUrl;
|
||||
|
||||
private void copyPresentationFile(File presFile, File dlownloadableFile) {
|
||||
try {
|
||||
FileUtils.copyFile(presFile, dlownloadableFile);
|
||||
} catch (IOException ex) {
|
||||
log.error("Failed to copy file: {}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void processMakePresentationDownloadableMsg(MakePresentationDownloadableMsg msg) {
|
||||
try {
|
||||
File presDir = Util.getPresentationDir(presentationBaseDir, msg.meetingId, msg.presId);
|
||||
Util.makePresentationDownloadable(presDir, msg.presId, msg.downloadable);
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to make presentation downloadable: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
public File getDownloadablePresentationFile(String meetingId, String presId, String presFilename) {
|
||||
log.info("Find downloadable presentation for meetingId={} presId={} filename={}", meetingId, presId,
|
||||
presFilename);
|
||||
|
||||
if (! Util.isPresFileIdValidFormat(presFilename)) {
|
||||
log.error("Invalid presentation filename for meetingId={} presId={} filename={}", meetingId, presId,
|
||||
presFilename);
|
||||
return null;
|
||||
}
|
||||
|
||||
String presFilenameExt = FilenameUtils.getExtension(presFilename);
|
||||
File presDir = Util.getPresentationDir(presentationBaseDir, meetingId, presId);
|
||||
File downloadMarker = Util.getPresFileDownloadMarker(presDir, presId);
|
||||
if (presDir != null && downloadMarker != null && downloadMarker.exists()) {
|
||||
String safePresFilename = presId.concat(".").concat(presFilenameExt);
|
||||
File presFile = new File(presDir.getAbsolutePath() + File.separatorChar + safePresFilename);
|
||||
if (presFile.exists()) {
|
||||
return presFile;
|
||||
}
|
||||
|
||||
log.error("Presentation file missing for meetingId={} presId={} filename={}", meetingId, presId,
|
||||
presFilename);
|
||||
return null;
|
||||
}
|
||||
|
||||
log.error("Invalid presentation directory for meetingId={} presId={} filename={}", meetingId, presId,
|
||||
presFilename);
|
||||
return null;
|
||||
}
|
||||
|
||||
public void kickOffRecordingChapterBreak(String meetingId, Long timestamp) {
|
||||
String done = recordStatusDir + File.separatorChar + meetingId + "-" + timestamp + ".done";
|
||||
|
||||
File doneFile = new File(done);
|
||||
if (!doneFile.exists()) {
|
||||
try {
|
||||
doneFile.createNewFile();
|
||||
if (!doneFile.exists())
|
||||
log.error("Failed to create {} file.", done);
|
||||
} catch (IOException e) {
|
||||
log.error("Exception occured when trying to create {} file", done);
|
||||
}
|
||||
} else {
|
||||
log.error("{} file already exists.", done);
|
||||
}
|
||||
}
|
||||
|
||||
public void startIngestAndProcessing(String meetingId) {
|
||||
String done = recordStatusDir + File.separatorChar + meetingId + ".done";
|
||||
|
||||
File doneFile = new File(done);
|
||||
if (!doneFile.exists()) {
|
||||
try {
|
||||
doneFile.createNewFile();
|
||||
if (!doneFile.exists())
|
||||
log.error("Failed to create {} file.", done);
|
||||
} catch (IOException e) {
|
||||
log.error("Exception occured when trying to create {} file.", done);
|
||||
}
|
||||
} else {
|
||||
log.error("{} file already exists.", done);
|
||||
}
|
||||
}
|
||||
|
||||
public void markAsEnded(String meetingId) {
|
||||
String done = recordStatusDir + "/../ended/" + meetingId + ".done";
|
||||
|
||||
File doneFile = new File(done);
|
||||
if (!doneFile.exists()) {
|
||||
try {
|
||||
doneFile.createNewFile();
|
||||
if (!doneFile.exists())
|
||||
log.error("Failed to create " + done + " file.");
|
||||
} catch (IOException e) {
|
||||
log.error("Exception occured when trying to create {} file.", done);
|
||||
}
|
||||
} else {
|
||||
log.error(done + " file already exists.");
|
||||
}
|
||||
}
|
||||
|
||||
public List<RecordingMetadata> getRecordingsMetadata(List<String> recordIDs, List<String> states) {
|
||||
List<RecordingMetadata> recs = new ArrayList<>();
|
||||
|
||||
Map<String, List<File>> allDirectories = getAllDirectories(states);
|
||||
if (recordIDs.isEmpty()) {
|
||||
for (Map.Entry<String, List<File>> entry : allDirectories.entrySet()) {
|
||||
recordIDs.addAll(getAllRecordingIds(entry.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
for (String recordID : recordIDs) {
|
||||
for (Map.Entry<String, List<File>> entry : allDirectories.entrySet()) {
|
||||
List<File> _recs = getRecordingsForPath(recordID, entry.getValue());
|
||||
for (File _rec : _recs) {
|
||||
RecordingMetadata r = getRecordingMetadata(_rec);
|
||||
if (r != null) {
|
||||
recs.add(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return recs;
|
||||
}
|
||||
|
||||
public Boolean validateTextTrackSingleUseToken(String recordId, String caption, String token) {
|
||||
return recordingServiceHelper.validateTextTrackSingleUseToken(recordId, caption, token);
|
||||
}
|
||||
|
||||
public String getRecordingTextTracks(String recordId) {
|
||||
return recordingServiceHelper.getRecordingTextTracks(recordId, captionsDir, getCaptionFileUrlDirectory());
|
||||
}
|
||||
|
||||
public String putRecordingTextTrack(UploadedTrack track) {
|
||||
return recordingServiceHelper.putRecordingTextTrack(track);
|
||||
}
|
||||
|
||||
public String getRecordings2x(List<String> idList, List<String> states, Map<String, String> metadataFilters, Pageable pageable) {
|
||||
List<RecordingMetadata> recsList = getRecordingsMetadata(idList, states);
|
||||
ArrayList<RecordingMetadata> recs = filterRecordingsByMetadata(recsList, metadataFilters);
|
||||
Page<RecordingMetadata> recordingsPage = listToPage(recs, pageable);
|
||||
String response = recordingServiceHelper.getRecordings2x(recs);
|
||||
return xmlService.constructPaginatedResponse(recordingsPage, response);
|
||||
}
|
||||
|
||||
private RecordingMetadata getRecordingMetadata(File dir) {
|
||||
File file = new File(dir.getPath() + File.separatorChar + "metadata.xml");
|
||||
return recordingServiceHelper.getRecordingMetadata(file);
|
||||
}
|
||||
|
||||
public boolean recordingMatchesMetadata(RecordingMetadata recording, Map<String, String> metadataFilters) {
|
||||
boolean matchesMetadata = true;
|
||||
Map<String, String> recMeta = recording.getMeta();
|
||||
for (Map.Entry<String, String> filter : metadataFilters.entrySet()) {
|
||||
String metadataValue = recMeta.get(filter.getKey());
|
||||
if ( metadataValue == null ) {
|
||||
// The recording doesn't have metadata specified
|
||||
matchesMetadata = false;
|
||||
} else {
|
||||
String filterValue = filter.getValue();
|
||||
if( filterValue.charAt(0) == '%' && filterValue.charAt(filterValue.length()-1) == '%' && metadataValue.contains(filterValue.substring(1, filterValue.length()-1)) ){
|
||||
// Filter value embraced by two wild cards
|
||||
// AND the filter value is part of the metadata value
|
||||
} else if( filterValue.charAt(0) == '%' && metadataValue.endsWith(filterValue.substring(1, filterValue.length())) ) {
|
||||
// Filter value starts with a wild cards
|
||||
// AND the filter value ends with the metadata value
|
||||
} else if( filterValue.charAt(filterValue.length()-1) == '%' && metadataValue.startsWith(filterValue.substring(0, filterValue.length()-1)) ) {
|
||||
// Filter value ends with a wild cards
|
||||
// AND the filter value starts with the metadata value
|
||||
} else if( metadataValue.equals(filterValue) ) {
|
||||
// Filter value doesnt have wildcards
|
||||
// AND the filter value is the same as metadata value
|
||||
} else {
|
||||
matchesMetadata = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return matchesMetadata;
|
||||
}
|
||||
|
||||
|
||||
public ArrayList<RecordingMetadata> filterRecordingsByMetadata(List<RecordingMetadata> recordings, Map<String, String> metadataFilters) {
|
||||
ArrayList<RecordingMetadata> resultRecordings = new ArrayList<>();
|
||||
for (RecordingMetadata entry : recordings) {
|
||||
if (recordingMatchesMetadata(entry, metadataFilters))
|
||||
resultRecordings.add(entry);
|
||||
}
|
||||
return resultRecordings;
|
||||
}
|
||||
|
||||
private ArrayList<File> getAllRecordingsFor(String recordId) {
|
||||
String[] format = getPlaybackFormats(publishedDir);
|
||||
ArrayList<File> ids = new ArrayList<File>();
|
||||
|
||||
for (int i = 0; i < format.length; i++) {
|
||||
List<File> recordings = getDirectories(publishedDir + File.separatorChar + format[i]);
|
||||
for (int f = 0; f < recordings.size(); f++) {
|
||||
if (recordId.equals(recordings.get(f).getName()))
|
||||
ids.add(recordings.get(f));
|
||||
}
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
public boolean isRecordingExist(String recordId) {
|
||||
List<String> publishList = getAllRecordingIds(publishedDir);
|
||||
List<String> unpublishList = getAllRecordingIds(unpublishedDir);
|
||||
if (publishList.contains(recordId) || unpublishList.contains(recordId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean existAnyRecording(List<String> idList) {
|
||||
List<String> publishList = getAllRecordingIds(publishedDir);
|
||||
List<String> unpublishList = getAllRecordingIds(unpublishedDir);
|
||||
|
||||
for (String id : idList) {
|
||||
if (publishList.contains(id) || unpublishList.contains(id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<String> getAllRecordingIds(String path) {
|
||||
String[] format = getPlaybackFormats(path);
|
||||
|
||||
return getAllRecordingIds(path, format);
|
||||
}
|
||||
|
||||
private List<String> getAllRecordingIds(String path, String[] format) {
|
||||
List<String> ids = new ArrayList<>();
|
||||
|
||||
for (String aFormat : format) {
|
||||
List<File> recordings = getDirectories(path + File.separatorChar + aFormat);
|
||||
for (File recording : recordings) {
|
||||
if (!ids.contains(recording.getName())) {
|
||||
ids.add(recording.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
private Set<String> getAllRecordingIds(List<File> recs) {
|
||||
Set<String> ids = new HashSet<>();
|
||||
|
||||
Iterator<File> iterator = recs.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
ids.add(iterator.next().getName());
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
private List<File> getRecordingsForPath(String id, List<File> recordings) {
|
||||
List<File> recs = new ArrayList<>();
|
||||
|
||||
Iterator<File> iterator = recordings.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
File rec = iterator.next();
|
||||
if (rec.getName().startsWith(id)) {
|
||||
recs.add(rec);
|
||||
}
|
||||
}
|
||||
return recs;
|
||||
}
|
||||
|
||||
private static void deleteRecording(String id, String path) {
|
||||
String[] format = getPlaybackFormats(path);
|
||||
for (String aFormat : format) {
|
||||
List<File> recordings = getDirectories(path + File.separatorChar + aFormat);
|
||||
for (File recording : recordings) {
|
||||
if (recording.getName().equals(id)) {
|
||||
deleteDirectory(recording);
|
||||
createDirectory(recording);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void createDirectory(File directory) {
|
||||
if (!directory.exists())
|
||||
directory.mkdirs();
|
||||
}
|
||||
|
||||
private static void deleteDirectory(File directory) {
|
||||
/**
|
||||
* Go through each directory and check if it's not empty. We need to
|
||||
* delete files inside a directory before a directory can be deleted.
|
||||
**/
|
||||
File[] files = directory.listFiles();
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
deleteDirectory(file);
|
||||
} else {
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
// Now that the directory is empty. Delete it.
|
||||
directory.delete();
|
||||
}
|
||||
|
||||
private static List<File> getDirectories(String path) {
|
||||
List<File> files = new ArrayList<>();
|
||||
try {
|
||||
DirectoryStream<Path> stream = Files.newDirectoryStream(FileSystems.getDefault().getPath(path));
|
||||
Iterator<Path> iter = stream.iterator();
|
||||
while (iter.hasNext()) {
|
||||
Path next = iter.next();
|
||||
files.add(next.toFile());
|
||||
}
|
||||
stream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
private static String[] getPlaybackFormats(String path) {
|
||||
System.out.println("Getting playback formats at " + path);
|
||||
List<File> dirs = getDirectories(path);
|
||||
String[] formats = new String[dirs.size()];
|
||||
|
||||
for (int i = 0; i < dirs.size(); i++) {
|
||||
System.out.println("Playback format = " + dirs.get(i).getName());
|
||||
formats[i] = dirs.get(i).getName();
|
||||
}
|
||||
return formats;
|
||||
}
|
||||
|
||||
public void setRecordingStatusDir(String dir) {
|
||||
recordStatusDir = dir;
|
||||
}
|
||||
|
||||
public void setUnpublishedDir(String dir) {
|
||||
unpublishedDir = dir;
|
||||
}
|
||||
|
||||
public void setPresentationBaseDir(String dir) {
|
||||
presentationBaseDir = dir;
|
||||
}
|
||||
|
||||
public void setDefaultServerUrl(String url) {
|
||||
defaultServerUrl = url;
|
||||
}
|
||||
|
||||
public void setDefaultTextTrackUrl(String url) {
|
||||
defaultTextTrackUrl = url;
|
||||
}
|
||||
|
||||
public void setPublishedDir(String dir) {
|
||||
publishedDir = dir;
|
||||
}
|
||||
|
||||
public void setCaptionsDir(String dir) {
|
||||
captionsDir = dir;
|
||||
}
|
||||
|
||||
public void setRecordingServiceHelper(RecordingMetadataReaderHelper r) {
|
||||
recordingServiceHelper = r;
|
||||
}
|
||||
|
||||
public void setXmlService(XmlService xmlService) { this.xmlService = xmlService; }
|
||||
|
||||
private boolean shouldIncludeState(List<String> states, String type) {
|
||||
boolean r = false;
|
||||
|
||||
if (!states.isEmpty()) {
|
||||
if (states.contains("any")) {
|
||||
r = true;
|
||||
} else {
|
||||
if (type.equals(Recording.STATE_PUBLISHED) && states.contains(Recording.STATE_PUBLISHED)) {
|
||||
r = true;
|
||||
} else if (type.equals(Recording.STATE_UNPUBLISHED) && states.contains(Recording.STATE_UNPUBLISHED)) {
|
||||
r = true;
|
||||
} else if (type.equals(Recording.STATE_DELETED) && states.contains(Recording.STATE_DELETED)) {
|
||||
r = true;
|
||||
} else if (type.equals(Recording.STATE_PROCESSING) && states.contains(Recording.STATE_PROCESSING)) {
|
||||
r = true;
|
||||
} else if (type.equals(Recording.STATE_PROCESSED) && states.contains(Recording.STATE_PROCESSED)) {
|
||||
r = true;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
if (type.equals(Recording.STATE_PUBLISHED) || type.equals(Recording.STATE_UNPUBLISHED)) {
|
||||
r = true;
|
||||
}
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
public boolean changeState(String recordingId, String state) {
|
||||
boolean succeeded = false;
|
||||
if (state.equals(Recording.STATE_PUBLISHED)) {
|
||||
// It can only be published if it is unpublished
|
||||
succeeded |= changeState(unpublishedDir, recordingId, state);
|
||||
} else if (state.equals(Recording.STATE_UNPUBLISHED)) {
|
||||
// It can only be unpublished if it is published
|
||||
succeeded |= changeState(publishedDir, recordingId, state);
|
||||
} else if (state.equals(Recording.STATE_DELETED)) {
|
||||
// It can be deleted from any state
|
||||
succeeded |= changeState(publishedDir, recordingId, state);
|
||||
succeeded |= changeState(unpublishedDir, recordingId, state);
|
||||
}
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
private boolean changeState(String path, String recordingId, String state) {
|
||||
boolean exists = false;
|
||||
boolean succeeded = true;
|
||||
String[] format = getPlaybackFormats(path);
|
||||
for (String aFormat : format) {
|
||||
List<File> recordings = getDirectories(path + File.separatorChar + aFormat);
|
||||
for (File recording : recordings) {
|
||||
if (recording.getName().equalsIgnoreCase(recordingId)) {
|
||||
exists = true;
|
||||
File dest;
|
||||
if (state.equals(Recording.STATE_PUBLISHED)) {
|
||||
dest = new File(publishedDir + File.separatorChar + aFormat);
|
||||
succeeded &= publishRecording(dest, recordingId, recording, aFormat);
|
||||
} else if (state.equals(Recording.STATE_UNPUBLISHED)) {
|
||||
dest = new File(unpublishedDir + File.separatorChar + aFormat);
|
||||
succeeded &= unpublishRecording(dest, recordingId, recording, aFormat);
|
||||
} else if (state.equals(Recording.STATE_DELETED)) {
|
||||
dest = new File(deletedDir + File.separatorChar + aFormat);
|
||||
succeeded &= deleteRecording(dest, recordingId, recording, aFormat);
|
||||
} else {
|
||||
log.debug(String.format("State: %s, is not supported", state));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return exists && succeeded;
|
||||
}
|
||||
|
||||
public boolean publishRecording(File destDir, String recordingId, File recordingDir, String format) {
|
||||
File metadataXml = recordingServiceHelper.getMetadataXmlLocation(recordingDir.getPath());
|
||||
RecordingMetadata r = recordingServiceHelper.getRecordingMetadata(metadataXml);
|
||||
if (r != null) {
|
||||
if (!destDir.exists()) destDir.mkdirs();
|
||||
|
||||
try {
|
||||
FileUtils.moveDirectory(recordingDir, new File(destDir.getPath() + File.separatorChar + recordingId));
|
||||
|
||||
r.setState(Recording.STATE_PUBLISHED);
|
||||
r.setPublished(true);
|
||||
|
||||
File medataXmlFile = recordingServiceHelper.getMetadataXmlLocation(
|
||||
destDir.getAbsolutePath() + File.separatorChar + recordingId);
|
||||
|
||||
// Process the changes by saving the recording into metadata.xml
|
||||
return recordingServiceHelper.saveRecordingMetadata(medataXmlFile, r);
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to publish recording : " + recordingId, e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean unpublishRecording(File destDir, String recordingId, File recordingDir, String format) {
|
||||
File metadataXml = recordingServiceHelper.getMetadataXmlLocation(recordingDir.getPath());
|
||||
|
||||
RecordingMetadata r = recordingServiceHelper.getRecordingMetadata(metadataXml);
|
||||
if (r != null) {
|
||||
if (!destDir.exists()) destDir.mkdirs();
|
||||
|
||||
try {
|
||||
FileUtils.moveDirectory(recordingDir, new File(destDir.getPath() + File.separatorChar + recordingId));
|
||||
r.setState(Recording.STATE_UNPUBLISHED);
|
||||
r.setPublished(false);
|
||||
|
||||
File medataXmlFile = recordingServiceHelper.getMetadataXmlLocation(
|
||||
destDir.getAbsolutePath() + File.separatorChar + recordingId);
|
||||
|
||||
// Process the changes by saving the recording into metadata.xml
|
||||
return recordingServiceHelper.saveRecordingMetadata(medataXmlFile, r);
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to unpublish recording : " + recordingId, e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean deleteRecording(File destDir, String recordingId, File recordingDir, String format) {
|
||||
File metadataXml = recordingServiceHelper.getMetadataXmlLocation(recordingDir.getPath());
|
||||
|
||||
RecordingMetadata r = recordingServiceHelper.getRecordingMetadata(metadataXml);
|
||||
if (r != null) {
|
||||
if (!destDir.exists()) destDir.mkdirs();
|
||||
|
||||
try {
|
||||
FileUtils.moveDirectory(recordingDir, new File(destDir.getPath() + File.separatorChar + recordingId));
|
||||
r.setState(Recording.STATE_DELETED);
|
||||
r.setPublished(false);
|
||||
|
||||
File medataXmlFile = recordingServiceHelper.getMetadataXmlLocation(
|
||||
destDir.getAbsolutePath() + File.separatorChar + recordingId);
|
||||
|
||||
// Process the changes by saving the recording into metadata.xml
|
||||
return recordingServiceHelper.saveRecordingMetadata(medataXmlFile, r);
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to delete recording : " + recordingId, e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private List<File> getAllDirectories(String state) {
|
||||
List<File> allDirectories = new ArrayList<>();
|
||||
|
||||
String dir = getDestinationBaseDirectoryName(state);
|
||||
|
||||
if ( dir != null ) {
|
||||
String[] formats = getPlaybackFormats(dir);
|
||||
for (String format : formats) {
|
||||
allDirectories.addAll(getDirectories(dir + File.separatorChar + format));
|
||||
}
|
||||
}
|
||||
|
||||
return allDirectories;
|
||||
}
|
||||
|
||||
private Map<String, List<File>> getAllDirectories(List<String> states) {
|
||||
Map<String, List<File>> allDirectories = new HashMap<>();
|
||||
|
||||
if ( shouldIncludeState(states, Recording.STATE_PUBLISHED) ) {
|
||||
List<File> listedDirectories = getAllDirectories(Recording.STATE_PUBLISHED);
|
||||
allDirectories.put(Recording.STATE_PUBLISHED, listedDirectories);
|
||||
}
|
||||
|
||||
if ( shouldIncludeState(states, Recording.STATE_UNPUBLISHED) ) {
|
||||
List<File> listedDirectories = getAllDirectories(Recording.STATE_UNPUBLISHED);
|
||||
allDirectories.put(Recording.STATE_UNPUBLISHED, listedDirectories);
|
||||
}
|
||||
|
||||
if ( shouldIncludeState(states, Recording.STATE_DELETED) ) {
|
||||
List<File> listedDirectories = getAllDirectories(Recording.STATE_DELETED);
|
||||
allDirectories.put(Recording.STATE_DELETED, listedDirectories);
|
||||
}
|
||||
|
||||
if ( shouldIncludeState(states, Recording.STATE_PROCESSING) ) {
|
||||
List<File> listedDirectories = getAllDirectories(Recording.STATE_PROCESSING);
|
||||
allDirectories.put(Recording.STATE_PROCESSING, listedDirectories);
|
||||
}
|
||||
|
||||
if ( shouldIncludeState(states, Recording.STATE_PROCESSED) ) {
|
||||
List<File> listedDirectories = getAllDirectories(Recording.STATE_PROCESSED);
|
||||
allDirectories.put(Recording.STATE_PROCESSED, listedDirectories);
|
||||
}
|
||||
|
||||
return allDirectories;
|
||||
}
|
||||
|
||||
public void updateMetaParams(List<String> recordIDs, Map<String,String> metaParams) {
|
||||
// Define the directories used to lookup the recording
|
||||
List<String> states = new ArrayList<>();
|
||||
states.add(Recording.STATE_PUBLISHED);
|
||||
states.add(Recording.STATE_UNPUBLISHED);
|
||||
states.add(Recording.STATE_DELETED);
|
||||
|
||||
// Gather all the existent directories based on the states defined for the lookup
|
||||
Map<String, List<File>> allDirectories = getAllDirectories(states);
|
||||
|
||||
// Retrieve the actual recording from the directories gathered for the lookup
|
||||
for (String recordID : recordIDs) {
|
||||
for (Map.Entry<String, List<File>> entry : allDirectories.entrySet()) {
|
||||
List<File> recs = getRecordingsForPath(recordID, entry.getValue());
|
||||
|
||||
// Go through all recordings of all formats
|
||||
for (File rec : recs) {
|
||||
File metadataXml = recordingServiceHelper.getMetadataXmlLocation(rec.getPath());
|
||||
updateRecordingMetadata(metadataXml, metaParams, metadataXml);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updateRecordingMetadata(File srxMetadataXml, Map<String,String> metaParams, File destMetadataXml) {
|
||||
RecordingMetadata rec = recordingServiceHelper.getRecordingMetadata(srxMetadataXml);
|
||||
|
||||
Map<String, String> recMeta = rec.getMeta();
|
||||
|
||||
if (rec != null && !recMeta.isEmpty()) {
|
||||
for (Map.Entry<String,String> meta : metaParams.entrySet()) {
|
||||
if ( !"".equals(meta.getValue()) ) {
|
||||
// As it has a value, if the meta parameter exists update it, otherwise add it
|
||||
recMeta.put(meta.getKey(), meta.getValue());
|
||||
} else {
|
||||
// As it doesn't have a value, if it exists delete it
|
||||
if ( recMeta.containsKey(meta.getKey()) ) {
|
||||
recMeta.remove(meta.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rec.setMeta(recMeta);
|
||||
|
||||
// Process the changes by saving the recording into metadata.xml
|
||||
recordingServiceHelper.saveRecordingMetadata(destMetadataXml, rec);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private Map<String,File> indexRecordings(List<File> recs) {
|
||||
Map<String,File> indexedRecs = new HashMap<>();
|
||||
|
||||
Iterator<File> iterator = recs.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
File rec = iterator.next();
|
||||
indexedRecs.put(rec.getName(), rec);
|
||||
}
|
||||
|
||||
return indexedRecs;
|
||||
}
|
||||
|
||||
private String getDestinationBaseDirectoryName(String state) {
|
||||
return getDestinationBaseDirectoryName(state, false);
|
||||
}
|
||||
|
||||
private String getDestinationBaseDirectoryName(String state, boolean forceDefault) {
|
||||
String baseDir = null;
|
||||
|
||||
if ( state.equals(Recording.STATE_PROCESSING) || state.equals(Recording.STATE_PROCESSED) )
|
||||
baseDir = processDir;
|
||||
else if ( state.equals(Recording.STATE_PUBLISHED) )
|
||||
baseDir = publishedDir;
|
||||
else if ( state.equals(Recording.STATE_UNPUBLISHED) )
|
||||
baseDir = unpublishedDir;
|
||||
else if ( state.equals(Recording.STATE_DELETED) )
|
||||
baseDir = deletedDir;
|
||||
else if ( forceDefault )
|
||||
baseDir = publishedDir;
|
||||
|
||||
return baseDir;
|
||||
}
|
||||
|
||||
public String getCaptionTrackInboxDir() {
|
||||
return captionsDir + File.separatorChar + "inbox";
|
||||
}
|
||||
|
||||
public String getCaptionsDir() {
|
||||
return captionsDir;
|
||||
}
|
||||
|
||||
public String getCaptionFileUrlDirectory() {
|
||||
return defaultTextTrackUrl + "/textTrack/";
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,594 @@
|
||||
package org.bigbluebutton.api.service.impl;
|
||||
|
||||
import org.bigbluebutton.api.model.entity.*;
|
||||
import org.bigbluebutton.api.service.XmlService;
|
||||
import org.w3c.dom.CharacterData;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.transform.OutputKeys;
|
||||
import javax.xml.transform.Transformer;
|
||||
import javax.xml.transform.TransformerFactory;
|
||||
import javax.xml.transform.dom.DOMSource;
|
||||
import javax.xml.transform.stream.StreamResult;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.StringWriter;
|
||||
import java.lang.reflect.Field;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.*;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.w3c.dom.Node;
|
||||
|
||||
import org.springframework.data.domain.*;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
public class XmlServiceImpl implements XmlService {
|
||||
|
||||
private static Logger logger = LoggerFactory.getLogger(XmlServiceImpl.class);
|
||||
|
||||
private DocumentBuilderFactory factory;
|
||||
private DocumentBuilder builder;
|
||||
|
||||
@Override
|
||||
public String recordingsToXml(Collection<Recording> recordings) {
|
||||
logger.info("Converting {} recordings to xml", recordings.size());
|
||||
try {
|
||||
setup();
|
||||
Document document = builder.newDocument();
|
||||
|
||||
Element rootElement = createElement(document, "recordings", null);
|
||||
document.appendChild(rootElement);
|
||||
|
||||
String xml;
|
||||
Document secondDoc;
|
||||
Node node;
|
||||
|
||||
for(Recording recording: recordings) {
|
||||
xml = recordingToXml(recording);
|
||||
secondDoc = builder.parse(new ByteArrayInputStream(xml.getBytes()));
|
||||
node = document.importNode(secondDoc.getDocumentElement(), true);
|
||||
rootElement.appendChild(node);
|
||||
}
|
||||
|
||||
String result = documentToString(document);
|
||||
// logger.info("========== Result ==========");
|
||||
// logger.info("{}", result);
|
||||
// logger.info("============================");
|
||||
return result;
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String recordingToXml(Recording recording) {
|
||||
logger.info("Converting {} to xml", recording);
|
||||
try {
|
||||
setup();
|
||||
Document document = builder.newDocument();
|
||||
|
||||
Element rootElement = createElement(document,"recording", null);
|
||||
document.appendChild(rootElement);
|
||||
appendFields(document, rootElement, recording, new String[] {"id", "metadata", "format", "callbackData"}, Type.CHILD);
|
||||
|
||||
Element meta = createElement(document, "meta", null);
|
||||
rootElement.appendChild(meta);
|
||||
|
||||
String xml;
|
||||
Document secondDoc;
|
||||
Node node;
|
||||
|
||||
if(recording.getMetadata() != null) {
|
||||
for(Metadata metadata: recording.getMetadata()) {
|
||||
xml = metadataToXml(metadata);
|
||||
secondDoc = builder.parse(new ByteArrayInputStream(xml.getBytes()));
|
||||
node = document.importNode(secondDoc.getDocumentElement(), true);
|
||||
meta.appendChild(node);
|
||||
}
|
||||
}
|
||||
|
||||
if(recording.getFormat() != null) {
|
||||
xml = playbackFormatToXml(recording.getFormat());
|
||||
secondDoc = builder.parse(new ByteArrayInputStream(xml.getBytes()));
|
||||
node = document.importNode(secondDoc.getDocumentElement(), true);
|
||||
rootElement.appendChild(node);
|
||||
}
|
||||
|
||||
if(recording.getCallbackData() != null) {
|
||||
xml = callbackDataToXml(recording.getCallbackData());
|
||||
secondDoc = builder.parse(new ByteArrayInputStream(xml.getBytes()));
|
||||
node = document.importNode(secondDoc.getDocumentElement(), true);
|
||||
rootElement.appendChild(node);
|
||||
}
|
||||
|
||||
String result = documentToString(document);
|
||||
// logger.info("========== Result ==========");
|
||||
// logger.info("{}", result);
|
||||
// logger.info("============================");
|
||||
return result;
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String metadataToXml(Metadata metadata) {
|
||||
logger.info("Converting {} to xml", metadata);
|
||||
|
||||
try {
|
||||
setup();
|
||||
Document document = builder.newDocument();
|
||||
|
||||
Element rootElement = createElement(document, metadata.getKey(), metadata.getValue());
|
||||
document.appendChild(rootElement);
|
||||
|
||||
String result = documentToString(document);
|
||||
// logger.info("========== Result ==========");
|
||||
// logger.info("{}", result);
|
||||
// logger.info("============================");
|
||||
return result;
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String playbackFormatToXml(PlaybackFormat playbackFormat) {
|
||||
logger.info("Converting {} to xml", playbackFormat);
|
||||
|
||||
try {
|
||||
setup();
|
||||
Document document = builder.newDocument();
|
||||
|
||||
Element rootElement = createElement(document, "playback", null);
|
||||
document.appendChild(rootElement);
|
||||
appendFields(document, rootElement, playbackFormat, new String[] {"id", "recording", "thumbnails"}, Type.CHILD);
|
||||
|
||||
if(playbackFormat.getThumbnails() != null && !playbackFormat.getThumbnails().isEmpty()) {
|
||||
Element images = createElement(document, "images", null);
|
||||
rootElement.appendChild(images);
|
||||
|
||||
List<Thumbnail> thumbnails = new ArrayList<>(playbackFormat.getThumbnails());
|
||||
Collections.sort(thumbnails);
|
||||
|
||||
for(Thumbnail thumbnail: thumbnails) {
|
||||
String xml = thumbnailToXml(thumbnail);
|
||||
Document thumbnailDoc = builder.parse(new ByteArrayInputStream(xml.getBytes()));
|
||||
Node node = document.importNode(thumbnailDoc.getDocumentElement(), true);
|
||||
images.appendChild(node);
|
||||
}
|
||||
}
|
||||
|
||||
String result = documentToString(document);
|
||||
// logger.info("========== Result ==========");
|
||||
// logger.info("{}", result);
|
||||
// logger.info("============================");
|
||||
return result;
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String thumbnailToXml(Thumbnail thumbnail) {
|
||||
logger.info("Converting {} to xml", thumbnail);
|
||||
|
||||
try {
|
||||
setup();
|
||||
Document document = builder.newDocument();
|
||||
|
||||
Element rootElement = createElement(document, "image", thumbnail.getUrl());
|
||||
document.appendChild(rootElement);
|
||||
appendFields(document, rootElement, thumbnail, new String[] {"id", "url", "playbackFormat"}, Type.ATTRIBUTE);
|
||||
|
||||
String result = documentToString(document);
|
||||
// logger.info("========== Result ==========");
|
||||
// logger.info("{}", result);
|
||||
// logger.info("============================");
|
||||
return result;
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String callbackDataToXml(CallbackData callbackData) {
|
||||
logger.info("Converting {} to xml", callbackData);
|
||||
|
||||
try {
|
||||
setup();
|
||||
Document document = builder.newDocument();
|
||||
|
||||
Element rootElement = createElement(document, "callback", null);
|
||||
document.appendChild(rootElement);
|
||||
appendFields(document, rootElement, callbackData, new String[] {"id", "recording"}, Type.CHILD);
|
||||
|
||||
String result = documentToString(document);
|
||||
// logger.info("========== Result ==========");
|
||||
// logger.info("{}", result);
|
||||
// logger.info("============================");
|
||||
return result;
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String constructResponseFromRecordingsXml(String xml) {
|
||||
logger.info("Constructing response from recordings xml");
|
||||
|
||||
try {
|
||||
setup();
|
||||
Document document = builder.newDocument();
|
||||
|
||||
Element rootElement = createElement(document, "response", null);
|
||||
document.appendChild(rootElement);
|
||||
|
||||
Element returnCode = createElement(document, "returncode", "SUCCESS");
|
||||
rootElement.appendChild(returnCode);
|
||||
|
||||
Document recordingsDoc = builder.parse(new ByteArrayInputStream(xml.getBytes()));
|
||||
Node recordingsNode = document.importNode(recordingsDoc.getDocumentElement(), true);
|
||||
rootElement.appendChild(recordingsNode);
|
||||
|
||||
String result = documentToString(document);
|
||||
// logger.info("========== Result ==========");
|
||||
// logger.info("{}", result);
|
||||
// logger.info("============================");
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String constructPaginatedResponse(Page<?> page, String response) {
|
||||
logger.info("Constructing paginated response");
|
||||
|
||||
try {
|
||||
setup();
|
||||
|
||||
if(response == null || response.equals("")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Document document = builder.parse(new ByteArrayInputStream(response.getBytes()));
|
||||
Element rootElement = document.getDocumentElement();
|
||||
|
||||
Element pagination = createElement(document, "pagination", null);
|
||||
|
||||
String xml;
|
||||
Document secondDoc;
|
||||
Node node;
|
||||
|
||||
xml = pageableToXml(page.getPageable());
|
||||
secondDoc = builder.parse(new ByteArrayInputStream(xml.getBytes()));
|
||||
node = document.importNode(secondDoc.getDocumentElement(), true);
|
||||
pagination.appendChild(node);
|
||||
|
||||
Element totalElements = createElement(document, "totalElements", String.valueOf(page.getTotalElements()));
|
||||
pagination.appendChild(totalElements);
|
||||
|
||||
Element last = createElement(document, "last", String.valueOf(page.isLast()));
|
||||
pagination.appendChild(last);
|
||||
|
||||
Element totalPages = createElement(document, "totalPages", String.valueOf(page.getTotalPages()));
|
||||
pagination.appendChild(totalPages);
|
||||
|
||||
Element first = createElement(document, "first", String.valueOf(page.isFirst()));
|
||||
pagination.appendChild(first);
|
||||
|
||||
Element empty = createElement(document, "empty", String.valueOf(!page.hasContent()));
|
||||
pagination.appendChild(empty);
|
||||
|
||||
rootElement.appendChild(pagination);
|
||||
|
||||
String result = documentToString(document);
|
||||
// logger.info("========== Result ==========");
|
||||
// logger.info("{}", result);
|
||||
// logger.info("============================");
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private String pageableToXml(Pageable pageable) {
|
||||
logger.info("Converting {} to xml", pageable);
|
||||
|
||||
try {
|
||||
setup();
|
||||
Document document = builder.newDocument();
|
||||
|
||||
Element rootElement = createElement(document, "pageable", null);
|
||||
document.appendChild(rootElement);
|
||||
|
||||
Sort sort = pageable.getSort();
|
||||
Element sortElement = createElement(document, "sort", null);
|
||||
|
||||
Element unsorted = createElement(document, "unsorted", String.valueOf(sort.isUnsorted()));
|
||||
sortElement.appendChild(unsorted);
|
||||
|
||||
Element sorted = createElement(document, "sorted", String.valueOf(sort.isSorted()));
|
||||
sortElement.appendChild(sorted);
|
||||
|
||||
Element empty = createElement(document, "empty", String.valueOf(sort.isEmpty()));
|
||||
sortElement.appendChild(empty);
|
||||
|
||||
rootElement.appendChild(sortElement);
|
||||
|
||||
Element offset = createElement(document, "offset", String.valueOf(pageable.getOffset()));
|
||||
rootElement.appendChild(offset);
|
||||
|
||||
Element pageSize = createElement(document, "pageSize", String.valueOf(pageable.getPageSize()));
|
||||
rootElement.appendChild(pageSize);
|
||||
|
||||
Element pageNumber = createElement(document, "pageNumber", String.valueOf(pageable.getPageNumber()));
|
||||
rootElement.appendChild(pageNumber);
|
||||
|
||||
Element paged = createElement(document, "paged", String.valueOf(pageable.isPaged()));
|
||||
rootElement.appendChild(paged);
|
||||
|
||||
Element unpaged = createElement(document, "unpaged", String.valueOf(pageable.isUnpaged()));
|
||||
rootElement.appendChild(unpaged);
|
||||
|
||||
String result = documentToString(document);
|
||||
// logger.info("========== Result ==========");
|
||||
// logger.info("{}", result);
|
||||
// logger.info("============================");
|
||||
return result;
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Recording xmlToRecording(String recordId, String xml) {
|
||||
try {
|
||||
setup();
|
||||
Document document = builder.parse(new ByteArrayInputStream(xml.getBytes()));
|
||||
Recording recording = parseRecordingDocument(document);
|
||||
|
||||
if (recording.getRecordId() == null || recording.getRecordId().equals(""))
|
||||
recording.setRecordId(recordId);
|
||||
|
||||
return recording;
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Recording parseRecordingDocument(Document recordingDocument) {
|
||||
String id = getNodeData(recordingDocument, "id");
|
||||
String state = getNodeData(recordingDocument, "state");
|
||||
String published = getNodeData(recordingDocument, "published");
|
||||
String startTime = getNodeData(recordingDocument, "start_time");
|
||||
String endTime = getNodeData(recordingDocument, "end_time");
|
||||
String participants = getNodeData(recordingDocument, "participants");
|
||||
String externalId = getNodeData(recordingDocument, "externalId");
|
||||
String name = getNodeData(recordingDocument, "name");
|
||||
|
||||
if (tagExists(recordingDocument, "meeting")) {
|
||||
Element meeting = (Element) recordingDocument.getElementsByTagName("meeting").item(0);
|
||||
externalId = meeting.getAttribute("externalId");
|
||||
name = meeting.getAttribute("name");
|
||||
if (id == null || id.equals(""))
|
||||
id = meeting.getAttribute("id");
|
||||
}
|
||||
|
||||
Recording recording = new Recording();
|
||||
recording.setRecordId(id);
|
||||
recording.setMeetingId(externalId);
|
||||
recording.setName(name);
|
||||
recording.setPublished(Boolean.parseBoolean(published));
|
||||
recording.setState(state);
|
||||
|
||||
try {
|
||||
recording.setStartTime(
|
||||
LocalDateTime.ofInstant(Instant.ofEpochMilli(Long.parseLong(startTime)), ZoneOffset.UTC));
|
||||
recording
|
||||
.setEndTime(LocalDateTime.ofInstant(Instant.ofEpochMilli(Long.parseLong(endTime)), ZoneOffset.UTC));
|
||||
recording.setParticipants(Integer.parseInt(participants));
|
||||
} catch (NumberFormatException e) {
|
||||
}
|
||||
|
||||
parseMetadata(recordingDocument, recording);
|
||||
PlaybackFormat playback = parsePlaybackFormat(recordingDocument);
|
||||
recording.setFormat(playback);
|
||||
playback.setRecording(recording);
|
||||
|
||||
logger.info("Finished constructing recording: {}", recording);
|
||||
|
||||
return recording;
|
||||
}
|
||||
|
||||
private void parseMetadata(Document recordingDocument, Recording recording) {
|
||||
Node meta = recordingDocument.getElementsByTagName("meta").item(0);
|
||||
NodeList children = meta.getChildNodes();
|
||||
|
||||
for (int i = 0; i < children.getLength(); i++) {
|
||||
Node node = children.item(i);
|
||||
|
||||
if (!(node instanceof Element))
|
||||
continue;
|
||||
|
||||
String key = node.getNodeName();
|
||||
String value = node.getTextContent();
|
||||
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setKey(key);
|
||||
metadata.setValue(value);
|
||||
|
||||
logger.info("Finished constructing metadata: {}", metadata);
|
||||
|
||||
recording.addMetadata(metadata);
|
||||
}
|
||||
}
|
||||
|
||||
private PlaybackFormat parsePlaybackFormat(Document recordingDocument) {
|
||||
PlaybackFormat playback = new PlaybackFormat();
|
||||
|
||||
String format = getNodeData(recordingDocument, "format");
|
||||
playback.setFormat(format);
|
||||
|
||||
String url = getNodeData(recordingDocument, "link");
|
||||
playback.setUrl(url);
|
||||
|
||||
String length = getNodeData(recordingDocument, "duration");
|
||||
String processingTime = getNodeData(recordingDocument, "processingTime");
|
||||
|
||||
try {
|
||||
playback.setLength(Integer.parseInt(length));
|
||||
playback.setProcessingTime(Integer.parseInt(processingTime));
|
||||
} catch (NumberFormatException e) {
|
||||
|
||||
}
|
||||
|
||||
NodeList images = recordingDocument.getElementsByTagName("image");
|
||||
|
||||
for (int i = 0; i < images.getLength(); i++) {
|
||||
Element image = (Element) images.item(i);
|
||||
|
||||
String height = image.getAttribute("height");
|
||||
String width = image.getAttribute("width");
|
||||
String alt = image.getAttribute("alt");
|
||||
String src = image.getTextContent();
|
||||
|
||||
Thumbnail thumbnail = new Thumbnail();
|
||||
|
||||
try {
|
||||
thumbnail.setHeight(Integer.parseInt(height));
|
||||
thumbnail.setWidth(Integer.parseInt(width));
|
||||
} catch (NumberFormatException e) {
|
||||
}
|
||||
|
||||
thumbnail.setAlt(alt);
|
||||
thumbnail.setUrl(src);
|
||||
thumbnail.setSequence(i);
|
||||
|
||||
logger.info("Finished constructing image: {}", image);
|
||||
|
||||
playback.addThumbnail(thumbnail);
|
||||
}
|
||||
|
||||
logger.info("Finished constructing playback format: {}", playback);
|
||||
|
||||
return playback;
|
||||
}
|
||||
|
||||
private void setup() throws ParserConfigurationException {
|
||||
if(factory == null) factory = DocumentBuilderFactory.newInstance();
|
||||
if(builder == null) builder = factory.newDocumentBuilder();
|
||||
}
|
||||
|
||||
private Element createElement(Document document, String name, String value) {
|
||||
Element element = document.createElement(name);
|
||||
if(value != null) element.setTextContent(value);
|
||||
return element;
|
||||
}
|
||||
|
||||
public String documentToString(Document document) {
|
||||
String output = null;
|
||||
|
||||
try {
|
||||
TransformerFactory factory = TransformerFactory.newInstance();
|
||||
Transformer transformer = factory.newTransformer();
|
||||
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
|
||||
transformer.setOutputProperty(OutputKeys.INDENT, "no");
|
||||
StringWriter writer = new StringWriter();
|
||||
transformer.transform(new DOMSource(document), new StreamResult(writer));
|
||||
output = writer.toString();
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private void appendFields(Document document, Element parent, Object object, String[] ignoredFields, Type type) throws IllegalAccessException {
|
||||
Field[] fields = object.getClass().getDeclaredFields();
|
||||
|
||||
for(Field field: fields) {
|
||||
if(Arrays.stream(ignoredFields).anyMatch(field.getName()::equals)) continue;
|
||||
field.setAccessible(true);
|
||||
Object fieldValue = field.get(object);
|
||||
if(fieldValue != null) {
|
||||
if(fieldValue instanceof LocalDateTime) {
|
||||
fieldValue = localDateTimeToEpoch((LocalDateTime) fieldValue);
|
||||
}
|
||||
|
||||
switch(type) {
|
||||
case CHILD:
|
||||
Element child = createElement(document, field.getName(), fieldValue.toString());
|
||||
parent.appendChild(child);
|
||||
break;
|
||||
case ATTRIBUTE:
|
||||
parent.setAttribute(field.getName(), fieldValue.toString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private String localDateTimeToEpoch(LocalDateTime localDateTime) {
|
||||
Instant instant = localDateTime.atZone(ZoneId.systemDefault()).toInstant();
|
||||
return String.valueOf(instant.toEpochMilli());
|
||||
}
|
||||
|
||||
private boolean tagExists(Document document, String tag) {
|
||||
NodeList node = document.getElementsByTagName(tag);
|
||||
if (node == null || node.getLength() == 0)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private String getNodeData(Document document, String tag) {
|
||||
String data = null;
|
||||
if (!tagExists(document, tag))
|
||||
return data;
|
||||
|
||||
NodeList node = document.getElementsByTagName(tag);
|
||||
Element element = (Element) node.item(0);
|
||||
Node child = element.getFirstChild();
|
||||
|
||||
if (child instanceof CharacterData) {
|
||||
CharacterData characterData = (CharacterData) child;
|
||||
data = characterData.getData();
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private enum Type {
|
||||
CHILD,
|
||||
ATTRIBUTE
|
||||
}
|
||||
}
|
262
bbb-common-web/src/main/java/org/bigbluebutton/api/util/DataStore.java
Executable file
262
bbb-common-web/src/main/java/org/bigbluebutton/api/util/DataStore.java
Executable file
@ -0,0 +1,262 @@
|
||||
package org.bigbluebutton.api.util;
|
||||
|
||||
import org.bigbluebutton.api.model.entity.*;
|
||||
import org.hibernate.Session;
|
||||
import org.hibernate.SessionFactory;
|
||||
import org.hibernate.Transaction;
|
||||
import org.hibernate.cfg.Configuration;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.persistence.criteria.CriteriaBuilder;
|
||||
import javax.persistence.criteria.CriteriaQuery;
|
||||
import javax.persistence.criteria.Predicate;
|
||||
import javax.persistence.criteria.Root;
|
||||
import javax.persistence.*;
|
||||
import java.util.List;
|
||||
|
||||
public class DataStore {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DataStore.class);
|
||||
|
||||
private SessionFactory sessionFactory;
|
||||
private static DataStore instance;
|
||||
|
||||
private DataStore() {
|
||||
openConnection();
|
||||
}
|
||||
|
||||
private void openConnection() {
|
||||
sessionFactory = new Configuration()
|
||||
.configure()
|
||||
.addAnnotatedClass(Recording.class)
|
||||
.addAnnotatedClass(Metadata.class)
|
||||
.addAnnotatedClass(PlaybackFormat.class)
|
||||
.addAnnotatedClass(Thumbnail.class)
|
||||
.addAnnotatedClass(CallbackData.class)
|
||||
.buildSessionFactory();
|
||||
}
|
||||
|
||||
public static DataStore getInstance() {
|
||||
if(instance == null) {
|
||||
instance = new DataStore();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public <T> void save(T entity) {
|
||||
logger.info("Attempting to save {}", entity);
|
||||
Session session = sessionFactory.openSession();
|
||||
Transaction transaction = null;
|
||||
|
||||
try {
|
||||
transaction = session.beginTransaction();
|
||||
session.saveOrUpdate(entity);
|
||||
transaction.commit();
|
||||
} catch(Exception e) {
|
||||
if(transaction != null) {
|
||||
transaction.rollback();
|
||||
e.printStackTrace();
|
||||
}
|
||||
} finally {
|
||||
session.close();
|
||||
}
|
||||
}
|
||||
|
||||
public <T> T find(String id, Class<T> entityClass) {
|
||||
logger.info("Attempting to find {} with ID {}", entityClass.getSimpleName(), id);
|
||||
Session session = sessionFactory.openSession();
|
||||
Transaction transaction = null;
|
||||
T result = null;
|
||||
|
||||
try {
|
||||
transaction = session.beginTransaction();
|
||||
result = session.find(entityClass, Long.parseLong(id));
|
||||
transaction.commit();
|
||||
} catch(Exception e) {
|
||||
if(transaction != null) {
|
||||
transaction.rollback();
|
||||
|
||||
if(e instanceof NoResultException) logger.info("No result found.");
|
||||
else e.printStackTrace();
|
||||
}
|
||||
} finally {
|
||||
session.close();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public <T> List<T> findAll(Class<T> entityClass) {
|
||||
logger.info("Attempting to fetch all {}", entityClass.getSimpleName());
|
||||
Session session = sessionFactory.openSession();
|
||||
Transaction transaction = null;
|
||||
List<T> result = null;
|
||||
|
||||
try {
|
||||
transaction = session.beginTransaction();
|
||||
CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder();
|
||||
CriteriaQuery<T> criteriaQuery = criteriaBuilder.createQuery(entityClass);
|
||||
Root<T> root = criteriaQuery.from(entityClass);
|
||||
CriteriaQuery<T> allEntities = criteriaQuery.select(root);
|
||||
result = session.createQuery(allEntities).getResultList();
|
||||
} catch(Exception e) {
|
||||
if(transaction != null) {
|
||||
transaction.rollback();
|
||||
|
||||
if(e instanceof NoResultException) logger.info("No result found.");
|
||||
else e.printStackTrace();
|
||||
}
|
||||
} finally {
|
||||
session.close();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public Recording findRecordingByRecordId(String recordId) {
|
||||
logger.info("Attempting to find recording with recordId {}", recordId);
|
||||
Session session = sessionFactory.openSession();
|
||||
Transaction transaction = null;
|
||||
Recording result = null;
|
||||
|
||||
try {
|
||||
transaction = session.beginTransaction();
|
||||
CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder();
|
||||
CriteriaQuery<Recording> criteriaQuery = criteriaBuilder.createQuery(Recording.class);
|
||||
Root<Recording> recordingRoot = criteriaQuery.from(Recording.class);
|
||||
criteriaQuery.where(criteriaBuilder.equal(recordingRoot.get("recordId"), recordId));
|
||||
result = session.createQuery(criteriaQuery).getSingleResult();
|
||||
transaction.commit();
|
||||
} catch(Exception e) {
|
||||
if(transaction != null) {
|
||||
transaction.rollback();
|
||||
|
||||
if(e instanceof NoResultException) logger.info("No result found.");
|
||||
else e.printStackTrace();
|
||||
}
|
||||
} finally {
|
||||
session.close();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<Recording> findRecordingsByMeetingId(String meetingId) {
|
||||
logger.info("Attempting to find recordings with meetingID {}", meetingId);
|
||||
Session session = sessionFactory.openSession();
|
||||
Transaction transaction = null;
|
||||
List<Recording> result = null;
|
||||
|
||||
try {
|
||||
transaction = session.beginTransaction();
|
||||
CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder();
|
||||
CriteriaQuery<Recording> criteriaQuery = criteriaBuilder.createQuery(Recording.class);
|
||||
Root<Recording> recordingRoot = criteriaQuery.from(Recording.class);
|
||||
criteriaQuery.where(criteriaBuilder.equal(recordingRoot.get("meetingId"), meetingId));
|
||||
result = session.createQuery(criteriaQuery).getResultList();
|
||||
transaction.commit();
|
||||
} catch(Exception e) {
|
||||
if(transaction != null) {
|
||||
transaction.rollback();
|
||||
|
||||
if(e instanceof NoResultException) logger.info("No results found.");
|
||||
else e.printStackTrace();
|
||||
}
|
||||
} finally {
|
||||
session.close();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<Recording> findRecordingsByState(String state) {
|
||||
logger.info("Attempting to find recordings with state {}", state);
|
||||
Session session = sessionFactory.openSession();
|
||||
Transaction transaction = null;
|
||||
List<Recording> result = null;
|
||||
|
||||
try {
|
||||
transaction = session.beginTransaction();
|
||||
CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder();
|
||||
CriteriaQuery<Recording> criteriaQuery = criteriaBuilder.createQuery(Recording.class);
|
||||
Root<Recording> recordingRoot = criteriaQuery.from(Recording.class);
|
||||
criteriaQuery.where(criteriaBuilder.equal(recordingRoot.get("state"), state));
|
||||
result = session.createQuery(criteriaQuery).getResultList();
|
||||
transaction.commit();
|
||||
} catch(Exception e) {
|
||||
if(transaction != null) {
|
||||
transaction.rollback();
|
||||
|
||||
if(e instanceof NoResultException) logger.info("No results found.");
|
||||
else e.printStackTrace();
|
||||
}
|
||||
} finally {
|
||||
session.close();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<Metadata> findMetadataByFilter(String key, String value) {
|
||||
logger.info("Attempting to find metadata with key {} and value {}", key, value);
|
||||
Session session = sessionFactory.openSession();
|
||||
Transaction transaction = null;
|
||||
List<Metadata> result = null;
|
||||
|
||||
try {
|
||||
transaction = session.beginTransaction();
|
||||
CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder();
|
||||
CriteriaQuery<Metadata> criteriaQuery = criteriaBuilder.createQuery(Metadata.class);
|
||||
Root<Metadata> metadataRoot = criteriaQuery.from(Metadata.class);
|
||||
Predicate predicateForKey = criteriaBuilder.equal(metadataRoot.get("key"), key);
|
||||
Predicate predicateForValue = criteriaBuilder.equal(metadataRoot.get("value"), value);
|
||||
criteriaQuery.where(criteriaBuilder.and(predicateForKey, predicateForValue));
|
||||
result = session.createQuery(criteriaQuery).getResultList();
|
||||
transaction.commit();
|
||||
} catch(Exception e) {
|
||||
if(transaction != null) {
|
||||
transaction.rollback();
|
||||
|
||||
if(e instanceof NoResultException) logger.info("No result found.");
|
||||
else e.printStackTrace();
|
||||
}
|
||||
} finally {
|
||||
session.close();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public <T> void delete(T entity) {
|
||||
logger.info("Attempting to delete {}", entity);
|
||||
Session session = sessionFactory.openSession();
|
||||
Transaction transaction = null;
|
||||
|
||||
try {
|
||||
transaction = session.beginTransaction();
|
||||
session.delete(entity);
|
||||
transaction.commit();
|
||||
} catch(Exception e) {
|
||||
if(transaction != null) {
|
||||
transaction.rollback();
|
||||
e.printStackTrace();
|
||||
}
|
||||
} finally {
|
||||
session.close();
|
||||
}
|
||||
}
|
||||
|
||||
public void truncateTables() {
|
||||
logger.info("Attempting to truncate tables");
|
||||
|
||||
List<Recording> recordings = findAll(Recording.class);
|
||||
|
||||
if(recordings != null) {
|
||||
for(Recording recording: recordings) {
|
||||
delete(recording);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -27,8 +27,8 @@ public class HTML5ProcessLine {
|
||||
|
||||
public HTML5ProcessLine(String input) {
|
||||
// $ ps -u meteor -o pcpu,cmd= | grep NODEJS_BACKEND_INSTANCE_ID
|
||||
// 1.1 /usr/share/node-v12.16.1-linux-x64/bin/node --max-old-space-size=2048 --max_semi_space_size=128 main.js NODEJS_BACKEND_INSTANCE_ID=1
|
||||
// 1.0 /usr/share/node-v12.16.1-linux-x64/bin/node --max-old-space-size=2048 --max_semi_space_size=128 main.js NODEJS_BACKEND_INSTANCE_ID=2
|
||||
// 1.1 /usr/lib/bbb-html5/node/bin/node --max-old-space-size=2048 --max_semi_space_size=128 main.js NODEJS_BACKEND_INSTANCE_ID=1
|
||||
// 1.0 /usr/lib/bbb-html5/node/bin/node --max-old-space-size=2048 --max_semi_space_size=128 main.js NODEJS_BACKEND_INSTANCE_ID=2
|
||||
|
||||
String[] a = input.trim().split(" ");
|
||||
this.percentageCPU = Double.parseDouble(a[0]);
|
||||
|
28
bbb-common-web/src/main/resources/hibernate.cfg.xml
Normal file
28
bbb-common-web/src/main/resources/hibernate.cfg.xml
Normal file
@ -0,0 +1,28 @@
|
||||
<!DOCTYPE hibernate-configuration PUBLIC
|
||||
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
|
||||
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
|
||||
<hibernate-configuration>
|
||||
<session-factory>
|
||||
<!-- JDBC Database connection settings -->
|
||||
<property name="connection.driver_class">org.postgresql.Driver</property>
|
||||
<property name="connection.url">jdbc:postgresql://localhost:5432/bbb</property>
|
||||
<property name="connection.username">bbb</property>
|
||||
<property name="connection.password">bbb123</property>
|
||||
<!-- JDBC connection pool settings -->
|
||||
<property name="hibernate.connection.provider_class">com.zaxxer.hikari.hibernate.HikariConnectionProvider</property>
|
||||
<property name="hibernate.hikari.minimumIdle">5</property>
|
||||
<property name="hibernate.hikari.maximumPoolSize">10</property>
|
||||
<property name="hibernate.hikari.idleTimeout">30000</property>
|
||||
<!-- Select our SQL dialect -->
|
||||
<property name="dialect">org.hibernate.dialect.PostgreSQL10Dialect</property>
|
||||
<!-- Echo the SQL to stdout -->
|
||||
<property name="show_sql">true</property>
|
||||
<!-- Set the current session context -->
|
||||
<property name="current_session_context_class">thread</property>
|
||||
<property name="hibernate.show_sql">false</property>
|
||||
<!-- format the sql nice -->
|
||||
<property name="hibernate.format_sql">false</property>
|
||||
<!-- show the hql as comment -->
|
||||
<property name="use_sql_comments">false</property>
|
||||
</session-factory>
|
||||
</hibernate-configuration>
|
1
bbb-export-annotations/.gitignore
vendored
Normal file
1
bbb-export-annotations/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
node_modules/
|
3
bbb-export-annotations/config/index.js
Normal file
3
bbb-export-annotations/config/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
const settings = require('./settings');
|
||||
const config = settings;
|
||||
module.exports = config;
|
38
bbb-export-annotations/config/settings.json
Normal file
38
bbb-export-annotations/config/settings.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "info"
|
||||
},
|
||||
"shared": {
|
||||
"presDir": "/var/bigbluebutton",
|
||||
"presAnnDropboxDir": "/tmp/pres-ann-dropbox"
|
||||
},
|
||||
"collector": {
|
||||
"backgroundSlideDPI": 300,
|
||||
"backgroundSlidePPI": 200
|
||||
},
|
||||
"process": {
|
||||
"whiteboardTextEncoding": "utf-8",
|
||||
"pointsPerInch": 72,
|
||||
"pixelsPerInch": 96
|
||||
},
|
||||
"notifier": {
|
||||
"pod_id": "DEFAULT_PRESENTATION_POD",
|
||||
"is_downloadable": "false",
|
||||
"msgName": "NewPresAnnFileAvailableMsg",
|
||||
"protocol": "https",
|
||||
"host": "localhost"
|
||||
},
|
||||
"bbbWeb": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 8090
|
||||
},
|
||||
"redis": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 6379,
|
||||
"password": null,
|
||||
"channels": {
|
||||
"queue": "exportJobs",
|
||||
"publish": "to-akka-apps-redis-channel"
|
||||
}
|
||||
}
|
||||
}
|
51
bbb-export-annotations/lib/utils/logger.js
Normal file
51
bbb-export-annotations/lib/utils/logger.js
Normal file
@ -0,0 +1,51 @@
|
||||
const config = require('../../config');
|
||||
|
||||
const { level } = config.log;
|
||||
const trace = level.toLowerCase() === 'trace';
|
||||
const debug = trace || level.toLowerCase() === 'debug';
|
||||
|
||||
const date = () => new Date().toISOString();
|
||||
|
||||
const parse = (messages) => {
|
||||
return messages.map(message => {
|
||||
if (typeof message === 'object') return JSON.stringify(message);
|
||||
|
||||
return message;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = class Logger {
|
||||
constructor(context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
trace(...messages) {
|
||||
if (trace) {
|
||||
console.log(date(), 'TRACE\t', `[${this.context}]`, ...parse(messages));
|
||||
}
|
||||
}
|
||||
|
||||
debug(...messages) {
|
||||
if (debug) {
|
||||
console.log(date(), 'DEBUG\t', `[${this.context}]`, ...parse(messages));
|
||||
}
|
||||
}
|
||||
|
||||
info(...messages) {
|
||||
console.log(date(), 'INFO\t', `[${this.context}]`, ...parse(messages));
|
||||
}
|
||||
|
||||
warn(...messages) {
|
||||
if (debug) {
|
||||
console.log(date(), 'WARN\t', `[${this.context}]`, ...parse(messages));
|
||||
}
|
||||
}
|
||||
|
||||
error(...messages) {
|
||||
console.log(date(), 'ERROR\t', `[${this.context}]`, ...parse(messages));
|
||||
}
|
||||
|
||||
fatal(...messages) {
|
||||
console.log(date(), 'FATAL\t', `[${this.context}]`, ...parse(messages));
|
||||
}
|
||||
};
|
62
bbb-export-annotations/master.js
Normal file
62
bbb-export-annotations/master.js
Normal file
@ -0,0 +1,62 @@
|
||||
const Logger = require('./lib/utils/logger');
|
||||
const config = require('./config');
|
||||
const fs = require('fs');
|
||||
const redis = require('redis');
|
||||
const { commandOptions } = require('redis');
|
||||
const { Worker } = require('worker_threads');
|
||||
const path = require('path');
|
||||
|
||||
const logger = new Logger('presAnn Master');
|
||||
logger.info("Running bbb-export-annotations");
|
||||
|
||||
const kickOffCollectorWorker = (jobId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new Worker(path.join(__dirname, 'workers', 'collector.js'), { workerData: jobId });
|
||||
worker.on('message', resolve);
|
||||
worker.on('error', reject);
|
||||
worker.on('exit', (code) => {
|
||||
if (code !== 0)
|
||||
reject(new Error(`PresAnn Collector Worker stopped with exit code ${code}`));
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const client = redis.createClient({
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
password: config.redis.password
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
client.on('error', (err) => logger.info('Redis Client Error', err));
|
||||
|
||||
async function waitForJobs () {
|
||||
const queue = client.blPop(
|
||||
commandOptions({ isolated: true }),
|
||||
config.redis.channels.queue,
|
||||
0
|
||||
);
|
||||
|
||||
let job = await queue;
|
||||
|
||||
logger.info('Received job', job.element);
|
||||
const exportJob = JSON.parse(job.element);
|
||||
|
||||
// Create folder in dropbox
|
||||
let dropbox = path.join(config.shared.presAnnDropboxDir, exportJob.jobId);
|
||||
fs.mkdirSync(dropbox, { recursive: true })
|
||||
|
||||
// Drop job into dropbox as JSON
|
||||
fs.writeFile(path.join(dropbox, 'job'), job.element, function(err) {
|
||||
if(err) { return logger.error(err); }
|
||||
});
|
||||
|
||||
kickOffCollectorWorker(exportJob.jobId)
|
||||
|
||||
waitForJobs();
|
||||
}
|
||||
|
||||
waitForJobs();
|
||||
})();
|
745
bbb-export-annotations/package-lock.json
generated
Normal file
745
bbb-export-annotations/package-lock.json
generated
Normal file
@ -0,0 +1,745 @@
|
||||
{
|
||||
"name": "bbb-export-annotations",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bbb-export-annotations",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"axios": "^0.26.0",
|
||||
"form-data": "^4.0.0",
|
||||
"perfect-freehand": "^1.0.16",
|
||||
"probe-image-size": "^7.2.3",
|
||||
"redis": "^4.0.3",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"xmlbuilder2": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-redis/bloom": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@node-redis/bloom/-/bloom-1.0.1.tgz",
|
||||
"integrity": "sha512-mXEBvEIgF4tUzdIN89LiYsbi6//EdpFA7L8M+DHCvePXg+bfHWi+ct5VI6nHUFQE5+ohm/9wmgihCH3HSkeKsw==",
|
||||
"peerDependencies": {
|
||||
"@node-redis/client": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-redis/client": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@node-redis/client/-/client-1.0.3.tgz",
|
||||
"integrity": "sha512-IXNgOG99PHGL3NxN3/e8J8MuX+H08I+OMNmheGmZBXngE0IntaCQwwrd7NzmiHA+zH3SKHiJ+6k3P7t7XYknMw==",
|
||||
"dependencies": {
|
||||
"cluster-key-slot": "1.1.0",
|
||||
"generic-pool": "3.8.2",
|
||||
"redis-parser": "3.0.0",
|
||||
"yallist": "4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-redis/graph": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@node-redis/graph/-/graph-1.0.0.tgz",
|
||||
"integrity": "sha512-mRSo8jEGC0cf+Rm7q8mWMKKKqkn6EAnA9IA2S3JvUv/gaWW/73vil7GLNwion2ihTptAm05I9LkepzfIXUKX5g==",
|
||||
"peerDependencies": {
|
||||
"@node-redis/client": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-redis/json": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@node-redis/json/-/json-1.0.2.tgz",
|
||||
"integrity": "sha512-qVRgn8WfG46QQ08CghSbY4VhHFgaTY71WjpwRBGEuqGPfWwfRcIf3OqSpR7Q/45X+v3xd8mvYjywqh0wqJ8T+g==",
|
||||
"peerDependencies": {
|
||||
"@node-redis/client": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-redis/search": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@node-redis/search/-/search-1.0.2.tgz",
|
||||
"integrity": "sha512-gWhEeji+kTAvzZeguUNJdMSZNH2c5dv3Bci8Nn2f7VGuf6IvvwuZDSBOuOlirLVgayVuWzAG7EhwaZWK1VDnWQ==",
|
||||
"peerDependencies": {
|
||||
"@node-redis/client": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-redis/time-series": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@node-redis/time-series/-/time-series-1.0.1.tgz",
|
||||
"integrity": "sha512-+nTn6EewVj3GlUXPuD3dgheWqo219jTxlo6R+pg24OeVvFHx9aFGGiyOgj3vBPhWUdRZ0xMcujXV5ki4fbLyMw==",
|
||||
"peerDependencies": {
|
||||
"@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==",
|
||||
"dependencies": {
|
||||
"@oozcitak/infra": "1.0.8",
|
||||
"@oozcitak/url": "1.0.4",
|
||||
"@oozcitak/util": "8.3.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.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/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz",
|
||||
"integrity": "sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.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",
|
||||
"integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
|
||||
"engines": {
|
||||
"node": ">=0.4.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/follow-redirects": {
|
||||
"version": "1.14.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
|
||||
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/generic-pool": {
|
||||
"version": "3.8.2",
|
||||
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.8.2.tgz",
|
||||
"integrity": "sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg==",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.51.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
|
||||
"integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.34",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz",
|
||||
"integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==",
|
||||
"dependencies": {
|
||||
"mime-db": "1.51.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/needle": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz",
|
||||
"integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==",
|
||||
"dependencies": {
|
||||
"debug": "^3.2.6",
|
||||
"iconv-lite": "^0.4.4",
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"bin": {
|
||||
"needle": "bin/needle"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/perfect-freehand": {
|
||||
"version": "1.0.16",
|
||||
"resolved": "https://registry.npmjs.org/perfect-freehand/-/perfect-freehand-1.0.16.tgz",
|
||||
"integrity": "sha512-D4+avUeR8CHSl2vaPbPYX/dNpSMRYO3VOFp7qSSc+LRkSgzQbLATVnXosy7VxtsSHEh1C5t8K8sfmo0zCVnfWQ=="
|
||||
},
|
||||
"node_modules/probe-image-size": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/probe-image-size/-/probe-image-size-7.2.3.tgz",
|
||||
"integrity": "sha512-HubhG4Rb2UH8YtV4ba0Vp5bQ7L78RTONYu/ujmCu5nBI8wGv24s4E9xSKBi0N1MowRpxk76pFCpJtW0KPzOK0w==",
|
||||
"dependencies": {
|
||||
"lodash.merge": "^4.6.2",
|
||||
"needle": "^2.5.2",
|
||||
"stream-parser": "~0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/redis": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/redis/-/redis-4.0.3.tgz",
|
||||
"integrity": "sha512-SJMRXvgiQUYN0HaWwWv002J5ZgkhYXOlbLomzcrL3kP42yRNZ8Jx5nvLYhVpgmf10xcDpanFOxxJkphu2eyIFQ==",
|
||||
"dependencies": {
|
||||
"@node-redis/bloom": "1.0.1",
|
||||
"@node-redis/client": "1.0.3",
|
||||
"@node-redis/graph": "1.0.0",
|
||||
"@node-redis/json": "1.0.2",
|
||||
"@node-redis/search": "1.0.2",
|
||||
"@node-redis/time-series": "1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/redis-errors": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
||||
"integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/redis-parser": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
|
||||
"integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=",
|
||||
"dependencies": {
|
||||
"redis-errors": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"node_modules/sanitize-filename": {
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
|
||||
"integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==",
|
||||
"dependencies": {
|
||||
"truncate-utf8-bytes": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
|
||||
},
|
||||
"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",
|
||||
"integrity": "sha1-FhhUhpRCACGhGC/wrxkRwSl2F3M=",
|
||||
"dependencies": {
|
||||
"debug": "2"
|
||||
}
|
||||
},
|
||||
"node_modules/stream-parser/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stream-parser/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"node_modules/truncate-utf8-bytes": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
|
||||
"integrity": "sha1-QFkjkJWS1W94pYGENLC3hInKXys=",
|
||||
"dependencies": {
|
||||
"utf8-byte-length": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/utf8-byte-length": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz",
|
||||
"integrity": "sha1-9F8VDExm7uloGGUFq5P8u4rWv2E="
|
||||
},
|
||||
"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",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@node-redis/bloom": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@node-redis/bloom/-/bloom-1.0.1.tgz",
|
||||
"integrity": "sha512-mXEBvEIgF4tUzdIN89LiYsbi6//EdpFA7L8M+DHCvePXg+bfHWi+ct5VI6nHUFQE5+ohm/9wmgihCH3HSkeKsw==",
|
||||
"requires": {}
|
||||
},
|
||||
"@node-redis/client": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@node-redis/client/-/client-1.0.3.tgz",
|
||||
"integrity": "sha512-IXNgOG99PHGL3NxN3/e8J8MuX+H08I+OMNmheGmZBXngE0IntaCQwwrd7NzmiHA+zH3SKHiJ+6k3P7t7XYknMw==",
|
||||
"requires": {
|
||||
"cluster-key-slot": "1.1.0",
|
||||
"generic-pool": "3.8.2",
|
||||
"redis-parser": "3.0.0",
|
||||
"yallist": "4.0.0"
|
||||
}
|
||||
},
|
||||
"@node-redis/graph": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@node-redis/graph/-/graph-1.0.0.tgz",
|
||||
"integrity": "sha512-mRSo8jEGC0cf+Rm7q8mWMKKKqkn6EAnA9IA2S3JvUv/gaWW/73vil7GLNwion2ihTptAm05I9LkepzfIXUKX5g==",
|
||||
"requires": {}
|
||||
},
|
||||
"@node-redis/json": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@node-redis/json/-/json-1.0.2.tgz",
|
||||
"integrity": "sha512-qVRgn8WfG46QQ08CghSbY4VhHFgaTY71WjpwRBGEuqGPfWwfRcIf3OqSpR7Q/45X+v3xd8mvYjywqh0wqJ8T+g==",
|
||||
"requires": {}
|
||||
},
|
||||
"@node-redis/search": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@node-redis/search/-/search-1.0.2.tgz",
|
||||
"integrity": "sha512-gWhEeji+kTAvzZeguUNJdMSZNH2c5dv3Bci8Nn2f7VGuf6IvvwuZDSBOuOlirLVgayVuWzAG7EhwaZWK1VDnWQ==",
|
||||
"requires": {}
|
||||
},
|
||||
"@node-redis/time-series": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@node-redis/time-series/-/time-series-1.0.1.tgz",
|
||||
"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==",
|
||||
"requires": {
|
||||
"@oozcitak/infra": "1.0.8",
|
||||
"@oozcitak/url": "1.0.4",
|
||||
"@oozcitak/util": "8.3.8"
|
||||
}
|
||||
},
|
||||
"@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=="
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz",
|
||||
"integrity": "sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.8"
|
||||
}
|
||||
},
|
||||
"cluster-key-slot": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz",
|
||||
"integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw=="
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"requires": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
|
||||
},
|
||||
"esprima": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.14.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
|
||||
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w=="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"generic-pool": {
|
||||
"version": "3.8.2",
|
||||
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.8.2.tgz",
|
||||
"integrity": "sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg=="
|
||||
},
|
||||
"iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"requires": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
}
|
||||
},
|
||||
"lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.51.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
|
||||
"integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g=="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.34",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz",
|
||||
"integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==",
|
||||
"requires": {
|
||||
"mime-db": "1.51.0"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"needle": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz",
|
||||
"integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==",
|
||||
"requires": {
|
||||
"debug": "^3.2.6",
|
||||
"iconv-lite": "^0.4.4",
|
||||
"sax": "^1.2.4"
|
||||
}
|
||||
},
|
||||
"perfect-freehand": {
|
||||
"version": "1.0.16",
|
||||
"resolved": "https://registry.npmjs.org/perfect-freehand/-/perfect-freehand-1.0.16.tgz",
|
||||
"integrity": "sha512-D4+avUeR8CHSl2vaPbPYX/dNpSMRYO3VOFp7qSSc+LRkSgzQbLATVnXosy7VxtsSHEh1C5t8K8sfmo0zCVnfWQ=="
|
||||
},
|
||||
"probe-image-size": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/probe-image-size/-/probe-image-size-7.2.3.tgz",
|
||||
"integrity": "sha512-HubhG4Rb2UH8YtV4ba0Vp5bQ7L78RTONYu/ujmCu5nBI8wGv24s4E9xSKBi0N1MowRpxk76pFCpJtW0KPzOK0w==",
|
||||
"requires": {
|
||||
"lodash.merge": "^4.6.2",
|
||||
"needle": "^2.5.2",
|
||||
"stream-parser": "~0.3.1"
|
||||
}
|
||||
},
|
||||
"redis": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/redis/-/redis-4.0.3.tgz",
|
||||
"integrity": "sha512-SJMRXvgiQUYN0HaWwWv002J5ZgkhYXOlbLomzcrL3kP42yRNZ8Jx5nvLYhVpgmf10xcDpanFOxxJkphu2eyIFQ==",
|
||||
"requires": {
|
||||
"@node-redis/bloom": "1.0.1",
|
||||
"@node-redis/client": "1.0.3",
|
||||
"@node-redis/graph": "1.0.0",
|
||||
"@node-redis/json": "1.0.2",
|
||||
"@node-redis/search": "1.0.2",
|
||||
"@node-redis/time-series": "1.0.1"
|
||||
}
|
||||
},
|
||||
"redis-errors": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
||||
"integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60="
|
||||
},
|
||||
"redis-parser": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
|
||||
"integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=",
|
||||
"requires": {
|
||||
"redis-errors": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"sanitize-filename": {
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
|
||||
"integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==",
|
||||
"requires": {
|
||||
"truncate-utf8-bytes": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
|
||||
},
|
||||
"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",
|
||||
"integrity": "sha1-FhhUhpRCACGhGC/wrxkRwSl2F3M=",
|
||||
"requires": {
|
||||
"debug": "2"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"truncate-utf8-bytes": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
|
||||
"integrity": "sha1-QFkjkJWS1W94pYGENLC3hInKXys=",
|
||||
"requires": {
|
||||
"utf8-byte-length": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"utf8-byte-length": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz",
|
||||
"integrity": "sha1-9F8VDExm7uloGGUFq5P8u4rWv2E="
|
||||
},
|
||||
"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",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
}
|
||||
}
|
||||
}
|
17
bbb-export-annotations/package.json
Normal file
17
bbb-export-annotations/package.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "bbb-export-annotations",
|
||||
"version": "0.0.1",
|
||||
"description": "BigBlueButton's Presentation Annotation Exporter",
|
||||
"scripts": {
|
||||
"start": "node master.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.26.0",
|
||||
"form-data": "^4.0.0",
|
||||
"perfect-freehand": "^1.0.16",
|
||||
"probe-image-size": "^7.2.3",
|
||||
"redis": "^4.0.3",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"xmlbuilder2": "^3.0.2"
|
||||
}
|
||||
}
|
110
bbb-export-annotations/workers/collector.js
Normal file
110
bbb-export-annotations/workers/collector.js
Normal file
@ -0,0 +1,110 @@
|
||||
const Logger = require('../lib/utils/logger');
|
||||
const config = require('../config');
|
||||
const fs = require('fs');
|
||||
const redis = require('redis');
|
||||
const { Worker, workerData, parentPort } = require('worker_threads');
|
||||
const path = require('path');
|
||||
const probe = require('probe-image-size');
|
||||
const { execSync } = require("child_process");
|
||||
|
||||
const jobId = workerData;
|
||||
|
||||
const logger = new Logger('presAnn Collector');
|
||||
logger.info("Collecting job " + jobId);
|
||||
|
||||
const kickOffProcessWorker = (jobId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new Worker('./workers/process.js', { workerData: jobId });
|
||||
worker.on('message', resolve);
|
||||
worker.on('error', reject);
|
||||
worker.on('exit', (code) => {
|
||||
if (code !== 0)
|
||||
reject(new Error(`PresAnn Process Worker stopped with exit code ${code}`));
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const dropbox = path.join(config.shared.presAnnDropboxDir, jobId);
|
||||
|
||||
// Takes the Job from the dropbox
|
||||
let job = fs.readFileSync(path.join(dropbox, 'job'));
|
||||
let exportJob = JSON.parse(job);
|
||||
|
||||
// Collect the annotations from Redis
|
||||
(async () => {
|
||||
const client = redis.createClient({
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
password: config.redis.password
|
||||
});
|
||||
|
||||
client.on('error', (err) => logger.info('Redis Client Error', err));
|
||||
|
||||
await client.connect();
|
||||
|
||||
let presAnn = await client.hGetAll(exportJob.jobId);
|
||||
|
||||
// Remove annotations from Redis
|
||||
await client.DEL(jobId);
|
||||
|
||||
client.disconnect();
|
||||
|
||||
let annotations = JSON.stringify(presAnn);
|
||||
|
||||
let whiteboard = JSON.parse(annotations);
|
||||
let pages = JSON.parse(whiteboard.pages);
|
||||
|
||||
fs.writeFile(path.join(dropbox, 'whiteboard'), annotations, function(err) {
|
||||
if(err) { return logger.error(err); }
|
||||
});
|
||||
|
||||
// Collect the Presentation Page files (PDF / PNG / JPEG) from the presentation directory
|
||||
let presentationFile = path.join(exportJob.presLocation, exportJob.presId);
|
||||
let pdfFile = `${presentationFile}.pdf`
|
||||
|
||||
if (fs.existsSync(pdfFile)) {
|
||||
|
||||
for (let p of pages) {
|
||||
let pageNumber = p.page;
|
||||
let svgFile = path.join(exportJob.presLocation, 'svgs', `slide${pageNumber}.svg`)
|
||||
let outputFile = path.join(dropbox, `slide${pageNumber}`);
|
||||
|
||||
// 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 -- Tldraw unfortunately uses absolute coordinates.
|
||||
|
||||
let extract_png_from_pdf = [
|
||||
'pdftocairo',
|
||||
'-png',
|
||||
'-f', pageNumber,
|
||||
'-l', pageNumber,
|
||||
'-r', config.collector.backgroundSlidePPI,
|
||||
'-singlefile',
|
||||
'-cropbox',
|
||||
pdfFile, outputFile,
|
||||
].join(' ')
|
||||
|
||||
execSync(extract_png_from_pdf);
|
||||
fs.copyFileSync(svgFile, path.join(dropbox, `slide${pageNumber}.svg`));
|
||||
}
|
||||
}
|
||||
|
||||
// If PNG file already available
|
||||
else if (fs.existsSync(`${presentationFile}.png`)) {
|
||||
fs.copyFileSync(`${presentationFile}.png`, path.join(dropbox, 'slide1.png'));
|
||||
}
|
||||
|
||||
// If JPEG file available
|
||||
else if (fs.existsSync(`${presentationFile}.jpeg`)) {
|
||||
fs.copyFileSync(`${presentationFile}.jpeg`, path.join(dropbox, 'slide1.jpeg'));
|
||||
}
|
||||
|
||||
else {
|
||||
return logger.error(`Could not find whiteboard presentation file for job ${exportJob.jobId}`);
|
||||
}
|
||||
|
||||
kickOffProcessWorker(exportJob.jobId);
|
||||
})()
|
||||
|
||||
parentPort.postMessage({ message: workerData })
|
80
bbb-export-annotations/workers/notifier.js
Normal file
80
bbb-export-annotations/workers/notifier.js
Normal file
@ -0,0 +1,80 @@
|
||||
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 { workerData, parentPort } = require('worker_threads')
|
||||
|
||||
const [jobType, jobId, filename] = workerData;
|
||||
|
||||
const logger = new Logger('presAnn Notifier Worker');
|
||||
|
||||
const dropbox = `${config.shared.presAnnDropboxDir}/${jobId}`
|
||||
let job = fs.readFileSync(path.join(dropbox, 'job'));
|
||||
let exportJob = JSON.parse(job);
|
||||
|
||||
async function notifyMeetingActor() {
|
||||
const client = redis.createClient({
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
password: config.redis.password
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
client.on('error', (err) => logger.info('Redis Client Error', err));
|
||||
|
||||
let link = `${config.notifier.protocol}://${config.notifier.host}/bigbluebutton/presentation/${exportJob.parentMeetingId}/${exportJob.parentMeetingId}/${exportJob.presId}/pdf/${jobId}/${filename}.pdf`;
|
||||
// Notify Meeting Actor of file availability by sending a message through Redis PubSub
|
||||
const notification = {
|
||||
envelope: {
|
||||
name: config.notifier.msgName,
|
||||
routing: {
|
||||
sender: exportJob.module
|
||||
},
|
||||
timestamp: (new Date()).getTime(),
|
||||
},
|
||||
core: {
|
||||
header: {
|
||||
name: config.notifier.msgName,
|
||||
meetingId: exportJob.parentMeetingId,
|
||||
userId: ""
|
||||
},
|
||||
body: {
|
||||
fileURI: link
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Annotated PDF available at ${link}`);
|
||||
await client.publish(config.redis.channels.publish, JSON.stringify(notification));
|
||||
client.disconnect();
|
||||
}
|
||||
|
||||
async function upload(exportJob) {
|
||||
let callbackUrl = `http://${config.bbbWeb.host}:${config.bbbWeb.port}/bigbluebutton/presentation/${exportJob.presentationUploadToken}/upload`
|
||||
let formData = new FormData();
|
||||
|
||||
formData.append('conference', exportJob.parentMeetingId);
|
||||
formData.append('pod_id', config.notifier.pod_id);
|
||||
formData.append('is_downloadable', config.notifier.is_downloadable);
|
||||
formData.append('temporaryPresentationId', jobId);
|
||||
formData.append('fileUpload', fs.createReadStream(`${exportJob.presLocation}/pdfs/${jobId}/${filename}.pdf`));
|
||||
|
||||
let res = await axios.post(callbackUrl, formData, { headers: formData.getHeaders() });
|
||||
logger.info(`Upload of job ${exportJob.jobId} returned ${res.data}`);
|
||||
}
|
||||
|
||||
if (jobType == 'PresentationWithAnnotationDownloadJob') {
|
||||
notifyMeetingActor();
|
||||
|
||||
} else if (jobType == 'PresentationWithAnnotationExportJob') {
|
||||
upload(exportJob);
|
||||
|
||||
} else {
|
||||
logger.error(`Notifier received unknown job type ${jobType}`);
|
||||
}
|
||||
|
||||
parentPort.postMessage({ message: workerData })
|
843
bbb-export-annotations/workers/process.js
Normal file
843
bbb-export-annotations/workers/process.js
Normal file
@ -0,0 +1,843 @@
|
||||
const Logger = require('../lib/utils/logger');
|
||||
const config = require('../config');
|
||||
const fs = require('fs');
|
||||
const { create } = require('xmlbuilder2', { encoding: 'utf-8' });
|
||||
const { execSync } = require("child_process");
|
||||
const { Worker, workerData, parentPort } = require('worker_threads');
|
||||
const path = require('path');
|
||||
const sanitize = require('sanitize-filename');
|
||||
const { getStroke, getStrokePoints } = require('perfect-freehand');
|
||||
const probe = require('probe-image-size');
|
||||
|
||||
const jobId = workerData;
|
||||
|
||||
const logger = new Logger('presAnn Process Worker');
|
||||
logger.info("Processing PDF for job " + jobId);
|
||||
|
||||
const kickOffNotifierWorker = (jobType, filename) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new Worker('./workers/notifier.js', { workerData: [jobType, jobId, filename] });
|
||||
worker.on('message', resolve);
|
||||
worker.on('error', reject);
|
||||
worker.on('exit', (code) => {
|
||||
if (code !== 0)
|
||||
reject(new Error(`PresAnn Notifier Worker stopped with exit code ${code}`));
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// General utilities for rendering SVGs resembling Tldraw as much as possible
|
||||
function align_to_pango(alignment) {
|
||||
switch (alignment) {
|
||||
case 'start': return 'left'
|
||||
case 'middle': return 'center'
|
||||
case 'end': return 'right'
|
||||
case 'justify': return 'justify'
|
||||
default: return 'left'
|
||||
}
|
||||
}
|
||||
|
||||
function color_to_hex(color, isStickyNote = false, isFilled = false) {
|
||||
if (isStickyNote) { color = `sticky-${color}` }
|
||||
if (isFilled) { color = `fill-${color}` }
|
||||
|
||||
switch (color) {
|
||||
case 'white': return '#1d1d1d'
|
||||
case 'fill-white': return '#fefefe'
|
||||
case 'sticky-white': return '#fddf8e'
|
||||
case 'lightGray': return '#c6cbd1'
|
||||
case 'fill-lightGray': return '#f1f2f3'
|
||||
case 'sticky-lightGray': return '#dde0e3'
|
||||
case 'gray': return '#788492'
|
||||
case 'fill-gray': return '#e3e5e7'
|
||||
case 'sticky-gray': return '#b3b9c1'
|
||||
case 'black': return '#1d1d1d'
|
||||
case 'fill-black': return '#d2d2d2'
|
||||
case 'sticky-black': return '#fddf8e'
|
||||
case 'green': return '#36b24d'
|
||||
case 'fill-green': return '#d7eddb'
|
||||
case 'sticky-green': return '#8ed29b'
|
||||
case 'cyan': return '#0e98ad'
|
||||
case 'fill-cyan': return '#d0e8ec'
|
||||
case 'sticky-cyan': return '#78c4d0'
|
||||
case 'blue': return '#1c7ed6'
|
||||
case 'fill-blue': return '#d2e4f4'
|
||||
case 'sticky-blue': return '#80b6e6'
|
||||
case 'indigo': return '#4263eb'
|
||||
case 'fill-indigo': return '#d9dff7'
|
||||
case 'sticky-indigo': return '#95a7f2'
|
||||
case 'violet': return '#7746f1'
|
||||
case 'fill-violet': return '#e2daf8'
|
||||
case 'sticky-violet': return '#b297f5'
|
||||
case 'red': return '#ff2133'
|
||||
case 'fill-red': return '#fbd3d6'
|
||||
case 'sticky-red': return '#fd838d'
|
||||
case 'orange': return '#ff9433'
|
||||
case 'fill-orange': return '#fbe8d6'
|
||||
case 'sticky-orange': return '#fdc28d'
|
||||
case 'yellow': return '#ffc936'
|
||||
case 'fill-yellow': return '#fbf1d7'
|
||||
case 'sticky-yellow': return '#fddf8e'
|
||||
|
||||
default: return '#0d0d0d'
|
||||
}
|
||||
}
|
||||
|
||||
function determine_dasharray(dash, gap = 0) {
|
||||
|
||||
switch (dash) {
|
||||
case 'dashed': return `stroke-linecap:butt;stroke-dasharray:${gap};`
|
||||
case 'dotted': return `stroke-linecap:round;stroke-dasharray:${gap};`
|
||||
|
||||
default: return 'stroke-linejoin:round;stroke-linecap:round;'
|
||||
}
|
||||
}
|
||||
|
||||
function determine_font_from_family(family) {
|
||||
switch (family) {
|
||||
case 'script': return 'Caveat Brush'
|
||||
case 'sans': return 'Source Sans Pro'
|
||||
case 'serif': return 'Crimson Pro'
|
||||
// Temporary workaround due to typo in messages
|
||||
case 'erif': return 'Crimson Pro'
|
||||
case 'mono': return 'Source Code Pro'
|
||||
|
||||
default: return 'Caveat Brush'
|
||||
}
|
||||
}
|
||||
|
||||
function rad_to_degree(angle) {
|
||||
return angle * (180 / Math.PI);
|
||||
}
|
||||
|
||||
// Convert pixels to points
|
||||
function to_pt(px) {
|
||||
return (px / config.process.pixelsPerInch) * config.process.pointsPerInch
|
||||
}
|
||||
|
||||
// Convert points to pixels
|
||||
function to_px(pt) {
|
||||
return (pt / config.process.pointsPerInch) * config.process.pixelsPerInch
|
||||
}
|
||||
|
||||
function render_textbox(textColor, font, fontSize, textAlign, text, id, textBoxWidth = null) {
|
||||
|
||||
fontSize = to_pt(fontSize);
|
||||
|
||||
// Sticky notes need automatic line wrapping: take width into account
|
||||
let size = textBoxWidth ? `-size ${textBoxWidth}x` : ''
|
||||
|
||||
let pangoText = `pango:"<span font_family='${font}' font='${fontSize}' color='${textColor}'>${text}</span>"`
|
||||
|
||||
let justify = textAlign === 'justify'
|
||||
textAlign = justify ? 'left' : textAlign
|
||||
|
||||
let commands = [
|
||||
'convert',
|
||||
'-encoding', `${config.process.whiteboardTextEncoding}`,
|
||||
'-density', config.process.pixelsPerInch,
|
||||
'-background', 'transparent',
|
||||
size,
|
||||
'-define', `pango:align=${textAlign}`,
|
||||
'-define', `pango:justify=${justify}`,
|
||||
'-define', 'pango:wrap=word-char',
|
||||
pangoText,
|
||||
path.join(dropbox, `text${id}.png`)
|
||||
].join(' ')
|
||||
|
||||
execSync(commands);
|
||||
}
|
||||
|
||||
function get_gap(dash, size) {
|
||||
switch (dash) {
|
||||
case 'dashed':
|
||||
if (size == 'small') { return '8 8' }
|
||||
else if (size == 'medium') { return '14 14' }
|
||||
else { return '20 20' }
|
||||
case 'dotted':
|
||||
if (size == 'small') { return '0.1 8' }
|
||||
else if (size == 'medium') { return '0.1 14' }
|
||||
else { return '0.1 20' }
|
||||
|
||||
default: return '0'
|
||||
}
|
||||
}
|
||||
|
||||
function get_stroke_width(dash, size) {
|
||||
switch (size) {
|
||||
case 'small': if (dash === 'draw') { return 1 } else { return 4 };
|
||||
case 'medium': if (dash === 'draw') { return 1.75 } else { return 6.25 };
|
||||
case 'large': if (dash === 'draw') { return 2.5 } else { return 8.5 }
|
||||
|
||||
default: return 1;
|
||||
}
|
||||
}
|
||||
|
||||
function sortByKey(array, key, value) {
|
||||
return array.sort(function (a, b) {
|
||||
let [x, y] = [a[key][value], b[key][value]];
|
||||
return x - y;
|
||||
});
|
||||
}
|
||||
|
||||
function text_size_to_px(size, scale = 1, isStickyNote = false) {
|
||||
if (isStickyNote) { size = `sticky-${size}` }
|
||||
|
||||
switch (size) {
|
||||
case 'sticky-small': return 24
|
||||
case 'small': return 28 * scale
|
||||
case 'sticky-medium': return 36
|
||||
case 'medium': return 48 * scale
|
||||
case 'sticky-large': return 48
|
||||
case 'large': return 96 * scale
|
||||
|
||||
default: return 28 * scale
|
||||
}
|
||||
}
|
||||
|
||||
// Methods based on tldraw's utilities
|
||||
function getPath(annotationPoints) {
|
||||
// Gets inner path of a stroke outline
|
||||
// For solid, dashed, and dotted types
|
||||
let stroke = getStrokePoints(annotationPoints).map((strokePoint) => strokePoint.point);
|
||||
|
||||
let [max_x, max_y] = [0, 0];
|
||||
let path = stroke.reduce(
|
||||
(acc, [x0, y0], i, arr) => {
|
||||
if (!arr[i + 1]) return acc
|
||||
let [x1, y1] = arr[i + 1]
|
||||
if (x1 >= max_x) { max_x = x1 }
|
||||
if (y1 >= max_y) { max_y = y1 }
|
||||
acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2)
|
||||
return acc
|
||||
},
|
||||
|
||||
['M', ...stroke[0], 'Q']
|
||||
)
|
||||
|
||||
path.join(' ');
|
||||
return [path, max_x, max_y];
|
||||
}
|
||||
|
||||
function getOutlinePath(annotationPoints) {
|
||||
// Gets outline of a hand-drawn input, with pressure
|
||||
let stroke = getStroke(annotationPoints, {
|
||||
simulatePressure: true,
|
||||
size: 8,
|
||||
});
|
||||
|
||||
let [max_x, max_y] = [0, 0];
|
||||
let path = stroke.reduce(
|
||||
(acc, [x0, y0], i, arr) => {
|
||||
let [x1, y1] = arr[(i + 1) % arr.length]
|
||||
if (x1 >= max_x) { max_x = x1 }
|
||||
if (y1 >= max_y) { max_y = y1 }
|
||||
acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2)
|
||||
return acc;
|
||||
},
|
||||
|
||||
['M', ...stroke[0], 'Q']
|
||||
);
|
||||
|
||||
path.push('Z');
|
||||
path.join(' ');
|
||||
|
||||
return [path, max_x, max_y];
|
||||
}
|
||||
|
||||
function circleFromThreePoints(A, B, C) {
|
||||
let [x1, y1] = A
|
||||
let [x2, y2] = B
|
||||
let [x3, y3] = C
|
||||
|
||||
let a = x1 * (y2 - y3) - y1 * (x2 - x3) + x2 * y3 - x3 * y2
|
||||
|
||||
let b =
|
||||
(x1 * x1 + y1 * y1) * (y3 - y2) +
|
||||
(x2 * x2 + y2 * y2) * (y1 - y3) +
|
||||
(x3 * x3 + y3 * y3) * (y2 - y1)
|
||||
|
||||
let c =
|
||||
(x1 * x1 + y1 * y1) * (x2 - x3) +
|
||||
(x2 * x2 + y2 * y2) * (x3 - x1) +
|
||||
(x3 * x3 + y3 * y3) * (x1 - x2)
|
||||
|
||||
let x = -b / (2 * a)
|
||||
let y = -c / (2 * a)
|
||||
|
||||
return [x, y, Math.hypot(x - x1, y - y1)]
|
||||
}
|
||||
|
||||
function distance(x1, y1, x2, y2) {
|
||||
return Math.sqrt(Math.pow((x2 - x1), 2) + Math.pow((y2 - y1), 2));
|
||||
}
|
||||
|
||||
function getArcLength(C, r, A, B) {
|
||||
let sweep = getSweep(C, A, B);
|
||||
return r * (2 * Math.PI) * (sweep / (2 * Math.PI));
|
||||
}
|
||||
|
||||
function getSweep(C, A, B) {
|
||||
|
||||
// Get angle between two vectors in radians
|
||||
let a0 = Math.atan2(A[1] - C[1], A[0] - C[0]);
|
||||
let a1 = Math.atan2(B[1] - C[1], B[0] - C[0]);
|
||||
|
||||
// Short distance between two angles
|
||||
let max = Math.PI * 2
|
||||
let da = (a1 - a0) % max
|
||||
|
||||
return ((2 * da) % max) - da
|
||||
}
|
||||
|
||||
function intersectCircleCircle(c1, r1, c2, r2) {
|
||||
|
||||
let dx = c2[0] - c1[0];
|
||||
let dy = c2[1] - c1[1];
|
||||
|
||||
let d = Math.sqrt(dx * dx + dy * dy);
|
||||
let x = (d * d - r2 * r2 + r1 * r1) / (2 * d);
|
||||
let y = Math.sqrt(r1 * r1 - x * x);
|
||||
|
||||
dx /= d
|
||||
dy /= d
|
||||
|
||||
return [[c1[0] + dx * x - dy * y, c1[1] + dy * x + dx * y],
|
||||
[c1[0] + dx * x + dy * y, c1[1] + dy * x - dx * y]]
|
||||
}
|
||||
|
||||
function rotWith(A, C, r = 0) {
|
||||
// Rotate a vector A around another vector C by r radians
|
||||
if (r === 0) return A
|
||||
|
||||
let s = Math.sin(r)
|
||||
let c = Math.cos(r)
|
||||
|
||||
let px = A[0] - C[0]
|
||||
let py = A[1] - C[1]
|
||||
|
||||
let nx = px * c - py * s
|
||||
let ny = px * s + py * c
|
||||
|
||||
return [nx + C[0], ny + C[1]]
|
||||
}
|
||||
|
||||
function nudge(A, B, d) {
|
||||
// Pushes a point A towards a point B by a given distance
|
||||
if (A[0] === B[0] && A[1] === B[1]) return A
|
||||
|
||||
// B - A
|
||||
let sub = [B[0] - A[0], B[1] - A[1]];
|
||||
|
||||
// Vector length
|
||||
let len = Math.hypot(sub[0], sub[1]);
|
||||
|
||||
// Get unit vector
|
||||
let unit = [sub[0] / len, sub[1] / len];
|
||||
|
||||
// Multiply by distance
|
||||
let mul = [unit[0] * d, unit[1] * d];
|
||||
|
||||
return [A[0] + mul[0], A[1] + mul[1]]
|
||||
}
|
||||
|
||||
function getCurvedArrowHeadPath(A, r1, C, r2, sweep) {
|
||||
const phi = (1 + Math.sqrt(5)) / 2;
|
||||
|
||||
// Determine intersections between two circles
|
||||
let ints = intersectCircleCircle(A, r1 * (phi - 1), C, r2)
|
||||
|
||||
if (!ints) {
|
||||
logger.info('Could not find an intersection for the arrow head.')
|
||||
return { left: A, right: A }
|
||||
}
|
||||
|
||||
let int = sweep ? ints[0] : ints[1]
|
||||
let left = int ? nudge(rotWith(int, A, Math.PI / 6), A, r1 * -0.382) : A
|
||||
let right = int ? nudge(rotWith(int, A, -Math.PI / 6), A, r1 * -0.382) : A
|
||||
|
||||
return `M ${left} L ${A} ${right}`
|
||||
}
|
||||
|
||||
// Methods to convert Akka message contents into SVG
|
||||
function overlay_arrow(svg, annotation) {
|
||||
|
||||
let [x, y] = annotation.point;
|
||||
let bend = annotation.bend;
|
||||
let decorations = annotation.decorations;
|
||||
|
||||
let dash = annotation.style.dash;
|
||||
dash = (dash == 'draw') ? 'solid' : dash // Use 'solid' thickness
|
||||
|
||||
let shapeColor = color_to_hex(annotation.style.color);
|
||||
let sw = get_stroke_width(dash, annotation.style.size);
|
||||
let gap = get_gap(dash, annotation.style.size);
|
||||
let stroke_dasharray = determine_dasharray(dash, gap);
|
||||
|
||||
let [start_x, start_y] = annotation.handles.start.point;
|
||||
let [end_x, end_y] = annotation.handles.end.point;
|
||||
let [bend_x, bend_y] = annotation.handles.bend.point;
|
||||
|
||||
let line = [];
|
||||
let arrowHead = [];
|
||||
let arrowDistance = distance(start_x, start_y, end_x, end_y);
|
||||
let arrowHeadLength = Math.min(arrowDistance / 3, 8 * sw);
|
||||
let isStraightLine = parseFloat(bend).toFixed(3) == 0;
|
||||
|
||||
let angle = Math.atan2(end_y - start_y, end_x - start_x)
|
||||
|
||||
if (isStraightLine) {
|
||||
// Draws a straight line / arrow
|
||||
line.push(`M ${start_x} ${start_y} L ${end_x} ${end_y}`);
|
||||
|
||||
if (decorations.start || decorations.end) {
|
||||
arrowHead.push(`M ${end_x} ${end_y}`);
|
||||
arrowHead.push(`L ${end_x + arrowHeadLength * Math.cos(angle + (7 / 6) * Math.PI)} ${end_y + arrowHeadLength * Math.sin(angle + (7 / 6) * Math.PI)}`);
|
||||
arrowHead.push(`M ${end_x} ${end_y}`);
|
||||
arrowHead.push(`L ${end_x + arrowHeadLength * Math.cos(angle + (5 / 6) * Math.PI)} ${end_y + arrowHeadLength * Math.sin(angle + (5 / 6) * Math.PI)}`);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// Curved lines and arrows
|
||||
let circle = circleFromThreePoints([start_x, start_y], [bend_x, bend_y], [end_x, end_y]);
|
||||
let center = [circle[0], circle[1]]
|
||||
let radius = circle[2]
|
||||
let length = getArcLength(center, radius, [start_x, start_y], [end_x, end_y]);
|
||||
|
||||
line.push(`M ${start_x} ${start_y} A ${radius} ${radius} 0 0 ${length > 0 ? '1' : '0'} ${end_x} ${end_y}`);
|
||||
|
||||
if (decorations.start)
|
||||
arrowHead.push(getCurvedArrowHeadPath([start_x, start_y], arrowHeadLength, center, radius, length < 0));
|
||||
else if (decorations.end) {
|
||||
arrowHead.push(getCurvedArrowHeadPath([end_x, end_y], arrowHeadLength, center, radius, length >= 0));
|
||||
}
|
||||
}
|
||||
|
||||
// The arrowhead is purposely not styled (e.g., dashed / dotted)
|
||||
svg.ele('g', {
|
||||
style: `stroke:${shapeColor};stroke-width:${sw};fill:none;`,
|
||||
transform: `translate(${x} ${y})`
|
||||
}).ele('path', {
|
||||
'style': stroke_dasharray,
|
||||
d: line.join(' '),
|
||||
}).up()
|
||||
.ele('path', {
|
||||
d: arrowHead.join(' '),
|
||||
}).up();
|
||||
}
|
||||
|
||||
function overlay_draw(svg, annotation) {
|
||||
let dash = annotation.style.dash;
|
||||
|
||||
let [path, max_x, max_y] = (dash == 'draw') ? getOutlinePath(annotation.points) : getPath(annotation.points);
|
||||
|
||||
if (!path.length) return;
|
||||
|
||||
let shapeColor = color_to_hex(annotation.style.color);
|
||||
let rotation = rad_to_degree(annotation.rotation);
|
||||
let thickness = get_stroke_width(dash, annotation.style.size);
|
||||
let gap = get_gap(dash, annotation.style.size);
|
||||
|
||||
let [x, y] = annotation.point;
|
||||
|
||||
let stroke_dasharray = determine_dasharray(dash, gap);
|
||||
let fill = (dash === 'draw') ? shapeColor : 'none';
|
||||
|
||||
let shapeFillColor = color_to_hex(`fill-${annotation.style.color}`)
|
||||
let shapeTransform = `translate(${x} ${y}), rotate(${rotation} ${max_x / 2} ${max_y / 2})`
|
||||
|
||||
// Fill assuming solid, small pencil used when path start- and end points overlap
|
||||
let shapeIsFilled =
|
||||
annotation.style.isFilled &&
|
||||
annotation.points.length > 3
|
||||
&& Math.round(distance(
|
||||
annotation.points[0][0],
|
||||
annotation.points[0][1],
|
||||
annotation.points[annotation.points.length - 1][0],
|
||||
annotation.points[annotation.points.length - 1][1]
|
||||
)) <= 2 * get_stroke_width('solid', 'small');
|
||||
|
||||
if (shapeIsFilled) {
|
||||
svg.ele('path', {
|
||||
style: `fill:${shapeFillColor};`,
|
||||
d: getPath(annotation.points)[0] + 'Z',
|
||||
transform: shapeTransform
|
||||
}).up()
|
||||
}
|
||||
|
||||
svg.ele('path', {
|
||||
style: `stroke:${shapeColor};stroke-width:${thickness};fill:${fill};${stroke_dasharray}`,
|
||||
d: path,
|
||||
transform: shapeTransform
|
||||
})
|
||||
}
|
||||
|
||||
function overlay_ellipse(svg, annotation) {
|
||||
|
||||
let dash = annotation.style.dash;
|
||||
dash = (dash == 'draw') ? 'solid' : dash // Use 'solid' thickness for draw type
|
||||
|
||||
let [x, y] = annotation.point; // Ellipse center coordinates
|
||||
let [rx, ry] = annotation.radius;
|
||||
let isFilled = annotation.style.isFilled;
|
||||
|
||||
let shapeColor = color_to_hex(annotation.style.color);
|
||||
let fillColor = isFilled ? color_to_hex(annotation.style.color, false, isFilled) : 'none';
|
||||
|
||||
let rotation = rad_to_degree(annotation.rotation);
|
||||
let sw = get_stroke_width(dash, annotation.style.size);
|
||||
let gap = get_gap(dash, annotation.style.size);
|
||||
|
||||
let stroke_dasharray = determine_dasharray(dash, gap);
|
||||
|
||||
svg.ele('g', {
|
||||
style: `stroke:${shapeColor};stroke-width:${sw};fill:${fillColor};${stroke_dasharray}`,
|
||||
}).ele('ellipse', {
|
||||
'cx': x + rx,
|
||||
'cy': y + ry,
|
||||
'rx': rx,
|
||||
'ry': ry,
|
||||
transform: `rotate(${rotation} ${x + rx} ${y + ry})`
|
||||
}).up()
|
||||
|
||||
if (annotation.label) { overlay_shape_label(svg, annotation) }
|
||||
}
|
||||
|
||||
function overlay_rectangle(svg, annotation) {
|
||||
|
||||
let dash = annotation.style.dash;
|
||||
let rect_dash = (dash == 'draw') ? 'solid' : dash // Use 'solid' thickness for draw type
|
||||
|
||||
let [x, y] = annotation.point;
|
||||
let [w, h] = annotation.size;
|
||||
let isFilled = annotation.style.isFilled;
|
||||
|
||||
let shapeColor = color_to_hex(annotation.style.color);
|
||||
let fillColor = isFilled ? color_to_hex(annotation.style.color, false, isFilled) : 'none';
|
||||
|
||||
let rotation = rad_to_degree(annotation.rotation);
|
||||
let sw = get_stroke_width(rect_dash, annotation.style.size);
|
||||
let gap = get_gap(dash, annotation.style.size);
|
||||
|
||||
let stroke_dasharray = determine_dasharray(dash, gap);
|
||||
|
||||
let rx = (dash == 'draw') ? Math.min(w / 4, sw * 2) : 0;
|
||||
let ry = (dash == 'draw') ? Math.min(h / 4, sw * 2) : 0;
|
||||
|
||||
svg.ele('g', {
|
||||
style: `stroke:${shapeColor};stroke-width:${sw};fill:${fillColor};${stroke_dasharray}`,
|
||||
}).ele('rect', {
|
||||
width: w,
|
||||
height: h,
|
||||
'rx': rx,
|
||||
'ry': ry,
|
||||
transform: `translate(${x} ${y}), rotate(${rotation} ${w / 2} ${h / 2})`
|
||||
}).up()
|
||||
|
||||
if (annotation.label) { overlay_shape_label(svg, annotation) }
|
||||
}
|
||||
|
||||
function overlay_shape_label(svg, annotation) {
|
||||
|
||||
let fontColor = color_to_hex(annotation.style.color);
|
||||
let font = determine_font_from_family(annotation.style.font);
|
||||
let fontSize = text_size_to_px(annotation.style.size, annotation.style.scale);
|
||||
let textAlign = 'center';
|
||||
let text = annotation.label;
|
||||
let id = annotation.id;
|
||||
let rotation = rad_to_degree(annotation.rotation);
|
||||
|
||||
let [shape_width, shape_height] = annotation.size
|
||||
let [shape_x, shape_y] = annotation.point;
|
||||
|
||||
let x_offset = annotation.labelPoint[0]
|
||||
let y_offset = annotation.labelPoint[1]
|
||||
|
||||
let label_center_x = shape_x + shape_width * x_offset
|
||||
let label_center_y = shape_y + shape_height * y_offset
|
||||
|
||||
render_textbox(fontColor, font, fontSize, textAlign, text, id);
|
||||
|
||||
let dimensions = probe.sync(fs.readFileSync(path.join(dropbox, `text${id}.png`)));
|
||||
let labelWidth = dimensions.width;
|
||||
let labelHeight = dimensions.height;
|
||||
|
||||
svg.ele('g', {
|
||||
transform: `rotate(${rotation} ${label_center_x} ${label_center_y})`
|
||||
}).ele('image', {
|
||||
x: label_center_x - (labelWidth * x_offset),
|
||||
y: label_center_y - (labelHeight * y_offset),
|
||||
width: labelWidth,
|
||||
height: labelHeight,
|
||||
'xlink:href': `file://${dropbox}/text${id}.png`,
|
||||
}).up();
|
||||
}
|
||||
|
||||
function overlay_sticky(svg, annotation) {
|
||||
|
||||
let backgroundColor = color_to_hex(annotation.style.color, true);
|
||||
let fontSize = text_size_to_px(annotation.style.size, annotation.style.scale, true);
|
||||
let rotation = rad_to_degree(annotation.rotation);
|
||||
let font = determine_font_from_family(annotation.style.font);
|
||||
let textAlign = align_to_pango(annotation.style.textAlign);
|
||||
|
||||
let [textBoxWidth, textBoxHeight] = annotation.size;
|
||||
let [textBox_x, textBox_y] = annotation.point;
|
||||
|
||||
let textColor = "#0d0d0d" // For sticky notes
|
||||
let text = annotation.text
|
||||
let id = annotation.id;
|
||||
|
||||
render_textbox(textColor, font, fontSize, textAlign, text, id, textBoxWidth);
|
||||
|
||||
// Overlay transparent text image over empty sticky note
|
||||
svg.ele('g', {
|
||||
transform: `rotate(${rotation}, ${textBox_x + (textBoxWidth / 2)}, ${textBox_y + (textBoxHeight / 2)})`
|
||||
}).ele('rect', {
|
||||
x: textBox_x,
|
||||
y: textBox_y,
|
||||
width: textBoxWidth,
|
||||
height: textBoxHeight,
|
||||
fill: backgroundColor,
|
||||
}).up()
|
||||
.ele('image', {
|
||||
x: textBox_x,
|
||||
y: textBox_y,
|
||||
width: textBoxWidth,
|
||||
height: textBoxHeight,
|
||||
'xlink:href': `file://${dropbox}/text${id}.png`,
|
||||
}).up();
|
||||
}
|
||||
|
||||
function overlay_triangle(svg, annotation) {
|
||||
|
||||
let dash = annotation.style.dash;
|
||||
dash = (dash == 'draw') ? 'solid' : dash
|
||||
|
||||
let [x, y] = annotation.point;
|
||||
let [w, h] = annotation.size;
|
||||
let isFilled = annotation.style.isFilled;
|
||||
|
||||
let shapeColor = color_to_hex(annotation.style.color);
|
||||
let fillColor = isFilled ? color_to_hex(annotation.style.color, false, isFilled) : 'none';
|
||||
|
||||
let rotation = rad_to_degree(annotation.rotation);
|
||||
let sw = get_stroke_width(dash, annotation.style.size);
|
||||
let gap = get_gap(dash, annotation.style.size);
|
||||
|
||||
let stroke_dasharray = determine_dasharray(dash, gap);
|
||||
let points = `${w / 2} 0, ${w} ${h}, 0 ${h}, ${w / 2} 0`
|
||||
|
||||
svg.ele('g', {
|
||||
style: `stroke:${shapeColor};stroke-width:${sw};fill:${fillColor};${stroke_dasharray}`,
|
||||
}).ele('polygon', {
|
||||
'points': points,
|
||||
transform: `translate(${x}, ${y}), rotate(${rotation} ${w / 2} ${h / 2})`
|
||||
}).up()
|
||||
|
||||
if (annotation.label) { overlay_shape_label(svg, annotation) }
|
||||
}
|
||||
|
||||
function overlay_text(svg, annotation) {
|
||||
|
||||
let [textBoxWidth, textBoxHeight] = annotation.size;
|
||||
let fontColor = color_to_hex(annotation.style.color);
|
||||
let font = determine_font_from_family(annotation.style.font);
|
||||
let fontSize = text_size_to_px(annotation.style.size, annotation.style.scale);
|
||||
let textAlign = align_to_pango(annotation.style.textAlign);
|
||||
let text = annotation.text
|
||||
let id = annotation.id;
|
||||
|
||||
let rotation = rad_to_degree(annotation.rotation);
|
||||
let [textBox_x, textBox_y] = annotation.point;
|
||||
|
||||
render_textbox(fontColor, font, fontSize, textAlign, text, id);
|
||||
|
||||
let rotation_x = textBox_x + (textBoxWidth / 2)
|
||||
let rotation_y = textBox_y + (textBoxHeight / 2)
|
||||
|
||||
svg.ele('g', {
|
||||
transform: `rotate(${rotation} ${rotation_x} ${rotation_y})`
|
||||
}).ele('image', {
|
||||
x: textBox_x,
|
||||
y: textBox_y,
|
||||
width: textBoxWidth,
|
||||
height: textBoxHeight,
|
||||
'xlink:href': `file://${dropbox}/text${id}.png`,
|
||||
}).up();
|
||||
}
|
||||
|
||||
function overlay_annotation(svg, currentAnnotation) {
|
||||
|
||||
if (currentAnnotation.childIndex >= 1) {
|
||||
switch (currentAnnotation.type) {
|
||||
case 'arrow':
|
||||
overlay_arrow(svg, currentAnnotation);
|
||||
break;
|
||||
case 'draw':
|
||||
overlay_draw(svg, currentAnnotation);
|
||||
break;
|
||||
case 'ellipse':
|
||||
overlay_ellipse(svg, currentAnnotation);
|
||||
break;
|
||||
case 'rectangle':
|
||||
overlay_rectangle(svg, currentAnnotation);
|
||||
break;
|
||||
case 'sticky':
|
||||
overlay_sticky(svg, currentAnnotation);
|
||||
break;
|
||||
case 'triangle':
|
||||
overlay_triangle(svg, currentAnnotation);
|
||||
break;
|
||||
case 'text':
|
||||
overlay_text(svg, currentAnnotation);
|
||||
break;
|
||||
default:
|
||||
logger.info(`Unknown annotation type ${currentAnnotation.type}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function overlay_annotations(svg, currentSlideAnnotations) {
|
||||
|
||||
// Sort annotations by lowest child index
|
||||
currentSlideAnnotations = sortByKey(currentSlideAnnotations, 'annotationInfo', 'childIndex');
|
||||
|
||||
for (let annotation of currentSlideAnnotations) {
|
||||
|
||||
switch (annotation.annotationInfo.type) {
|
||||
case 'group':
|
||||
// Get annotations that have this group as parent
|
||||
let children = annotation.annotationInfo.children;
|
||||
|
||||
for (let childId of children) {
|
||||
let childAnnotation = currentSlideAnnotations.find(ann => ann.id == childId);
|
||||
overlay_annotation(svg, childAnnotation.annotationInfo);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
// Add individual annotations if they don't belong to a group
|
||||
if (annotation.annotationInfo.parentId % 1 === 0) {
|
||||
overlay_annotation(svg, annotation.annotationInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process the presentation pages and annotations into a PDF file
|
||||
|
||||
// 1. Get the job
|
||||
const dropbox = path.join(config.shared.presAnnDropboxDir, jobId);
|
||||
let job = fs.readFileSync(path.join(dropbox, 'job'));
|
||||
let exportJob = JSON.parse(job);
|
||||
|
||||
// 2. Get the annotations
|
||||
let annotations = fs.readFileSync(path.join(dropbox, 'whiteboard'));
|
||||
let whiteboard = JSON.parse(annotations);
|
||||
let pages = JSON.parse(whiteboard.pages);
|
||||
let ghostScriptInput = ""
|
||||
|
||||
// 3. Convert annotations to SVG
|
||||
for (let currentSlide of pages) {
|
||||
|
||||
let backgroundImagePath = path.join(dropbox, `slide${currentSlide.page}`);
|
||||
let svgFileExists = fs.existsSync(`${backgroundImagePath}.svg`)
|
||||
let backgroundFormat = fs.existsSync(`${backgroundImagePath}.png`) ? 'png' : 'jpeg'
|
||||
|
||||
// Output dimensions in pixels even if stated otherwise (pt)
|
||||
// CairoSVG didn't like attempts to read the dimensions from a stream
|
||||
// that would prevent loading file in memory
|
||||
// Ideally, use dimensions provided by tldraw's background image asset
|
||||
// (this is not yet always provided)
|
||||
let dimensions = svgFileExists ?
|
||||
probe.sync(fs.readFileSync(`${backgroundImagePath}.svg`)) :
|
||||
probe.sync(fs.readFileSync(`${backgroundImagePath}.${backgroundFormat}`));
|
||||
|
||||
let slideWidth = parseInt(dimensions.width, 10);
|
||||
let slideHeight = parseInt(dimensions.height, 10);
|
||||
|
||||
// Create the SVG slide with the background image
|
||||
let svg = create({ version: '1.0', encoding: 'UTF-8' })
|
||||
.ele('svg', {
|
||||
xmlns: 'http://www.w3.org/2000/svg',
|
||||
'xmlns:xlink': 'http://www.w3.org/1999/xlink',
|
||||
width: `${slideWidth}px`,
|
||||
height: `${slideHeight}px`,
|
||||
})
|
||||
.dtd({
|
||||
pubID: '-//W3C//DTD SVG 1.1//EN',
|
||||
sysID: 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'
|
||||
})
|
||||
.ele('image', {
|
||||
'xlink:href': `file://${dropbox}/slide${currentSlide.page}.${backgroundFormat}`,
|
||||
width: `${slideWidth}px`,
|
||||
height: `${slideHeight}px`,
|
||||
})
|
||||
.up()
|
||||
.ele('g', {
|
||||
class: 'canvas'
|
||||
});
|
||||
|
||||
// 4. Overlay annotations onto slides
|
||||
overlay_annotations(svg, currentSlide.annotations)
|
||||
|
||||
svg = svg.end({ prettyPrint: true });
|
||||
|
||||
// Write annotated SVG file
|
||||
let SVGfile = path.join(dropbox, `annotated-slide${currentSlide.page}.svg`)
|
||||
let PDFfile = path.join(dropbox, `annotated-slide${currentSlide.page}.pdf`)
|
||||
|
||||
fs.writeFileSync(SVGfile, svg, function (err) {
|
||||
if (err) { return logger.error(err); }
|
||||
});
|
||||
|
||||
// Dimensions converted back to a pixel size which,
|
||||
// when converted to points, will yield the desired
|
||||
// dimension in pixels when read without conversion
|
||||
|
||||
// e.g. say Tldraw's canvas is 1920x1080 px.
|
||||
// The background SVG dimensions are set to 1920x1080 pt (incorrect unit).
|
||||
// So we read it in ignoring the unit as 1920x1080 px, making the position of the drawings match.
|
||||
// Now we assume we had 1920x1080pt and resize to 2560x1440 px so that the SVG generates with the original "wrong" size.
|
||||
|
||||
let convertAnnotatedSlide = [
|
||||
'cairosvg',
|
||||
SVGfile,
|
||||
'--output-width', to_px(slideWidth),
|
||||
'--output-height', to_px(slideHeight),
|
||||
'-o', PDFfile
|
||||
].join(' ');
|
||||
|
||||
execSync(convertAnnotatedSlide);
|
||||
|
||||
ghostScriptInput += `${PDFfile} `
|
||||
}
|
||||
|
||||
// Create PDF output directory if it doesn't exist
|
||||
let output_dir = path.join(exportJob.presLocation, 'pdfs', jobId);
|
||||
|
||||
if (!fs.existsSync(output_dir)) { fs.mkdirSync(output_dir, { recursive: true }); }
|
||||
|
||||
let filename = sanitize(exportJob.filename.replace(/\s/g, '_'));
|
||||
|
||||
let mergePDFs = [
|
||||
'gs',
|
||||
'-dNOPAUSE',
|
||||
'-sDEVICE=pdfwrite',
|
||||
`-sOUTPUTFILE="${path.join(output_dir, `${filename}.pdf`)}"`,
|
||||
`-dBATCH`,
|
||||
ghostScriptInput,
|
||||
].join(' ');
|
||||
|
||||
// Resulting PDF file is stored in the presentation dir
|
||||
execSync(mergePDFs);
|
||||
|
||||
// Launch Notifier Worker depending on job type
|
||||
logger.info(`Saved PDF at ${output_dir}/${jobId}/${filename}.pdf`);
|
||||
|
||||
kickOffNotifierWorker(exportJob.jobType, filename);
|
||||
|
||||
parentPort.postMessage({ message: workerData });
|
14887
bbb-learning-dashboard/package-lock.json
generated
14887
bbb-learning-dashboard/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -16,7 +16,6 @@
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-intl": "^5.20.6",
|
||||
"react-scripts": "^5.0.0",
|
||||
"typescript": "^4.3.5",
|
||||
"web-vitals": "^1.1.2"
|
||||
},
|
||||
@ -54,6 +53,7 @@
|
||||
"eslint-plugin-react": "^7.24.0",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"postcss": "^8.4.5",
|
||||
"react-scripts": "^5.0.0",
|
||||
"tailwindcss": "^3.0.11"
|
||||
}
|
||||
}
|
||||
|
@ -244,7 +244,7 @@ class UsersTable extends React.Component {
|
||||
|
||||
<div className="inline-block">
|
||||
<button
|
||||
className="leading-none border-0 p-0 m-0 bg-none font-semibold truncate xl:max-w-sm max-w-xs cursor-pointer rounded-md focus:outline-none focus:ring ring-offset-0 focus:ring-gray-500 focus:ring-opacity-50"
|
||||
className="leading-none border-0 p-0 m-0 bg-none font-semibold truncate xl:max-w-sm max-w-xs cursor-pointer focus:rounded focus:outline-none focus:ring ring-offset-0 focus:ring-gray-500 focus:ring-opacity-50 underline decoration-dotted decoration-from-font hover:opacity-75 focus:no-underline active:opacity-95"
|
||||
type="button"
|
||||
onClick={() => this.openUserModal(user)}
|
||||
aria-label={`Open user details modal - ${user.name}`}
|
||||
|
@ -1 +1 @@
|
||||
git clone --branch v1.2.0 --depth 1 https://github.com/bigbluebutton/bbb-pads bbb-pads
|
||||
git clone --branch v1.2.1 --depth 1 https://github.com/bigbluebutton/bbb-pads bbb-pads
|
||||
|
@ -1 +1 @@
|
||||
git clone --branch v4.0.0 --depth 1 https://github.com/bigbluebutton/bbb-playback bbb-playback
|
||||
git clone --branch v5.0.0-alpha.1 --depth 1 https://github.com/bigbluebutton/bbb-playback bbb-playback
|
||||
|
48
bbb-recording-imex/README.md
Normal file
48
bbb-recording-imex/README.md
Normal file
@ -0,0 +1,48 @@
|
||||
# BBB-Recording-Importer
|
||||
|
||||
Imports and parses recording metadata.xml files and stores the data in a Postgresql database
|
||||
|
||||
|
||||
## How to use
|
||||
|
||||
1. In bbb-common-web
|
||||
- Edit the .env file and set the environment variables
|
||||
- Run the hibernate.cfg script to generate the hibernate config file
|
||||
- Run "docker-compose up" to start up the docker container containing the Postgresql database
|
||||
- Interact with the database using the psql script
|
||||
2. In bbb-recording-imex
|
||||
- Unit tests for parsing and persisting recording metadata can be found in src/test/java/org/bigbluebutton/recording/
|
||||
- Edit the "metadataDirectory" variables in the test files to point to where the recording metadata can be found
|
||||
- Run the unit tests using the command "mvn test"
|
||||
- Use the deploy.sh script to compile the program
|
||||
- Run the program with the recording-imex.sh script found in ~/usr/local/bin
|
||||
- Use the --help option to see the usage
|
||||
|
||||
Usage: {-e|-i <persist>} [-s <id>] [PATH]
|
||||
Import/export recording(s) to/from PATH. The default PATH is
|
||||
/var/bigbluebutton/published/presentation
|
||||
-e export recording(s)
|
||||
-i <persist> import recording(s) and indicate if they should be persisted [true|false]
|
||||
-s <id> ID of single recording to be imported/exported
|
||||
|
||||
|
||||
Examples
|
||||
|
||||
~/usr/local/bin/recording-imex.sh -i true -s random-7739095 /var/bigbluebutton/published/presentation/1abbc41a2f2faf1d754dbd130fba9ae072c6e742-1652301432519/metadata.xml
|
||||
|
||||
~/usr/local/bin/recording-imex.sh -i true /var/bigbluebutton/published/presentation/
|
||||
|
||||
|
||||
## Testing the new recording service
|
||||
|
||||
1. In bigbluebutton-web
|
||||
- Edit the "recordingService" bean in /grails-app/conf/spring/resources.xml to use "org.bigbluebutton.api.service.impl.RecordingServiceDbImpl"
|
||||
- Use "org.bigbluebutton.api.service.impl.RecordingServiceFileImpl" if you want to use the traditional file system service
|
||||
2. In bbb-recording-imex
|
||||
- Use the get-recordings.sh script to test the getRecordings endpoint on the recording API
|
||||
- Edit the "SALT" variable to have the value of your security salt
|
||||
- The script accepts arguments through the use of flags
|
||||
- "-i" for the meetingID
|
||||
- "-r" for the recordID(s)
|
||||
- "-s" for the state(s)
|
||||
- "-m" for the metadata
|
12
bbb-recording-imex/deploy.sh
Executable file
12
bbb-recording-imex/deploy.sh
Executable file
@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
JAR_DIR="$HOME/usr/share/recording-imex"
|
||||
JAR_NAME="bbb-recording-imex-1.0-SNAPSHOT-shaded.jar"
|
||||
RUN_DIR="$HOME/usr/local/bin"
|
||||
|
||||
mkdir -p $JAR_DIR
|
||||
mkdir -p $RUN_DIR
|
||||
mvn package -Dmaven.test.skip
|
||||
cp target/${JAR_NAME} $JAR_DIR
|
||||
echo '#!/bin/bash
|
||||
java -jar '${JAR_DIR}'/'${JAR_NAME} '"$@"'> ${RUN_DIR}/recording-imex.sh
|
||||
chmod +x ${RUN_DIR}/recording-imex.sh
|
50
bbb-recording-imex/get-recordings.sh
Executable file
50
bbb-recording-imex/get-recordings.sh
Executable file
@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
while getopts i:r:s:m: flag
|
||||
do
|
||||
case "${flag}" in
|
||||
i) MEETING_ID=${OPTARG};;
|
||||
r) RECORD_ID=${OPTARG};;
|
||||
s) STATE=${OPTARG};;
|
||||
m) META=${OPTARG};;
|
||||
esac
|
||||
done
|
||||
|
||||
BASE_URL=""
|
||||
SUBDIRECTORY="bigbluebutton/api/"
|
||||
ENDPOINT="getRecordings"
|
||||
QUERY=""
|
||||
|
||||
if ! [[ -z ${MEETING_ID+x} ]]; then QUERY+="meetingID=$MEETING_ID&"; fi
|
||||
if ! [[ -z ${RECORD_ID+x} ]]; then QUERY+="recordID=$RECORD_ID&"; fi
|
||||
if ! [[ -z ${STATE+x} ]]; then QUERY+="state=$STATE&"; fi
|
||||
if ! [[ -z ${META+x} ]]; then QUERY+="meta=$META"; fi
|
||||
|
||||
echo "query: $QUERY"
|
||||
|
||||
INDEX=${#QUERY}-1
|
||||
if [ "${QUERY:$INDEX:1}" = "&" ]; then QUERY=${QUERY:0:$INDEX}; fi
|
||||
|
||||
echo "query: $QUERY"
|
||||
|
||||
SALT=
|
||||
DATA="$ENDPOINT$QUERY$SALT"
|
||||
|
||||
echo "data: $DATA"
|
||||
|
||||
CHECKSUM=$(echo -n $DATA | sha256sum)
|
||||
CHECKSUM=${CHECKSUM:0:64}
|
||||
|
||||
echo "sha256 checksum: $CHECKSUM"
|
||||
|
||||
QUERY="?$QUERY"
|
||||
|
||||
if ! [[ ${#QUERY} -eq 1 ]]; then QUERY+="&"; fi
|
||||
|
||||
QUERY+="checksum=$CHECKSUM"
|
||||
|
||||
echo "query: $QUERY"
|
||||
|
||||
REQUEST="$BASE_URL$SUBDIRECTORY$ENDPOINT$QUERY"
|
||||
echo "request: $REQUEST"
|
||||
|
||||
curl -s -X GET "$REQUEST"
|
111
bbb-recording-imex/pom.xml
Executable file
111
bbb-recording-imex/pom.xml
Executable file
@ -0,0 +1,111 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.bigbluebutton</groupId>
|
||||
<artifactId>bbb-recording-imex</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<shadedArtifactAttached>true</shadedArtifactAttached>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>org.bigbluebutton.RecordingApp</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>2.22.2</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<version>2.22.2</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>net.revelc.code.formatter</groupId>
|
||||
<artifactId>formatter-maven-plugin</artifactId>
|
||||
<version>2.16.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>format</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<compilerSource>1.8</compilerSource>
|
||||
<compilerCompliance>1.8</compilerCompliance>
|
||||
<compilerTargetPlatform>1.8</compilerTargetPlatform>
|
||||
<lineEnding>LF</lineEnding>
|
||||
<encoding>UTF-8</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.bigbluebutton</groupId>
|
||||
<artifactId>bbb-common-web</artifactId>
|
||||
<version>0.0.3-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-core</artifactId>
|
||||
<version>1.2.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>1.7.30</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>1.2.3</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>5.7.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>5.7.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.platform</groupId>
|
||||
<artifactId>junit-platform-console-standalone</artifactId>
|
||||
<version>1.8.2</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
224
bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingApp.java
Executable file
224
bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingApp.java
Executable file
@ -0,0 +1,224 @@
|
||||
package org.bigbluebutton;
|
||||
|
||||
import java.io.Console;
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
public class RecordingApp {
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length > 0) {
|
||||
commandMode(args);
|
||||
} else {
|
||||
interactiveMode();
|
||||
}
|
||||
}
|
||||
|
||||
private static void commandMode(String[] args) {
|
||||
int i = 0, j;
|
||||
String arg;
|
||||
char flag;
|
||||
boolean export = false;
|
||||
boolean persist = false;
|
||||
String id = null;
|
||||
String path;
|
||||
|
||||
while (i < args.length && args[i].startsWith("-")) {
|
||||
arg = args[i++];
|
||||
|
||||
if (arg.equals("--help")) {
|
||||
printUsage();
|
||||
return;
|
||||
}
|
||||
|
||||
for (j = 1; j < arg.length(); j++) {
|
||||
flag = arg.charAt(j);
|
||||
|
||||
switch (flag) {
|
||||
case 'e':
|
||||
export = true;
|
||||
break;
|
||||
case 'i':
|
||||
export = false;
|
||||
if (i < args.length) {
|
||||
String shouldPersist = args[i++];
|
||||
if (shouldPersist.equalsIgnoreCase("true"))
|
||||
persist = true;
|
||||
else if (shouldPersist.equalsIgnoreCase("false"))
|
||||
persist = false;
|
||||
else {
|
||||
System.out.println("Error: Could not parse persist argument");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
System.out.println("Error: Imports require an argument specifying if they should be persisted");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 's':
|
||||
if (i < args.length)
|
||||
id = args[i++];
|
||||
else {
|
||||
System.out.println(
|
||||
"Error: To import/export a single recording you must provide the recording ID");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
System.out.println("Error: Illegal option " + flag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (i < args.length)
|
||||
path = args[i];
|
||||
else {
|
||||
path = createDefaultDirectory();
|
||||
if (path == null)
|
||||
return;
|
||||
}
|
||||
|
||||
executeCommands(export, persist, id, path);
|
||||
}
|
||||
|
||||
private static void printUsage() {
|
||||
System.out.println("Usage: {-e|-i <persist>} [-s <id>] [PATH]");
|
||||
System.out.println("Import/export recording(s) to/from PATH. The default PATH is "
|
||||
+ "\n/var/bigbluebutton/published/presentation");
|
||||
System.out.println("-e export recording(s)");
|
||||
System.out.println(
|
||||
"-i <persist> import recording(s) and indicate if they should be persisted [true|false]");
|
||||
System.out.println("-s <id> ID of single recording to be imported/exported");
|
||||
}
|
||||
|
||||
private static String createDefaultDirectory() {
|
||||
Path root = Paths.get(System.getProperty("user.dir")).getFileSystem().getRootDirectories().iterator().next();
|
||||
String path = root.toAbsolutePath() + "var/bigbluebutton/published/presentation";
|
||||
|
||||
File directory = new File(path);
|
||||
if (!directory.exists()) {
|
||||
boolean created = directory.mkdirs();
|
||||
if (!created) {
|
||||
System.out.println("Error: Failed to create default presentation directory");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private static void executeCommands(boolean export, boolean persist, String id, String path) {
|
||||
if (!export) {
|
||||
RecordingImportHandler handler = RecordingImportHandler.getInstance();
|
||||
if (id == null || id.isEmpty())
|
||||
handler.importRecordings(path, persist);
|
||||
else
|
||||
handler.importRecording(path, id, persist);
|
||||
} else {
|
||||
RecordingExportHandler handler = RecordingExportHandler.getInstance();
|
||||
if (id == null || id.isEmpty())
|
||||
handler.exportRecordings(path);
|
||||
else
|
||||
handler.exportRecording(id, path);
|
||||
}
|
||||
}
|
||||
|
||||
private static void interactiveMode() {
|
||||
System.out.println("Use this application to import and export recording metadata");
|
||||
|
||||
do {
|
||||
int impex = getResponse("Are you importing or exporting recordings? (1-Import 2-Export 3-Quit) ",
|
||||
new int[] { 1, 2, 3 }, "Please enter either 1, 2, or 3");
|
||||
|
||||
if (impex == 1) {
|
||||
importRecordings();
|
||||
} else if (impex == 2) {
|
||||
exportRecordings();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} while (true);
|
||||
}
|
||||
|
||||
private static void importRecordings() {
|
||||
RecordingImportHandler handler = RecordingImportHandler.getInstance();
|
||||
int importIndividually = getResponse("Are you importing recordings individually? (1-Yes 2-No) ",
|
||||
new int[] { 1, 2 }, "Please enter either 1 or 2");
|
||||
int persist = getResponse("Should the imported recording(s) be persisted? (1-Yes 2-No) ", new int[] { 1, 2 },
|
||||
"Please enter either 1 or 2");
|
||||
boolean shouldPersist = persist == 1;
|
||||
|
||||
if (importIndividually == 1) {
|
||||
do {
|
||||
String path = getResponse(
|
||||
"Please enter the path to the recording metadata.xml file (enter q to quit): ");
|
||||
|
||||
if (path.equalsIgnoreCase("q") || path.equalsIgnoreCase("quit"))
|
||||
break;
|
||||
|
||||
String recordingId = getResponse("Please enter the ID of the recording: ");
|
||||
handler.importRecording(path, recordingId, shouldPersist);
|
||||
} while (true);
|
||||
} else {
|
||||
String path = getResponse("Please enter the path to the directory containing the metadata.xml files: ");
|
||||
handler.importRecordings(path, shouldPersist);
|
||||
}
|
||||
}
|
||||
|
||||
private static void exportRecordings() {
|
||||
RecordingExportHandler handler = RecordingExportHandler.getInstance();
|
||||
int exportAll = getResponse("Do you want to export all recordings? (1-Yes 2-No) ", new int[] { 1, 2 },
|
||||
"Please enter either 1 or 2");
|
||||
String path = getResponse("Please enter the path to the directory that the recordings should be exported to: ");
|
||||
|
||||
if (exportAll == 1) {
|
||||
handler.exportRecordings(path);
|
||||
} else {
|
||||
do {
|
||||
String response = getResponse(
|
||||
"Please enter the ID of the recording you would like to export (enter q to quit): ");
|
||||
if (response.equalsIgnoreCase("q") || response.equalsIgnoreCase("quit"))
|
||||
break;
|
||||
handler.exportRecording(response, path);
|
||||
} while (true);
|
||||
}
|
||||
}
|
||||
|
||||
private static int getResponse(String prompt, int[] options, String error) {
|
||||
Console console = System.console();
|
||||
String response;
|
||||
int result;
|
||||
do {
|
||||
response = console.readLine(prompt);
|
||||
result = parseResponse(response, error);
|
||||
} while (!contains(options, result));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String getResponse(String prompt) {
|
||||
Console console = System.console();
|
||||
String response = "";
|
||||
do {
|
||||
response = console.readLine(prompt);
|
||||
} while (response == "");
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private static int parseResponse(String response, String error) {
|
||||
try {
|
||||
int parsedResponse = Integer.parseInt(response);
|
||||
return parsedResponse;
|
||||
} catch (NumberFormatException e) {
|
||||
System.out.println(error);
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static boolean contains(final int[] array, final int key) {
|
||||
return IntStream.of(array).anyMatch(x -> x == key);
|
||||
}
|
||||
}
|
120
bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingExportHandler.java
Executable file
120
bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingExportHandler.java
Executable file
@ -0,0 +1,120 @@
|
||||
package org.bigbluebutton;
|
||||
|
||||
import org.bigbluebutton.api.model.entity.Recording;
|
||||
import org.bigbluebutton.api.util.DataStore;
|
||||
import org.bigbluebutton.api.service.XmlService;
|
||||
import org.bigbluebutton.api.service.impl.XmlServiceImpl;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.InputSource;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.transform.OutputKeys;
|
||||
import javax.xml.transform.Transformer;
|
||||
import javax.xml.transform.TransformerFactory;
|
||||
import javax.xml.transform.dom.DOMSource;
|
||||
import javax.xml.transform.stream.StreamResult;
|
||||
import javax.xml.xpath.XPath;
|
||||
import javax.xml.xpath.XPathConstants;
|
||||
import javax.xml.xpath.XPathFactory;
|
||||
import java.io.File;
|
||||
import java.io.StringReader;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
|
||||
public class RecordingExportHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(RecordingExportHandler.class);
|
||||
|
||||
private static RecordingExportHandler instance;
|
||||
private DataStore dataStore;
|
||||
private XmlService xmlService;
|
||||
|
||||
private RecordingExportHandler() {
|
||||
dataStore = DataStore.getInstance();
|
||||
xmlService = new XmlServiceImpl();
|
||||
}
|
||||
|
||||
public static RecordingExportHandler getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new RecordingExportHandler();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void exportRecordings(String path) {
|
||||
List<Recording> recordings = dataStore.findAll(Recording.class);
|
||||
|
||||
for (Recording recording : recordings) {
|
||||
exportRecording(recording, path);
|
||||
}
|
||||
}
|
||||
|
||||
public void exportRecording(String recordId, String path) {
|
||||
Recording recording = null;
|
||||
if (recordId != null) {
|
||||
recording = dataStore.findRecordingByRecordId(recordId);
|
||||
}
|
||||
|
||||
if (recording != null) {
|
||||
exportRecording(recording, path);
|
||||
}
|
||||
}
|
||||
|
||||
private void exportRecording(Recording recording, String path) {
|
||||
logger.info("Attempting to export recording {} to {}", recording.getRecordId(), path);
|
||||
try {
|
||||
|
||||
Path dirPath = Paths.get(path);
|
||||
File dir = new File(dirPath.toAbsolutePath() + File.separator + recording.getRecordId());
|
||||
logger.info("Checking if directory {} exists", dir.getAbsolutePath());
|
||||
if (!dir.exists()) {
|
||||
logger.info("Directory does not exist, creating");
|
||||
dir.mkdir();
|
||||
}
|
||||
|
||||
File file = new File(dir + File.separator + "metadata.xml");
|
||||
logger.info("Attempting to create file {}", file.getAbsolutePath());
|
||||
boolean fileCreated = file.createNewFile();
|
||||
|
||||
if (fileCreated) {
|
||||
logger.info("Exporting {}", recording);
|
||||
|
||||
String xml = xmlService.recordingToXml(recording);
|
||||
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
Document document = builder.parse(new InputSource(new StringReader(xml)));
|
||||
|
||||
document.normalize();
|
||||
XPath xPath = XPathFactory.newInstance().newXPath();
|
||||
NodeList nodeList = (NodeList) xPath.evaluate("//text()[normalize-space()='']", document,
|
||||
XPathConstants.NODESET);
|
||||
|
||||
for (int i = 0; i < nodeList.getLength(); i++) {
|
||||
Node node = nodeList.item(i);
|
||||
node.getParentNode().removeChild(node);
|
||||
}
|
||||
|
||||
TransformerFactory transformerFactory = TransformerFactory.newInstance();
|
||||
Transformer transformer = transformerFactory.newTransformer();
|
||||
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
|
||||
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
|
||||
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
|
||||
transformer.setOutputProperty(OutputKeys.STANDALONE, "no");
|
||||
DOMSource source = new DOMSource(document);
|
||||
|
||||
StreamResult result = new StreamResult(file);
|
||||
transformer.transform(source, result);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to export recording {}", recording.getRecordId());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
package org.bigbluebutton;
|
||||
|
||||
import org.bigbluebutton.api.model.entity.*;
|
||||
import org.bigbluebutton.api.util.DataStore;
|
||||
import org.bigbluebutton.api.service.XmlService;
|
||||
import org.bigbluebutton.api.service.impl.XmlServiceImpl;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.w3c.dom.CharacterData;
|
||||
import org.w3c.dom.*;
|
||||
import org.xml.sax.InputSource;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
public class RecordingImportHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(RecordingImportHandler.class);
|
||||
|
||||
private static RecordingImportHandler instance;
|
||||
private DataStore dataStore;
|
||||
private XmlService xmlService;
|
||||
|
||||
private RecordingImportHandler() {
|
||||
dataStore = DataStore.getInstance();
|
||||
xmlService = new XmlServiceImpl();
|
||||
}
|
||||
|
||||
public static RecordingImportHandler getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new RecordingImportHandler();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void importRecordings(String directory, boolean persist) {
|
||||
logger.info("Attempting to import recordings from {}", directory);
|
||||
|
||||
String[] entries = new File(directory).list();
|
||||
|
||||
if (entries == null || entries.length == 0) {
|
||||
logger.info("No recordings were found in the provided directory");
|
||||
return;
|
||||
}
|
||||
|
||||
for (String entry : entries) {
|
||||
Recording recording = dataStore.findRecordingByRecordId(entry);
|
||||
if (recording != null && persist) {
|
||||
logger.info("Record found for {}. Skipping", entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
String path = directory + "/" + entry + "/metadata.xml";
|
||||
importRecording(path, entry, persist);
|
||||
}
|
||||
}
|
||||
|
||||
public Recording importRecording(String path, String recordId, boolean persist) {
|
||||
logger.info("Attempting to import {}", path);
|
||||
|
||||
String content = null;
|
||||
try {
|
||||
byte[] encoded = Files.readAllBytes(Paths.get(path));
|
||||
content = new String(encoded, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to import {}", path);
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
Recording recording = null;
|
||||
|
||||
if (content != null) {
|
||||
logger.info("File content: {}", content);
|
||||
recording = xmlService.xmlToRecording(recordId, content);
|
||||
}
|
||||
|
||||
if (recording != null) {
|
||||
if (persist)
|
||||
dataStore.save(recording);
|
||||
}
|
||||
|
||||
return recording;
|
||||
}
|
||||
}
|
47
bbb-recording-imex/src/main/resources/logback.xml
Executable file
47
bbb-recording-imex/src/main/resources/logback.xml
Executable file
@ -0,0 +1,47 @@
|
||||
<configuration>
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<layout class="ch.qos.logback.classic.PatternLayout">
|
||||
<Pattern>
|
||||
%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n
|
||||
</Pattern>
|
||||
</layout>
|
||||
</appender>
|
||||
|
||||
<property name="HOME_LOG" value="logs/app.log"/>
|
||||
|
||||
<appender name="FILE-ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${HOME_LOG}</file>
|
||||
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>logs/archived/app.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
|
||||
<!-- each archived file, size max 10MB -->
|
||||
<maxFileSize>10MB</maxFileSize>
|
||||
<!-- total size of all archive files, if total size > 20GB, it will delete old archived file -->
|
||||
<totalSizeCap>20GB</totalSizeCap>
|
||||
<!-- 60 days to keep -->
|
||||
<maxHistory>60</maxHistory>
|
||||
</rollingPolicy>
|
||||
|
||||
<encoder>
|
||||
<pattern>%d %p %c{1.} [%t] %m%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="rollingFileLogger" level="debug" additivity="false">
|
||||
<appender-ref ref="FILE-ROLLING"/>
|
||||
</logger>
|
||||
|
||||
<logger name="consoleLogger" level="debug">
|
||||
<appender-ref ref="CONSOLE" />
|
||||
</logger>
|
||||
|
||||
<logger name="org.hibernate.SQL" level="ERROR"/>
|
||||
<logger name="org.hibernate.type" level="INFO"/>
|
||||
|
||||
<root level="info">
|
||||
<appender-ref ref="FILE-ROLLING"/>
|
||||
<appender-ref ref="CONSOLE" />
|
||||
</root>
|
||||
|
||||
</configuration>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user