From 3b32fb02b5c9dc33c5aa445058477a518234580e Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Thu, 13 Jan 2022 12:45:22 +0100 Subject: [PATCH 001/268] Initial implementation of MakePresentationWithAnnotationDownloadReqMsg --- .../core/running/MeetingActor.scala | 38 ++++++++++++++++--- .../common2/msgs/PresentationMsgs.scala | 10 ++++- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index 79b9267e5f..4d40528f5f 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -418,13 +418,14 @@ class MeetingActor( updateModeratorsPresence() // Whiteboard - case m: SendCursorPositionPubMsg => wbApp.handle(m, liveMeeting, msgBus) - case m: ClearWhiteboardPubMsg => wbApp.handle(m, liveMeeting, msgBus) - case m: UndoWhiteboardPubMsg => wbApp.handle(m, liveMeeting, msgBus) - case m: ModifyWhiteboardAccessPubMsg => wbApp.handle(m, liveMeeting, msgBus) - case m: SendWhiteboardAnnotationPubMsg => wbApp.handle(m, liveMeeting, msgBus) + case m: SendCursorPositionPubMsg => wbApp.handle(m, liveMeeting, msgBus) + case m: ClearWhiteboardPubMsg => wbApp.handle(m, liveMeeting, msgBus) + case m: UndoWhiteboardPubMsg => wbApp.handle(m, liveMeeting, msgBus) + case m: ModifyWhiteboardAccessPubMsg => wbApp.handle(m, liveMeeting, msgBus) + case m: SendWhiteboardAnnotationPubMsg => + handleMakePresentationWithAnnotationDownloadReqMsg(liveMeeting) + wbApp.handle(m, liveMeeting, msgBus) case m: GetWhiteboardAnnotationsReqMsg => wbApp.handle(m, liveMeeting, msgBus) - // Poll case m: StartPollReqMsg => pollApp.handle(m, state, liveMeeting, msgBus) // passing state but not modifying it @@ -498,6 +499,8 @@ class MeetingActor( // Presentation case m: PreuploadedPresentationsSysPubMsg => presentationApp2x.handle(m, liveMeeting, msgBus) case m: AssignPresenterReqMsg => state = handlePresenterChange(m, state) + case m: MakePresentationWithAnnotationDownloadReqMsg => handleMakePresentationWithAnnotationDownloadReqMsg(liveMeeting) + case m: ExportPresentationWithAnnotationReqMsg => handleExportPresentationWithAnnotationReqMsg(liveMeeting) // Presentation Pods case m: CreateNewPresentationPodPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) @@ -716,6 +719,29 @@ class MeetingActor( } + def handleMakePresentationWithAnnotationDownloadReqMsg(liveMeeting: LiveMeeting): Unit = { + log.warning("*** Hello World! ***") + + liveMeeting.presModel.getPresentations foreach println + + val presentationId = getMeetingInfoPresentationDetails()._1 // current presentationId = whiteboardId + // log.warning(liveMeeting.wbModel.getHistory()) + + println(presentationId) + + log.warning("*****") + + // 1) Insert Export Job to Redis + // 2) Export Annotations to Redis + + } + + def handleExportPresentationWithAnnotationReqMsg(liveMeeting: LiveMeeting): Unit = { + log.warning("***** Hello World 2! ") + // 1) Insert Export Job to Redis + // 2) Export Annotations to Redis + } + def handleDeskShareGetDeskShareInfoRequest(msg: DeskShareGetDeskShareInfoRequest): Unit = { log.info("handleDeskShareGetDeskShareInfoRequest: " + msg.conferenceName + "isBroadcasting=" + ScreenshareModel.isBroadcastingRTMP(liveMeeting.screenshareModel) + " URL:" + diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationMsgs.scala index 9ec70e1c72..8b60e19f72 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationMsgs.scala @@ -3,13 +3,21 @@ 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) +case class MakePresentationWithAnnotationDownloadReqMsgBody(presId: String, allPages: Boolean, pages: List[Int]) + +object ExportPresentationWithAnnotationReqMsg { val NAME = "ExportPresentationWithAnnotationReqMsg" } +case class ExportPresentationWithAnnotationReqMsg(header: BbbClientMsgHeader, body: ExportPresentationWithAnnotationReqMsgBody) +case class ExportPresentationWithAnnotationReqMsgBody(parentMeetingId: String, allPages: Boolean) + // ------------ bbb-common-web to akka-apps ------------ // ------------ akka-apps to client ------------ From 56669cab9870bc5dde4aca43fac5da64b12894ea Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Sun, 16 Jan 2022 16:56:46 +0100 Subject: [PATCH 002/268] Button in HTML5 interface that emits activity sign --- .../core/running/MeetingActor.scala | 27 +++++++++++---- .../presentation-toolbar/component.jsx | 34 ++++++++++++++++++- .../presentation-toolbar/container.jsx | 2 ++ .../presentation-toolbar/service.js | 5 +++ .../presentation-toolbar/styles.js | 29 ++++++++++++++++ bigbluebutton-html5/public/locales/en.json | 1 + 6 files changed, 91 insertions(+), 7 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index 4d40528f5f..82d8c6ddfd 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -423,8 +423,8 @@ class MeetingActor( case m: UndoWhiteboardPubMsg => wbApp.handle(m, liveMeeting, msgBus) case m: ModifyWhiteboardAccessPubMsg => wbApp.handle(m, liveMeeting, msgBus) case m: SendWhiteboardAnnotationPubMsg => - handleMakePresentationWithAnnotationDownloadReqMsg(liveMeeting) wbApp.handle(m, liveMeeting, msgBus) + handleMakePresentationWithAnnotationDownloadReqMsg(liveMeeting) case m: GetWhiteboardAnnotationsReqMsg => wbApp.handle(m, liveMeeting, msgBus) // Poll case m: StartPollReqMsg => @@ -720,16 +720,31 @@ class MeetingActor( } def handleMakePresentationWithAnnotationDownloadReqMsg(liveMeeting: LiveMeeting): Unit = { - log.warning("*** Hello World! ***") + println("*** Current Whiteboard State ***") liveMeeting.presModel.getPresentations foreach println - val presentationId = getMeetingInfoPresentationDetails()._1 // current presentationId = whiteboardId - // log.warning(liveMeeting.wbModel.getHistory()) + val pageNumber: String = "1" + val whiteboardId: String = getMeetingInfoPresentationDetails().id + s"/$pageNumber" - println(presentationId) + println("Whiteboard ID is: " + whiteboardId) - log.warning("*****") + val whiteboardHistory = liveMeeting.wbModel.getHistory(whiteboardId) + + println("Whiteboard history length: " + whiteboardHistory.length) + println("============================") + + for (drawing <- whiteboardHistory) { + println(drawing.id) + println(drawing.status) + println(drawing.annotationType) + println(drawing.annotationInfo) + println(drawing.wbId) + println(drawing.userId) + println(drawing.position) + } + + println("*****") // 1) Insert Export Job to Redis // 2) Export Annotations to Redis diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx index c1047caa89..a48513f85a 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx @@ -8,8 +8,13 @@ import Styled from './styles'; import ZoomTool from './zoom-tool/component'; import TooltipContainer from '/imports/ui/components/tooltip/container'; import KEY_CODES from '/imports/utils/keyCodes'; +import { ThemeConsumer } from 'styled-components'; const intlMessages = defineMessages({ + downloadAnnotatedSlidesLabel: { + id: 'app.presentation.presentationToolbar.downloadSlideWithAnnotations', + description: 'Download slide with annotations label' + }, previousSlideLabel: { id: 'app.presentation.presentationToolbar.prevSlideLabel', description: 'Previous slide button label', @@ -75,7 +80,7 @@ const intlMessages = defineMessages({ class PresentationToolbar extends PureComponent { constructor(props) { super(props); - + this.handleDownloadAnnotatedSlides = this.handleDownloadAnnotatedSlides.bind(this); this.handleSkipToSlideChange = this.handleSkipToSlideChange.bind(this); this.change = this.change.bind(this); this.renderAriaDescs = this.renderAriaDescs.bind(this); @@ -115,6 +120,14 @@ class PresentationToolbar extends PureComponent { } } + handleDownloadAnnotatedSlides() { + const { + downloadAnnotatedSlides + } = this.props; + + downloadAnnotatedSlides() + } + handleSkipToSlideChange(event) { const { skipToSlide, @@ -187,6 +200,9 @@ class PresentationToolbar extends PureComponent {
{intl.formatMessage(intlMessages.nextSlideDesc)}
+
+ {intl.formatMessage(intlMessages.downloadAnnotatedSlidesLabel)} +
{intl.formatMessage(intlMessages.noNextSlideDesc)}
@@ -253,6 +269,8 @@ class PresentationToolbar extends PureComponent { ? intl.formatMessage(intlMessages.nextSlideLabel) : `${intl.formatMessage(intlMessages.nextSlideLabel)} (${currentSlideNum >= 1 ? (currentSlideNum + 1) : ''})`; + const downloadAnnotatedSlidesLabel = intl.formatMessage(intlMessages.downloadAnnotatedSlidesLabel); + return ( + } { @@ -381,6 +412,7 @@ PresentationToolbar.propTypes = { nextSlide: PropTypes.func.isRequired, previousSlide: PropTypes.func.isRequired, skipToSlide: PropTypes.func.isRequired, + downloadAnnotatedSlides: PropTypes.func.isRequired, intl: PropTypes.shape({ formatMessage: PropTypes.func.isRequired, }).isRequired, diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx index d68647a6c7..44e2978b6b 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx @@ -53,6 +53,7 @@ export default withTracker((params) => { nextSlide: PresentationToolbarService.nextSlide, previousSlide: PresentationToolbarService.previousSlide, skipToSlide: PresentationToolbarService.skipToSlide, + downloadAnnotatedSlides: PresentationToolbarService.downloadAnnotatedSlides, isMeteorConnected: Meteor.status().connected, isPollingEnabled: POLLING_ENABLED, currentSlidHasContent: PresentationService.currentSlidHasContent(), @@ -74,4 +75,5 @@ PresentationToolbarContainer.propTypes = { nextSlide: PropTypes.func.isRequired, previousSlide: PropTypes.func.isRequired, skipToSlide: PropTypes.func.isRequired, + downloadAnnotatedSlides: PropTypes.func.isRequired, }; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js index e8ca731e59..f240e85de1 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js @@ -29,6 +29,10 @@ const nextSlide = (currentSlideNum, numberOfSlides, podId) => { } }; +const downloadAnnotatedSlides = () => { + makeCall('userActivitySign') +} + const zoomSlide = throttle((currentSlideNum, podId, widthRatio, heightRatio, xOffset, yOffset) => { makeCall('zoomSlide', currentSlideNum, podId, widthRatio, heightRatio, xOffset, yOffset); }, PAN_ZOOM_INTERVAL); @@ -40,6 +44,7 @@ const skipToSlide = (requestedSlideNum, podId) => { export default { getNumberOfSlides, nextSlide, + downloadAnnotatedSlides, previousSlide, skipToSlide, zoomSlide, diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.js b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.js index b71311aa7d..7f5fa6916a 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.js @@ -140,6 +140,34 @@ const NextSlideButton = styled(Button)` } `; +const DownloadAnnotatedSlidesButton = styled(Button)` + border: none !important; + + & > i { + font-size: 1rem; + + [dir="rtl"] & { + -webkit-transform: scale(-1, 1); + -moz-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + -o-transform: scale(-1, 1); + transform: scale(-1, 1); + } + } + + position: relative; + color: ${toolbarButtonColor}; + background-color: ${colorOffWhite}; + border-radius: 0; + box-shadow: none !important; + border: 0; + + &:focus { + background-color: ${colorOffWhite}; + border: 0; + } +`; + const SkipSlideSelect = styled.select` padding: 0 ${smPaddingY}; margin: ${borderSize}; @@ -226,6 +254,7 @@ export default { PresentationSlideControls, PrevSlideButton, NextSlideButton, + DownloadAnnotatedSlidesButton, SkipSlideSelect, PresentationZoomControls, FitToWidthButton, diff --git a/bigbluebutton-html5/public/locales/en.json b/bigbluebutton-html5/public/locales/en.json index 48412c3c5b..f87c87fc5f 100755 --- a/bigbluebutton-html5/public/locales/en.json +++ b/bigbluebutton-html5/public/locales/en.json @@ -169,6 +169,7 @@ "app.presentation.presentationToolbar.prevSlideDesc": "Change the presentation to the previous slide", "app.presentation.presentationToolbar.nextSlideLabel": "Next slide", "app.presentation.presentationToolbar.nextSlideDesc": "Change the presentation to the next slide", + "app.presentation.presentationToolbar.downloadSlideWithAnnotations": "Download slide with annotations", "app.presentation.presentationToolbar.skipSlideLabel": "Skip slide", "app.presentation.presentationToolbar.skipSlideDesc": "Change the presentation to a specific slide", "app.presentation.presentationToolbar.fitWidthLabel": "Fit to width", From 4442da1aa5754726d51867475b01a648fe2f9e9a Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Sun, 16 Jan 2022 19:43:13 +0100 Subject: [PATCH 003/268] Send makePresentationWithAnnotationDownloadReqMsg to Redis PubSub --- .../core/running/MeetingActor.scala | 2 +- .../imports/api/annotations/server/methods.js | 2 + ...resentationWithAnnotationDownloadReqMsg.js | 38 +++++++++++++++++++ .../presentation-toolbar/component.jsx | 1 - .../presentation-toolbar/service.js | 1 + 5 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index 82d8c6ddfd..00c65cd97c 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -424,7 +424,7 @@ class MeetingActor( case m: ModifyWhiteboardAccessPubMsg => wbApp.handle(m, liveMeeting, msgBus) case m: SendWhiteboardAnnotationPubMsg => wbApp.handle(m, liveMeeting, msgBus) - handleMakePresentationWithAnnotationDownloadReqMsg(liveMeeting) + // handleMakePresentationWithAnnotationDownloadReqMsg(liveMeeting) case m: GetWhiteboardAnnotationsReqMsg => wbApp.handle(m, liveMeeting, msgBus) // Poll case m: StartPollReqMsg => diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods.js b/bigbluebutton-html5/imports/api/annotations/server/methods.js index beb862327f..e535558237 100644 --- a/bigbluebutton-html5/imports/api/annotations/server/methods.js +++ b/bigbluebutton-html5/imports/api/annotations/server/methods.js @@ -3,10 +3,12 @@ import undoAnnotation from './methods/undoAnnotation'; import clearWhiteboard from './methods/clearWhiteboard'; import sendAnnotation from './methods/sendAnnotation'; import sendBulkAnnotations from './methods/sendBulkAnnotations'; +import makePresentationWithAnnotationDownloadReqMsg from './methods/makePresentationWithAnnotationDownloadReqMsg' Meteor.methods({ undoAnnotation, clearWhiteboard, sendAnnotation, sendBulkAnnotations, + makePresentationWithAnnotationDownloadReqMsg, }); diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js b/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js new file mode 100644 index 0000000000..faa766c805 --- /dev/null +++ b/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js @@ -0,0 +1,38 @@ +import RedisPubSub from '/imports/startup/server/redis'; +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import { extractCredentials } from '/imports/api/common/server/helpers'; +import Logger from '/imports/startup/server/logger'; + +export default function makePresentationWithAnnotationDownloadReqMsg() { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'MakePresentationWithAnnotationDownloadReqMsg'; + + Logger.warn(`IVE GOTTEN ALL THE WAY HERE! LELLL`); + + try { + + const { meetingId, requesterUserId } = extractCredentials(this.userId); + + Logger.warn(`ok!`); + check(meetingId, String); + check(requesterUserId, String); + // check(whiteboardId, String); + Logger.warn(`ok2`); + const payload = { + // whiteboardId + }; + Logger.warn('************'); + Logger.warn(CHANNEL) + Logger.warn(EVENT_NAME) + Logger.warn(meetingId) + Logger.warn(requesterUserId) + Logger.warn(payload) + Logger.warn('************'); + + return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); + } catch (err) { + Logger.error(`Exception while invoking method makePresentationWithAnnotationDownloadReqMsg ${err.stack}`); + } +} diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx index a48513f85a..1613573b99 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx @@ -124,7 +124,6 @@ class PresentationToolbar extends PureComponent { const { downloadAnnotatedSlides } = this.props; - downloadAnnotatedSlides() } diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js index f240e85de1..a7b9b2ac9b 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js @@ -31,6 +31,7 @@ const nextSlide = (currentSlideNum, numberOfSlides, podId) => { const downloadAnnotatedSlides = () => { makeCall('userActivitySign') + makeCall('makePresentationWithAnnotationDownloadReqMsg') } const zoomSlide = throttle((currentSlideNum, podId, widthRatio, heightRatio, xOffset, yOffset) => { From fea9afab5c1690dff231b30981164c3be6eac092 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Tue, 18 Jan 2022 13:07:49 +0100 Subject: [PATCH 004/268] Send out makePresentationWithAnnotationDownloadReqMsg from client --- ...resentationWithAnnotationDownloadReqMsg.js | 20 +++++++++++-------- .../presentation-toolbar/component.jsx | 4 ++-- .../presentation-toolbar/service.js | 2 +- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js b/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js index faa766c805..15c6cd3f1d 100644 --- a/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js +++ b/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js @@ -9,26 +9,30 @@ export default function makePresentationWithAnnotationDownloadReqMsg() { const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'MakePresentationWithAnnotationDownloadReqMsg'; - Logger.warn(`IVE GOTTEN ALL THE WAY HERE! LELLL`); - try { - const { meetingId, requesterUserId } = extractCredentials(this.userId); - Logger.warn(`ok!`); check(meetingId, String); check(requesterUserId, String); - // check(whiteboardId, String); - Logger.warn(`ok2`); + + presentationId = "placeholder-val"; + allPages = true; + pages = []; + const payload = { - // whiteboardId + presentationId, + allPages, + pages, }; + Logger.warn('************'); Logger.warn(CHANNEL) Logger.warn(EVENT_NAME) Logger.warn(meetingId) Logger.warn(requesterUserId) - Logger.warn(payload) + Logger.warn(presentationId) + Logger.warn(allPages) + Logger.warn(pages) Logger.warn('************'); return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx index 1613573b99..2178411e39 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx @@ -8,7 +8,6 @@ import Styled from './styles'; import ZoomTool from './zoom-tool/component'; import TooltipContainer from '/imports/ui/components/tooltip/container'; import KEY_CODES from '/imports/utils/keyCodes'; -import { ThemeConsumer } from 'styled-components'; const intlMessages = defineMessages({ downloadAnnotatedSlidesLabel: { @@ -122,8 +121,9 @@ class PresentationToolbar extends PureComponent { handleDownloadAnnotatedSlides() { const { - downloadAnnotatedSlides + downloadAnnotatedSlides } = this.props; + downloadAnnotatedSlides() } diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js index a7b9b2ac9b..13581613da 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js @@ -30,8 +30,8 @@ const nextSlide = (currentSlideNum, numberOfSlides, podId) => { }; const downloadAnnotatedSlides = () => { - makeCall('userActivitySign') makeCall('makePresentationWithAnnotationDownloadReqMsg') + makeCall('userActivitySign') } const zoomSlide = throttle((currentSlideNum, podId, widthRatio, heightRatio, xOffset, yOffset) => { From 22d50a7572e6f1bb2c4dad866601b6a40dbd49db Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Tue, 18 Jan 2022 16:05:02 +0100 Subject: [PATCH 005/268] MakePresentationWithAnnotationDownloadReqMsg with parameters in MeetingActor --- .../senders/ReceivedJsonMsgHandlerActor.scala | 2 ++ .../bigbluebutton/core/running/MeetingActor.scala | 10 +++++++--- .../common2/msgs/PresentationMsgs.scala | 4 ++-- .../makePresentationWithAnnotationDownloadReqMsg.js | 13 +++---------- .../client/meeting/AllowedMessageNames.scala | 2 ++ 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala index 81bb0c9b6d..ef2d00ab95 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala @@ -266,6 +266,8 @@ class ReceivedJsonMsgHandlerActor( routeGenericMsg[PdfConversionInvalidErrorSysPubMsg](envelope, jsonNode) case AssignPresenterReqMsg.NAME => routeGenericMsg[AssignPresenterReqMsg](envelope, jsonNode) + case MakePresentationWithAnnotationDownloadReqMsg.NAME => + routeGenericMsg[MakePresentationWithAnnotationDownloadReqMsg](envelope, jsonNode) // Presentation Pods case CreateNewPresentationPodPubMsg.NAME => diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index 00c65cd97c..57cd0f5230 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -424,7 +424,6 @@ class MeetingActor( case m: ModifyWhiteboardAccessPubMsg => wbApp.handle(m, liveMeeting, msgBus) case m: SendWhiteboardAnnotationPubMsg => wbApp.handle(m, liveMeeting, msgBus) - // handleMakePresentationWithAnnotationDownloadReqMsg(liveMeeting) case m: GetWhiteboardAnnotationsReqMsg => wbApp.handle(m, liveMeeting, msgBus) // Poll case m: StartPollReqMsg => @@ -499,7 +498,7 @@ class MeetingActor( // Presentation case m: PreuploadedPresentationsSysPubMsg => presentationApp2x.handle(m, liveMeeting, msgBus) case m: AssignPresenterReqMsg => state = handlePresenterChange(m, state) - case m: MakePresentationWithAnnotationDownloadReqMsg => handleMakePresentationWithAnnotationDownloadReqMsg(liveMeeting) + case m: MakePresentationWithAnnotationDownloadReqMsg => handleMakePresentationWithAnnotationDownloadReqMsg(m, liveMeeting) case m: ExportPresentationWithAnnotationReqMsg => handleExportPresentationWithAnnotationReqMsg(liveMeeting) // Presentation Pods @@ -719,7 +718,7 @@ class MeetingActor( } - def handleMakePresentationWithAnnotationDownloadReqMsg(liveMeeting: LiveMeeting): Unit = { + def handleMakePresentationWithAnnotationDownloadReqMsg(m: MakePresentationWithAnnotationDownloadReqMsg, liveMeeting: LiveMeeting): Unit = { println("*** Current Whiteboard State ***") liveMeeting.presModel.getPresentations foreach println @@ -727,6 +726,11 @@ class MeetingActor( val pageNumber: String = "1" val whiteboardId: String = getMeetingInfoPresentationDetails().id + s"/$pageNumber" + println("") + println(m) + println("") + + println("") println("Whiteboard ID is: " + whiteboardId) val whiteboardHistory = liveMeeting.wbModel.getHistory(whiteboardId) diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationMsgs.scala index 8b60e19f72..4c508064d8 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationMsgs.scala @@ -11,11 +11,11 @@ case class PreuploadedPresentationsSysPubMsg(header: BbbClientMsgHeader, body: P case class PreuploadedPresentationsSysPubMsgBody(presentations: Vector[PresentationVO]) object MakePresentationWithAnnotationDownloadReqMsg { val NAME = "MakePresentationWithAnnotationDownloadReqMsg" } -case class MakePresentationWithAnnotationDownloadReqMsg(header: BbbClientMsgHeader, body: MakePresentationWithAnnotationDownloadReqMsgBody) +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) +case class ExportPresentationWithAnnotationReqMsg(header: BbbClientMsgHeader, body: ExportPresentationWithAnnotationReqMsgBody) extends StandardMsg case class ExportPresentationWithAnnotationReqMsgBody(parentMeetingId: String, allPages: Boolean) // ------------ bbb-common-web to akka-apps ------------ diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js b/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js index 15c6cd3f1d..13b2eb579d 100644 --- a/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js +++ b/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js @@ -15,14 +15,10 @@ export default function makePresentationWithAnnotationDownloadReqMsg() { check(meetingId, String); check(requesterUserId, String); - presentationId = "placeholder-val"; - allPages = true; - pages = []; - const payload = { - presentationId, - allPages, - pages, + presId: "placeholder-val", + allPages: true, + pages: [], }; Logger.warn('************'); @@ -30,9 +26,6 @@ export default function makePresentationWithAnnotationDownloadReqMsg() { Logger.warn(EVENT_NAME) Logger.warn(meetingId) Logger.warn(requesterUserId) - Logger.warn(presentationId) - Logger.warn(allPages) - Logger.warn(pages) Logger.warn('************'); return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); diff --git a/labs/vertx-akka/src/main/scala/org/bigbluebutton/client/meeting/AllowedMessageNames.scala b/labs/vertx-akka/src/main/scala/org/bigbluebutton/client/meeting/AllowedMessageNames.scala index cd7b18454c..cc4153a8f5 100755 --- a/labs/vertx-akka/src/main/scala/org/bigbluebutton/client/meeting/AllowedMessageNames.scala +++ b/labs/vertx-akka/src/main/scala/org/bigbluebutton/client/meeting/AllowedMessageNames.scala @@ -61,6 +61,8 @@ object AllowedMessageNames { CreateNewPresentationPodPubMsg.NAME, RemovePresentationPodPubMsg.NAME, SetPresenterInPodReqMsg.NAME, + MakePresentationWithAnnotationDownloadReqMsg.NAME, + ExportPresentationWithAnnotationReqMsg.NAME, // Whiteboard Messages ModifyWhiteboardAccessPubMsg.NAME, From e1489bd3d74ae057bd29568b5518e417684d1e1a Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Wed, 19 Jan 2022 13:52:12 +0100 Subject: [PATCH 006/268] Get number of pages in current presentation --- .../core/running/MeetingActor.scala | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index 57cd0f5230..1ca9ce153c 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -22,6 +22,7 @@ import org.bigbluebutton.core.apps.users.UsersApp2x import org.bigbluebutton.core.apps.whiteboard.WhiteboardApp2x import org.bigbluebutton.core.bus._ import org.bigbluebutton.core.models.{ Users2x, VoiceUsers, _ } +import org.bigbluebutton.core.util.RandomStringGenerator import org.bigbluebutton.core2.{ MeetingStatus2x, Permissions } import org.bigbluebutton.core2.message.handlers._ import org.bigbluebutton.core2.message.handlers.meeting._ @@ -498,7 +499,7 @@ class MeetingActor( // Presentation case m: PreuploadedPresentationsSysPubMsg => presentationApp2x.handle(m, liveMeeting, msgBus) case m: AssignPresenterReqMsg => state = handlePresenterChange(m, state) - case m: MakePresentationWithAnnotationDownloadReqMsg => handleMakePresentationWithAnnotationDownloadReqMsg(m, liveMeeting) + case m: MakePresentationWithAnnotationDownloadReqMsg => handleMakePresentationWithAnnotationDownloadReqMsg(m, state, liveMeeting) case m: ExportPresentationWithAnnotationReqMsg => handleExportPresentationWithAnnotationReqMsg(liveMeeting) // Presentation Pods @@ -718,7 +719,7 @@ class MeetingActor( } - def handleMakePresentationWithAnnotationDownloadReqMsg(m: MakePresentationWithAnnotationDownloadReqMsg, liveMeeting: LiveMeeting): Unit = { + def handleMakePresentationWithAnnotationDownloadReqMsg(m: MakePresentationWithAnnotationDownloadReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = { println("*** Current Whiteboard State ***") liveMeeting.presModel.getPresentations foreach println @@ -726,11 +727,13 @@ class MeetingActor( val pageNumber: String = "1" val whiteboardId: String = getMeetingInfoPresentationDetails().id + s"/$pageNumber" - println("") - println(m) - println("") + val presId = m.body.presId // Whiteboard Id + val allPages = m.body.allPages + val pages = m.body.pages - println("") + val jobId = RandomStringGenerator.randomAlphanumericString(16) + + println(jobId) println("Whiteboard ID is: " + whiteboardId) val whiteboardHistory = liveMeeting.wbModel.getHistory(whiteboardId) @@ -750,6 +753,26 @@ class MeetingActor( println("*****") + // Determine page amount + val presentationPods: Vector[PresentationPod] = state.presentationPodManager.getAllPresentationPodsInMeeting() + val currentPres = presentationPods.flatMap(_.getCurrentPresentation()) + + val pageCount = currentPres(0).pages.size + println("There are " + pageCount + " pages!") + println("---------") + + // val presentationId: String = presentationPods.flatMap(_.getCurrentPresentation.map(_.id)).mkString + // val presentationName: String = presentationPods.flatMap(_.getCurrentPresentation.map(_.name)).mkString + + println(presentationPods) + println(currentPres) + + if (allPages) { + + } else { + + } + // 1) Insert Export Job to Redis // 2) Export Annotations to Redis From 05a149da560bcb44c842e4bd168b73d2f6e73853 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Wed, 19 Jan 2022 15:32:40 +0100 Subject: [PATCH 007/268] Iterate over pages collecting annotations --- .../core/running/MeetingActor.scala | 66 ++++++++----------- 1 file changed, 28 insertions(+), 38 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index 1ca9ce153c..a59c1aa8e1 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -722,58 +722,48 @@ class MeetingActor( def handleMakePresentationWithAnnotationDownloadReqMsg(m: MakePresentationWithAnnotationDownloadReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = { println("*** Current Whiteboard State ***") - liveMeeting.presModel.getPresentations foreach println + val presId: String = m.body.presId // Whiteboard Id + val allPages: Boolean = m.body.allPages + val pages: List[Int] = m.body.pages - val pageNumber: String = "1" - val whiteboardId: String = getMeetingInfoPresentationDetails().id + s"/$pageNumber" - - val presId = m.body.presId // Whiteboard Id - val allPages = m.body.allPages - val pages = m.body.pages - - val jobId = RandomStringGenerator.randomAlphanumericString(16) - - println(jobId) - println("Whiteboard ID is: " + whiteboardId) - - val whiteboardHistory = liveMeeting.wbModel.getHistory(whiteboardId) - - println("Whiteboard history length: " + whiteboardHistory.length) - println("============================") - - for (drawing <- whiteboardHistory) { - println(drawing.id) - println(drawing.status) - println(drawing.annotationType) - println(drawing.annotationInfo) - println(drawing.wbId) - println(drawing.userId) - println(drawing.position) - } - - println("*****") + var whiteboardId: String = getMeetingInfoPresentationDetails().id // TODO: set as var whiteboardId: String only // Determine page amount val presentationPods: Vector[PresentationPod] = state.presentationPodManager.getAllPresentationPodsInMeeting() val currentPres = presentationPods.flatMap(_.getCurrentPresentation()) - val pageCount = currentPres(0).pages.size - println("There are " + pageCount + " pages!") - println("---------") + val pageCount = currentPres.head.pages.size - // val presentationId: String = presentationPods.flatMap(_.getCurrentPresentation.map(_.id)).mkString - // val presentationName: String = presentationPods.flatMap(_.getCurrentPresentation.map(_.name)).mkString + // println(presentationPods) + // println(currentPres) - println(presentationPods) - println(currentPres) + val pagesRange: List[Int] = if (allPages) (1 to pageCount).toList else pages - if (allPages) { + println(pagesRange) - } else { + for (pageNumber <- pagesRange) { + // whiteboardId = s"${presId}/${pageNumber.toString}" // TODO: use this + whiteboardId = s"${getMeetingInfoPresentationDetails().id}/${pageNumber.toString}" + val whiteboardHistory = liveMeeting.wbModel.getHistory(whiteboardId) + + for (drawing <- whiteboardHistory) { + println(drawing.id) + println(drawing.status) + println(drawing.annotationType) + println(drawing.annotationInfo) + println(drawing.wbId) + println(drawing.userId) + println(drawing.position) + } + + println("*****") + println("") } // 1) Insert Export Job to Redis + val jobId = RandomStringGenerator.randomAlphanumericString(16) + // 2) Export Annotations to Redis } From 6c13edc32c3f72bb9963f3b1e5a0544f0d41fe1b Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Mon, 24 Jan 2022 16:35:23 +0100 Subject: [PATCH 008/268] Add export job / annotations object --- .../core/models/PresentationPods.scala | 26 +++++++++ .../core/running/MeetingActor.scala | 58 +++++++++++-------- 2 files changed, 59 insertions(+), 25 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/PresentationPods.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/PresentationPods.scala index 9cf8535799..e492630a14 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/PresentationPods.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/PresentationPods.scala @@ -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) @@ -30,6 +31,31 @@ case class PresentationPage( heightRatio: Double = 100D ) +case class PresentationPageForExport( + page: Int, + xOffset: Double, + yOffset: Double, + widthRatio: Double, + heightRatio: Double, + annotations: Array[AnnotationVO], +) + +case class StoredAnnotations( + presId: String, + pages: Array[PresentationPageForExport], +) + +case class ExportJob( + jobId: String, + jobType: String, + presId: String, + presLocation: String, + allPages: Boolean, + pages: Array[PresentationPageForExport], + parentMeetingId: String, + presUploadToken: String, +) + object PresentationInPod { def addPage(pres: PresentationInPod, page: PresentationPage): PresentationInPod = { val newPages = pres.pages + (page.id -> page) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index a59c1aa8e1..de8a7808b1 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -722,54 +722,62 @@ class MeetingActor( def handleMakePresentationWithAnnotationDownloadReqMsg(m: MakePresentationWithAnnotationDownloadReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = { println("*** Current Whiteboard State ***") - val presId: String = m.body.presId // Whiteboard Id - val allPages: Boolean = m.body.allPages - val pages: List[Int] = m.body.pages + val presId: String = m.body.presId // Whiteboard ID + val allPages: Boolean = m.body.allPages // Whether or not all pages of the presentation should be exported + val pages: List[Int] = m.body.pages // Desired presentation pages for export - var whiteboardId: String = getMeetingInfoPresentationDetails().id // TODO: set as var whiteboardId: String only + var whiteboardId: String = getMeetingInfoPresentationDetails().id // TODO: use presId from message instead, remove this // Determine page amount val presentationPods: Vector[PresentationPod] = state.presentationPodManager.getAllPresentationPodsInMeeting() - val currentPres = presentationPods.flatMap(_.getCurrentPresentation()) - - val pageCount = currentPres.head.pages.size - - // println(presentationPods) - // println(currentPres) + val currentPres = presentationPods.flatMap(_.getCurrentPresentation()).head + val pageCount = currentPres.pages.size val pagesRange: List[Int] = if (allPages) (1 to pageCount).toList else pages - println(pagesRange) - + var storeAnnotationPages = new Array[PresentationPageForExport](pagesRange.size) + var index = 0 for (pageNumber <- pagesRange) { // whiteboardId = s"${presId}/${pageNumber.toString}" // TODO: use this whiteboardId = s"${getMeetingInfoPresentationDetails().id}/${pageNumber.toString}" - val whiteboardHistory = liveMeeting.wbModel.getHistory(whiteboardId) + println(currentPres.pages(whiteboardId)) - for (drawing <- whiteboardHistory) { - println(drawing.id) - println(drawing.status) - println(drawing.annotationType) - println(drawing.annotationInfo) - println(drawing.wbId) - println(drawing.userId) - println(drawing.position) - } + val presentationPage: PresentationPage = currentPres.pages(whiteboardId) + val xOffset: Double = presentationPage.xOffset + val yOffset: Double = presentationPage.yOffset + val widthRatio: Double = presentationPage.widthRatio + val heightRatio: Double = presentationPage.heightRatio + val whiteboardHistory: Array[AnnotationVO] = liveMeeting.wbModel.getHistory(whiteboardId) println("*****") println("") + + storeAnnotationPages(index) = new PresentationPageForExport(index + 1, xOffset, yOffset, widthRatio, heightRatio, whiteboardHistory) + index += 1 } - // 1) Insert Export Job to Redis - val jobId = RandomStringGenerator.randomAlphanumericString(16) + // 1) Export Annotations to Redis + var annotations = new StoredAnnotations(presId, storeAnnotationPages) + println(annotations) - // 2) Export Annotations to Redis + // val event = MsgBuilder.buildStoreAnnotationsInRedisMsg( + // props.meetingProp.intId, + // props.voiceProp.voiceConf + // ) + // outGW.send(event) + + // 2) Insert Export Job to Redis + val jobId = RandomStringGenerator.randomAlphanumericString(16) + val jobType = "PresentationWithAnnotationDownloadJob" + val presLocation = s"/var/bigbluebutton/${presId}" + val exportJob = new ExportJob(jobId, jobType, presLocation, allPages, storeAnnotationPages, "", "") } def handleExportPresentationWithAnnotationReqMsg(liveMeeting: LiveMeeting): Unit = { log.warning("***** Hello World 2! ") + val jobType = "PresentationWithAnnotationExportJob" // 1) Insert Export Job to Redis // 2) Export Annotations to Redis } From 41a7ff87eaf290660fcd5eb1412d108424ad1631 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Tue, 25 Jan 2022 18:15:11 +0100 Subject: [PATCH 009/268] Send out message to store annotation and export job in Redis --- .../core/models/PresentationPods.scala | 25 ---------- .../senders/ReceivedJsonMsgHandlerActor.scala | 2 +- .../core/running/MeetingActor.scala | 47 ++++++++++++------- .../bigbluebutton/core2/AnalyticsActor.scala | 4 ++ .../endpoint/redis/RedisRecorderActor.scala | 12 +++++ .../common2/msgs/WhiteboardMsgs.scala | 33 +++++++++++++ .../presentation-toolbar/service.js | 5 +- .../client/meeting/AllowedMessageNames.scala | 2 + 8 files changed, 83 insertions(+), 47 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/PresentationPods.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/PresentationPods.scala index e492630a14..8f2bdc9e8e 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/PresentationPods.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/PresentationPods.scala @@ -31,31 +31,6 @@ case class PresentationPage( heightRatio: Double = 100D ) -case class PresentationPageForExport( - page: Int, - xOffset: Double, - yOffset: Double, - widthRatio: Double, - heightRatio: Double, - annotations: Array[AnnotationVO], -) - -case class StoredAnnotations( - presId: String, - pages: Array[PresentationPageForExport], -) - -case class ExportJob( - jobId: String, - jobType: String, - presId: String, - presLocation: String, - allPages: Boolean, - pages: Array[PresentationPageForExport], - parentMeetingId: String, - presUploadToken: String, -) - object PresentationInPod { def addPage(pres: PresentationInPod, page: PresentationPage): PresentationInPod = { val newPages = pres.pages + (page.id -> page) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala index ef2d00ab95..325f63b1ed 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala @@ -26,7 +26,7 @@ class ReceivedJsonMsgHandlerActor( def receive = { case msg: ReceivedJsonMessage => - // log.debug("handling {} - {}", msg.channel, msg.data) + // log.debug("handling {} - {}", msg.channel, msg.data) handleReceivedJsonMessage(msg) case _ => // do nothing } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index de8a7808b1..f29ae0b7ba 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -719,8 +719,27 @@ class MeetingActor( } + def buildStoreAnnotationsInRedisSysMsg(annotations: StoredAnnotations): BbbCommonEnvCoreMsg = { + val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka") + val envelope = BbbCoreEnvelope(StoreAnnotationsInRedisSysMsg.NAME, routing) + val body = StoreAnnotationsInRedisSysMsgBody(annotations) + val header = BbbCoreBaseHeader(StoreAnnotationsInRedisSysMsg.NAME) + val event = StoreAnnotationsInRedisSysMsg(header, body) + + BbbCommonEnvCoreMsg(envelope, event) + } + + def buildStoreExportJobInRedisSysMsg(exportJob: ExportJob): BbbCommonEnvCoreMsg = { + val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka") + val envelope = BbbCoreEnvelope(StoreExportJobInRedisSysMsg.NAME, routing) + val body = StoreExportJobInRedisSysMsgBody(exportJob) + val header = BbbCoreBaseHeader(StoreExportJobInRedisSysMsg.NAME) + val event = StoreExportJobInRedisSysMsg(header, body) + + BbbCommonEnvCoreMsg(envelope, event) + } + def handleMakePresentationWithAnnotationDownloadReqMsg(m: MakePresentationWithAnnotationDownloadReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = { - println("*** Current Whiteboard State ***") val presId: String = m.body.presId // Whiteboard ID val allPages: Boolean = m.body.allPages // Whether or not all pages of the presentation should be exported @@ -741,8 +760,6 @@ class MeetingActor( // whiteboardId = s"${presId}/${pageNumber.toString}" // TODO: use this whiteboardId = s"${getMeetingInfoPresentationDetails().id}/${pageNumber.toString}" - println(currentPres.pages(whiteboardId)) - val presentationPage: PresentationPage = currentPres.pages(whiteboardId) val xOffset: Double = presentationPage.xOffset val yOffset: Double = presentationPage.yOffset @@ -750,36 +767,30 @@ class MeetingActor( val heightRatio: Double = presentationPage.heightRatio val whiteboardHistory: Array[AnnotationVO] = liveMeeting.wbModel.getHistory(whiteboardId) - println("*****") - println("") - storeAnnotationPages(index) = new PresentationPageForExport(index + 1, xOffset, yOffset, widthRatio, heightRatio, whiteboardHistory) index += 1 } - // 1) Export Annotations to Redis + // 1) Send Annotations to Redis var annotations = new StoredAnnotations(presId, storeAnnotationPages) - println(annotations) + outGW.send(buildStoreAnnotationsInRedisSysMsg(annotations)) - // val event = MsgBuilder.buildStoreAnnotationsInRedisMsg( - // props.meetingProp.intId, - // props.voiceProp.voiceConf - // ) - // outGW.send(event) - - // 2) Insert Export Job to Redis + // 2) Insert Export Job in Redis val jobId = RandomStringGenerator.randomAlphanumericString(16) val jobType = "PresentationWithAnnotationDownloadJob" val presLocation = s"/var/bigbluebutton/${presId}" - val exportJob = new ExportJob(jobId, jobType, presLocation, allPages, storeAnnotationPages, "", "") - + val exportJob = new ExportJob(jobId, jobType, presId, presLocation, allPages, storeAnnotationPages, "", "") + var job = buildStoreExportJobInRedisSysMsg(exportJob) + outGW.send(job) } def handleExportPresentationWithAnnotationReqMsg(liveMeeting: LiveMeeting): Unit = { - log.warning("***** Hello World 2! ") val jobType = "PresentationWithAnnotationExportJob" + // 1) Insert Export Job to Redis + // 2) Export Annotations to Redis + } def handleDeskShareGetDeskShareInfoRequest(msg: DeskShareGetDeskShareInfoRequest): Unit = { diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala index d7343df1f4..bee0cba874 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala @@ -114,6 +114,10 @@ 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: PresentationPageConversionStartedSysMsg => logMessage(msg) case m: PresentationConversionEndedSysMsg => logMessage(msg) case m: PresentationConversionRequestReceivedSysMsg => logMessage(msg) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala index 09d56b2a7c..3a2a1873b3 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala @@ -73,6 +73,8 @@ class RedisRecorderActor( case m: CreateNewPresentationPodEvtMsg => handleCreateNewPresentationPodEvtMsg(m) case m: RemovePresentationPodEvtMsg => handleRemovePresentationPodEvtMsg(m) case m: SetPresenterInPodRespMsg => handleSetPresenterInPodRespMsg(m) + case m: StoreAnnotationsInRedisSysMsg => handleStoreAnnotationsInRedisSysMsg(m) + case m: StoreExportJobInRedisSysMsg => handleStoreExportJobInRedisSysMsg(m) // Whiteboard case m: SendWhiteboardAnnotationEvtMsg => handleSendWhiteboardAnnotationEvtMsg(m) @@ -133,6 +135,16 @@ class RedisRecorderActor( } } + private def handleStoreAnnotationsInRedisSysMsg(msg: StoreAnnotationsInRedisSysMsg) { + println("These are the annotations lmao") + println(msg) + } + + private def handleStoreExportJobInRedisSysMsg(msg: StoreExportJobInRedisSysMsg) { + println("This is the export message lol") + println(msg) + } + private def handleGroupChatMessageBroadcastEvtMsg(msg: GroupChatMessageBroadcastEvtMsg) { if (msg.body.chatId == GroupChatApp.MAIN_PUBLIC_CHAT) { val ev = new PublicChatRecordEvent() diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala index 7e79667bf3..a72b373d9a 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala @@ -3,6 +3,31 @@ package org.bigbluebutton.common2.msgs case class AnnotationVO(id: String, status: String, annotationType: String, annotationInfo: scala.collection.immutable.Map[String, Any], wbId: String, userId: String, position: Int) +case class PresentationPageForExport( + page: Int, + xOffset: Double, + yOffset: Double, + widthRatio: Double, + heightRatio: Double, + annotations: Array[AnnotationVO], +) + +case class StoredAnnotations( + presId: String, + pages: Array[PresentationPageForExport], +) + +case class ExportJob( + jobId: String, + jobType: String, + presId: String, + presLocation: String, + allPages: Boolean, + pages: Array[PresentationPageForExport], + parentMeetingId: String, + presUploadToken: String, +) + // ------------ client to akka-apps ------------ object ClientToServerLatencyTracerMsg { val NAME = "ClientToServerLatencyTracerMsg" } case class ClientToServerLatencyTracerMsg(header: BbbClientMsgHeader, body: ClientToServerLatencyTracerMsgBody) extends StandardMsg @@ -65,4 +90,12 @@ case class SendWhiteboardAnnotationEvtMsgBody(annotation: AnnotationVO) object UndoWhiteboardEvtMsg { val NAME = "UndoWhiteboardEvtMsg" } case class UndoWhiteboardEvtMsg(header: BbbClientMsgHeader, body: UndoWhiteboardEvtMsgBody) extends BbbCoreMsg case class UndoWhiteboardEvtMsgBody(whiteboardId: String, userId: String, annotationId: String) + // ------------ akka-apps to client ------------ +object StoreAnnotationsInRedisSysMsg { val NAME = "StoreAnnotationsInRedisSysMsg" } +case class StoreAnnotationsInRedisSysMsg(header: BbbCoreBaseHeader, body: StoreAnnotationsInRedisSysMsgBody) extends BbbCoreMsg +case class StoreAnnotationsInRedisSysMsgBody(annotations: StoredAnnotations) + +object StoreExportJobInRedisSysMsg { val NAME = "StoreExportJobInRedisSysMsg" } +case class StoreExportJobInRedisSysMsg(header: BbbCoreBaseHeader, body: StoreExportJobInRedisSysMsgBody) extends BbbCoreMsg +case class StoreExportJobInRedisSysMsgBody(exportJob: ExportJob) diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js index 13581613da..2829cb1350 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js @@ -30,9 +30,8 @@ const nextSlide = (currentSlideNum, numberOfSlides, podId) => { }; const downloadAnnotatedSlides = () => { - makeCall('makePresentationWithAnnotationDownloadReqMsg') - makeCall('userActivitySign') -} + makeCall('makePresentationWithAnnotationDownloadReqMsg'); +}; const zoomSlide = throttle((currentSlideNum, podId, widthRatio, heightRatio, xOffset, yOffset) => { makeCall('zoomSlide', currentSlideNum, podId, widthRatio, heightRatio, xOffset, yOffset); diff --git a/labs/vertx-akka/src/main/scala/org/bigbluebutton/client/meeting/AllowedMessageNames.scala b/labs/vertx-akka/src/main/scala/org/bigbluebutton/client/meeting/AllowedMessageNames.scala index cc4153a8f5..6ad8a38490 100755 --- a/labs/vertx-akka/src/main/scala/org/bigbluebutton/client/meeting/AllowedMessageNames.scala +++ b/labs/vertx-akka/src/main/scala/org/bigbluebutton/client/meeting/AllowedMessageNames.scala @@ -63,6 +63,8 @@ object AllowedMessageNames { SetPresenterInPodReqMsg.NAME, MakePresentationWithAnnotationDownloadReqMsg.NAME, ExportPresentationWithAnnotationReqMsg.NAME, + StoreAnnotationsInRedisSysMsg.NAME, + StoreExportJobInRedisSysMsg.NAME, // Whiteboard Messages ModifyWhiteboardAccessPubMsg.NAME, From 5e6dd1b74f0f77e29135099c4d4fe59d8ca4553f Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Wed, 26 Jan 2022 19:23:11 +0100 Subject: [PATCH 010/268] Store messages in Redis --- .../senders/ReceivedJsonMsgHandlerActor.scala | 2 +- .../AbstractPresentationRecordEvent.scala | 2 +- .../AbstractPresentationWithAnnotations.scala | 24 +++++++ .../AbstractWhiteboardRecordEvent.scala | 2 +- .../StoreExportJobInRedisRecordEvent.scala | 72 +++++++++++++++++++ ...rePresentationAnnotationsRecordEvent.scala | 43 +++++++++++ .../core/running/MeetingActor.scala | 10 +-- .../endpoint/redis/RedisRecorderActor.scala | 26 +++++-- .../common2/redis/RedisStorageService.java | 9 +++ .../common2/msgs/WhiteboardMsgs.scala | 4 +- 10 files changed, 180 insertions(+), 14 deletions(-) create mode 100644 akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AbstractPresentationWithAnnotations.scala create mode 100644 akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StoreExportJobInRedisRecordEvent.scala create mode 100644 akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StorePresentationAnnotationsRecordEvent.scala diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala index 325f63b1ed..ef2d00ab95 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala @@ -26,7 +26,7 @@ class ReceivedJsonMsgHandlerActor( def receive = { case msg: ReceivedJsonMessage => - // log.debug("handling {} - {}", msg.channel, msg.data) + // log.debug("handling {} - {}", msg.channel, msg.data) handleReceivedJsonMessage(msg) case _ => // do nothing } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AbstractPresentationRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AbstractPresentationRecordEvent.scala index a47198d909..b479ba6ff5 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AbstractPresentationRecordEvent.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AbstractPresentationRecordEvent.scala @@ -31,4 +31,4 @@ trait AbstractPresentationRecordEvent extends RecordEvent { object AbstractPresentationRecordEvent { protected final val POD_ID = "podId" -} \ No newline at end of file +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AbstractPresentationWithAnnotations.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AbstractPresentationWithAnnotations.scala new file mode 100644 index 0000000000..4071ae4236 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AbstractPresentationWithAnnotations.scala @@ -0,0 +1,24 @@ +/** + * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ + * + * Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below). + * + * This program is free software; you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation; either version 3.0 of the License, or (at your option) any later + * version. + * + * BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with BigBlueButton; if not, see . + * + */ + +package org.bigbluebutton.core.record.events + +trait AbstractPresentationWithAnnotations extends RecordEvent { + setModule("PRES-ANN") +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AbstractWhiteboardRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AbstractWhiteboardRecordEvent.scala index 3be4ea16ab..091df4a763 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AbstractWhiteboardRecordEvent.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AbstractWhiteboardRecordEvent.scala @@ -45,4 +45,4 @@ object AbstractWhiteboardRecordEvent { protected final val PRESENTATION = "presentation" protected final val PAGE_NUM = "pageNumber" protected final val WHITEBOARD_ID = "whiteboardId" -} \ No newline at end of file +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StoreExportJobInRedisRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StoreExportJobInRedisRecordEvent.scala new file mode 100644 index 0000000000..d3b22f7ae3 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StoreExportJobInRedisRecordEvent.scala @@ -0,0 +1,72 @@ +/** + * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ + * + * Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below). + * + * This program is free software; you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation; either version 3.0 of the License, or (at your option) any later + * version. + * + * BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with BigBlueButton; if not, see . + * + */ + +package org.bigbluebutton.core.record.events + +import org.bigbluebutton.common2.msgs.{ AnnotationVO, ExportJob, StoredAnnotations, PresentationPageForExport } +import org.bigbluebutton.common2.util.JsonUtil + +class StoreExportJobInRedisRecordEvent extends AbstractPresentationWithAnnotations { + import StoreExportJobInRedisRecordEvent._ + + setEvent("StoreExportJobInRedisRecordEvent") + + def setJobId(jobId: String) { + eventMap.put(JOB_ID, jobId) + } + + def setJobType(jobType: String) { + eventMap.put(JOB_TYPE, jobType) + } + + 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: Array[PresentationPageForExport]) { + 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 StoreExportJobInRedisRecordEvent { + protected final val JOB_ID = "jobId" + protected final val JOB_TYPE = "jobType" + 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" +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StorePresentationAnnotationsRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StorePresentationAnnotationsRecordEvent.scala new file mode 100644 index 0000000000..5fd09e5dee --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StorePresentationAnnotationsRecordEvent.scala @@ -0,0 +1,43 @@ +/** + * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ + * + * Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below). + * + * This program is free software; you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation; either version 3.0 of the License, or (at your option) any later + * version. + * + * BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with BigBlueButton; if not, see . + * + */ + +package org.bigbluebutton.core.record.events + +import org.bigbluebutton.common2.msgs.{ AnnotationVO, ExportJob, StoredAnnotations, PresentationPageForExport } +import org.bigbluebutton.common2.util.JsonUtil + +class StorePresentationAnnotationsRecordEvent extends AbstractPresentationWithAnnotations { + import StorePresentationAnnotationsRecordEvent._ + + setEvent("StorePresentationAnnotationsRecordEvent") + + def setPresId(presId: String) { + eventMap.put(PRES_ID, presId) + } + + def setPages(pages: Array[PresentationPageForExport]) { + eventMap.put(PAGES, JsonUtil.toJson(pages)) + } +} + +object StorePresentationAnnotationsRecordEvent { + protected final val PRES_ID = "presId" + protected final val PAGES = "pages" +} + diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index f29ae0b7ba..c9f706c267 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -723,7 +723,7 @@ class MeetingActor( val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka") val envelope = BbbCoreEnvelope(StoreAnnotationsInRedisSysMsg.NAME, routing) val body = StoreAnnotationsInRedisSysMsgBody(annotations) - val header = BbbCoreBaseHeader(StoreAnnotationsInRedisSysMsg.NAME) + val header = BbbCoreHeaderWithMeetingId(StoreAnnotationsInRedisSysMsg.NAME, liveMeeting.props.meetingProp.intId) val event = StoreAnnotationsInRedisSysMsg(header, body) BbbCommonEnvCoreMsg(envelope, event) @@ -733,7 +733,7 @@ class MeetingActor( val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka") val envelope = BbbCoreEnvelope(StoreExportJobInRedisSysMsg.NAME, routing) val body = StoreExportJobInRedisSysMsgBody(exportJob) - val header = BbbCoreBaseHeader(StoreExportJobInRedisSysMsg.NAME) + val header = BbbCoreHeaderWithMeetingId(StoreExportJobInRedisSysMsg.NAME, liveMeeting.props.meetingProp.intId) val event = StoreExportJobInRedisSysMsg(header, body) BbbCommonEnvCoreMsg(envelope, event) @@ -755,7 +755,7 @@ class MeetingActor( val pagesRange: List[Int] = if (allPages) (1 to pageCount).toList else pages var storeAnnotationPages = new Array[PresentationPageForExport](pagesRange.size) - var index = 0 + var resultingPage = 0 for (pageNumber <- pagesRange) { // whiteboardId = s"${presId}/${pageNumber.toString}" // TODO: use this whiteboardId = s"${getMeetingInfoPresentationDetails().id}/${pageNumber.toString}" @@ -767,8 +767,8 @@ class MeetingActor( val heightRatio: Double = presentationPage.heightRatio val whiteboardHistory: Array[AnnotationVO] = liveMeeting.wbModel.getHistory(whiteboardId) - storeAnnotationPages(index) = new PresentationPageForExport(index + 1, xOffset, yOffset, widthRatio, heightRatio, whiteboardHistory) - index += 1 + storeAnnotationPages(resultingPage) = new PresentationPageForExport(resultingPage + 1, xOffset, yOffset, widthRatio, heightRatio, whiteboardHistory) + resultingPage += 1 } // 1) Send Annotations to Redis diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala index 3a2a1873b3..ecb459e05a 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala @@ -49,6 +49,10 @@ class RedisRecorderActor( redis.recordAndExpire(session, message) } + 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 @@ -136,13 +140,27 @@ class RedisRecorderActor( } private def handleStoreAnnotationsInRedisSysMsg(msg: StoreAnnotationsInRedisSysMsg) { - println("These are the annotations lmao") - println(msg) + val ev = new StorePresentationAnnotationsRecordEvent() + + ev.setPresId(msg.body.annotations.presId) + ev.setPages(msg.body.annotations.pages) + + storePresentationAnnotations(msg.header.meetingId, ev.toMap.asJava, "Annotations") } private def handleStoreExportJobInRedisSysMsg(msg: StoreExportJobInRedisSysMsg) { - println("This is the export message lol") - println(msg) + val ev = new StoreExportJobInRedisRecordEvent() + + ev.setJobId(msg.body.exportJob.jobId) + ev.setJobType(msg.body.exportJob.jobType) + 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") } private def handleGroupChatMessageBroadcastEvtMsg(msg: GroupChatMessageBroadcastEvtMsg) { diff --git a/bbb-common-message/src/main/java/org/bigbluebutton/common2/redis/RedisStorageService.java b/bbb-common-message/src/main/java/org/bigbluebutton/common2/redis/RedisStorageService.java index 26fac9d2b2..0ff3781d54 100755 --- a/bbb-common-message/src/main/java/org/bigbluebutton/common2/redis/RedisStorageService.java +++ b/bbb-common-message/src/main/java/org/bigbluebutton/common2/redis/RedisStorageService.java @@ -112,6 +112,15 @@ public class RedisStorageService extends RedisAwareCommunicator { commands.exec(); } + public void storePresentationAnnotations(String meetingId, Map event, String msgType) { + RedisCommands commands = connection.sync(); + Long msgid = commands.incr("global:nextRecordedMsgId"); + commands.multi(); + commands.hmset("store" + msgType + ":" + meetingId + ":" + msgid, event); + commands.rpush("meeting:" + meetingId + ":" + "store" + msgType, Long.toString(msgid)); + commands.exec(); + } + // @fixme: not used anywhere public void removeMeeting(String meetingId) { RedisCommands commands = connection.sync(); diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala index a72b373d9a..ed9381f807 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala @@ -93,9 +93,9 @@ case class UndoWhiteboardEvtMsgBody(whiteboardId: String, userId: String, annota // ------------ akka-apps to client ------------ object StoreAnnotationsInRedisSysMsg { val NAME = "StoreAnnotationsInRedisSysMsg" } -case class StoreAnnotationsInRedisSysMsg(header: BbbCoreBaseHeader, body: StoreAnnotationsInRedisSysMsgBody) extends BbbCoreMsg +case class StoreAnnotationsInRedisSysMsg(header: BbbCoreHeaderWithMeetingId, body: StoreAnnotationsInRedisSysMsgBody) extends BbbCoreMsg case class StoreAnnotationsInRedisSysMsgBody(annotations: StoredAnnotations) object StoreExportJobInRedisSysMsg { val NAME = "StoreExportJobInRedisSysMsg" } -case class StoreExportJobInRedisSysMsg(header: BbbCoreBaseHeader, body: StoreExportJobInRedisSysMsgBody) extends BbbCoreMsg +case class StoreExportJobInRedisSysMsg(header: BbbCoreHeaderWithMeetingId, body: StoreExportJobInRedisSysMsgBody) extends BbbCoreMsg case class StoreExportJobInRedisSysMsgBody(exportJob: ExportJob) From 289ae0b71fbf3cde6c0a59ad174065d1d9774100 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Wed, 26 Jan 2022 22:37:52 +0100 Subject: [PATCH 011/268] Add PresentationUploadToken to handleExportPresentationWithAnnotationReqMsg --- .../senders/ReceivedJsonMsgHandlerActor.scala | 2 + .../core/running/MeetingActor.scala | 67 ++++++++++++++++--- ...resentationWithAnnotationDownloadReqMsg.js | 12 +++- 3 files changed, 69 insertions(+), 12 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala index ef2d00ab95..e9a68cfeae 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala @@ -268,6 +268,8 @@ class ReceivedJsonMsgHandlerActor( routeGenericMsg[AssignPresenterReqMsg](envelope, jsonNode) case MakePresentationWithAnnotationDownloadReqMsg.NAME => routeGenericMsg[MakePresentationWithAnnotationDownloadReqMsg](envelope, jsonNode) + case ExportPresentationWithAnnotationReqMsg.NAME => + routeGenericMsg[ExportPresentationWithAnnotationReqMsg](envelope, jsonNode) // Presentation Pods case CreateNewPresentationPodPubMsg.NAME => diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index c9f706c267..e682f56b5c 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -500,7 +500,7 @@ class MeetingActor( case m: PreuploadedPresentationsSysPubMsg => presentationApp2x.handle(m, liveMeeting, msgBus) case m: AssignPresenterReqMsg => state = handlePresenterChange(m, state) case m: MakePresentationWithAnnotationDownloadReqMsg => handleMakePresentationWithAnnotationDownloadReqMsg(m, state, liveMeeting) - case m: ExportPresentationWithAnnotationReqMsg => handleExportPresentationWithAnnotationReqMsg(liveMeeting) + case m: ExportPresentationWithAnnotationReqMsg => handleExportPresentationWithAnnotationReqMsg(m, state, liveMeeting) // Presentation Pods case m: CreateNewPresentationPodPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) @@ -739,14 +739,21 @@ class MeetingActor( 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 handleMakePresentationWithAnnotationDownloadReqMsg(m: MakePresentationWithAnnotationDownloadReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = { val presId: String = m.body.presId // Whiteboard ID val allPages: Boolean = m.body.allPages // Whether or not all pages of the presentation should be exported val pages: List[Int] = m.body.pages // Desired presentation pages for export - var whiteboardId: String = getMeetingInfoPresentationDetails().id // TODO: use presId from message instead, remove this - // Determine page amount val presentationPods: Vector[PresentationPod] = state.presentationPodManager.getAllPresentationPodsInMeeting() val currentPres = presentationPods.flatMap(_.getCurrentPresentation()).head @@ -756,10 +763,10 @@ class MeetingActor( var storeAnnotationPages = new Array[PresentationPageForExport](pagesRange.size) var resultingPage = 0 - for (pageNumber <- pagesRange) { - // whiteboardId = s"${presId}/${pageNumber.toString}" // TODO: use this - whiteboardId = s"${getMeetingInfoPresentationDetails().id}/${pageNumber.toString}" + for (pageNumber <- pagesRange) { + + var whiteboardId = s"${presId}/${pageNumber.toString}" val presentationPage: PresentationPage = currentPres.pages(whiteboardId) val xOffset: Double = presentationPage.xOffset val yOffset: Double = presentationPage.yOffset @@ -784,13 +791,53 @@ class MeetingActor( outGW.send(job) } - def handleExportPresentationWithAnnotationReqMsg(liveMeeting: LiveMeeting): Unit = { - val jobType = "PresentationWithAnnotationExportJob" + def handleExportPresentationWithAnnotationReqMsg(m: ExportPresentationWithAnnotationReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = { - // 1) Insert Export Job to Redis + val userId = m.header.userId + val presId: String = getMeetingInfoPresentationDetails.id + val parentMeetingId: String = m.body.parentMeetingId + val allPages: Boolean = m.body.allPages - // 2) Export Annotations to Redis + val presentationPods: Vector[PresentationPod] = state.presentationPodManager.getAllPresentationPodsInMeeting() + val currentPres = presentationPods.flatMap(_.getCurrentPresentation()).head + val currentPage: PresentationPage = PresentationInPod.getCurrentPage(currentPres).get + val pageCount = currentPres.pages.size + val pagesRange: List[Int] = if (allPages) (1 to pageCount).toList else List(currentPage.num) + + var storeAnnotationPages = new Array[PresentationPageForExport](pagesRange.size) + var resultingPage = 0 + + for (pageNumber <- pagesRange) { + + var whiteboardId = s"${presId}/${pageNumber.toString}" + val presentationPage: PresentationPage = currentPres.pages(whiteboardId) + val xOffset: Double = presentationPage.xOffset + val yOffset: Double = presentationPage.yOffset + val widthRatio: Double = presentationPage.widthRatio + val heightRatio: Double = presentationPage.heightRatio + val whiteboardHistory: Array[AnnotationVO] = liveMeeting.wbModel.getHistory(whiteboardId) + + storeAnnotationPages(resultingPage) = new PresentationPageForExport(resultingPage + 1, xOffset, yOffset, widthRatio, heightRatio, whiteboardHistory) + resultingPage += 1 + } + + val presentationUploadToken: String = PresentationPodsApp.generateToken("DEFAULT_PRESENTATION_POD", userId) + + // 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 + outGW.send(buildPresentationUploadTokenSysPubMsg(parentMeetingId, userId, presentationUploadToken, currentPres.name)) + + // 1) Send Annotations to Redis + var annotations = new StoredAnnotations(presId, storeAnnotationPages) + outGW.send(buildStoreAnnotationsInRedisSysMsg(annotations)) + + // 2) Insert Export Job in Redis + val jobId = RandomStringGenerator.randomAlphanumericString(16) + val jobType: String = "PresentationWithAnnotationExportJob" + val presLocation = s"/var/bigbluebutton/${presId}" + val exportJob = new ExportJob(jobId, jobType, presId, presLocation, allPages, storeAnnotationPages, parentMeetingId, presentationUploadToken) + var job = buildStoreExportJobInRedisSysMsg(exportJob) + outGW.send(job) } def handleDeskShareGetDeskShareInfoRequest(msg: DeskShareGetDeskShareInfoRequest): Unit = { diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js b/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js index 13b2eb579d..07d81af15e 100644 --- a/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js +++ b/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js @@ -8,6 +8,7 @@ export default function makePresentationWithAnnotationDownloadReqMsg() { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'MakePresentationWithAnnotationDownloadReqMsg'; + const SECOND_EVENT_NAME = 'ExportPresentationWithAnnotationReqMsg'; try { const { meetingId, requesterUserId } = extractCredentials(this.userId); @@ -16,11 +17,16 @@ export default function makePresentationWithAnnotationDownloadReqMsg() { check(requesterUserId, String); const payload = { - presId: "placeholder-val", + presId: "placeholder-pres-id", allPages: true, pages: [], }; + const payload2 = { + parentMeetingId: "placeholder-parent-meeting-id", + allPages: false, + }; + Logger.warn('************'); Logger.warn(CHANNEL) Logger.warn(EVENT_NAME) @@ -28,7 +34,9 @@ export default function makePresentationWithAnnotationDownloadReqMsg() { Logger.warn(requesterUserId) Logger.warn('************'); - return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); + return RedisPubSub.publishUserMessage(CHANNEL, SECOND_EVENT_NAME, meetingId, requesterUserId, payload2); + + // return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); } catch (err) { Logger.error(`Exception while invoking method makePresentationWithAnnotationDownloadReqMsg ${err.stack}`); } From 87f51b1ba48ce5557ea24fce1b678aef1c15dafd Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Wed, 26 Jan 2022 22:37:52 +0100 Subject: [PATCH 012/268] Add PresentationUploadToken to handleExportPresentationWithAnnotationReqMsg --- .../senders/ReceivedJsonMsgHandlerActor.scala | 2 + .../core/running/MeetingActor.scala | 69 ++++++++++++++++--- ...resentationWithAnnotationDownloadReqMsg.js | 12 +++- 3 files changed, 70 insertions(+), 13 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala index ef2d00ab95..e9a68cfeae 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala @@ -268,6 +268,8 @@ class ReceivedJsonMsgHandlerActor( routeGenericMsg[AssignPresenterReqMsg](envelope, jsonNode) case MakePresentationWithAnnotationDownloadReqMsg.NAME => routeGenericMsg[MakePresentationWithAnnotationDownloadReqMsg](envelope, jsonNode) + case ExportPresentationWithAnnotationReqMsg.NAME => + routeGenericMsg[ExportPresentationWithAnnotationReqMsg](envelope, jsonNode) // Presentation Pods case CreateNewPresentationPodPubMsg.NAME => diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index c9f706c267..faab7364b1 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -500,7 +500,7 @@ class MeetingActor( case m: PreuploadedPresentationsSysPubMsg => presentationApp2x.handle(m, liveMeeting, msgBus) case m: AssignPresenterReqMsg => state = handlePresenterChange(m, state) case m: MakePresentationWithAnnotationDownloadReqMsg => handleMakePresentationWithAnnotationDownloadReqMsg(m, state, liveMeeting) - case m: ExportPresentationWithAnnotationReqMsg => handleExportPresentationWithAnnotationReqMsg(liveMeeting) + case m: ExportPresentationWithAnnotationReqMsg => handleExportPresentationWithAnnotationReqMsg(m, state, liveMeeting) // Presentation Pods case m: CreateNewPresentationPodPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) @@ -739,14 +739,21 @@ class MeetingActor( 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 handleMakePresentationWithAnnotationDownloadReqMsg(m: MakePresentationWithAnnotationDownloadReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = { val presId: String = m.body.presId // Whiteboard ID val allPages: Boolean = m.body.allPages // Whether or not all pages of the presentation should be exported val pages: List[Int] = m.body.pages // Desired presentation pages for export - var whiteboardId: String = getMeetingInfoPresentationDetails().id // TODO: use presId from message instead, remove this - // Determine page amount val presentationPods: Vector[PresentationPod] = state.presentationPodManager.getAllPresentationPodsInMeeting() val currentPres = presentationPods.flatMap(_.getCurrentPresentation()).head @@ -756,10 +763,10 @@ class MeetingActor( var storeAnnotationPages = new Array[PresentationPageForExport](pagesRange.size) var resultingPage = 0 - for (pageNumber <- pagesRange) { - // whiteboardId = s"${presId}/${pageNumber.toString}" // TODO: use this - whiteboardId = s"${getMeetingInfoPresentationDetails().id}/${pageNumber.toString}" + for (pageNumber <- pagesRange) { + + var whiteboardId = s"${presId}/${pageNumber.toString}" val presentationPage: PresentationPage = currentPres.pages(whiteboardId) val xOffset: Double = presentationPage.xOffset val yOffset: Double = presentationPage.yOffset @@ -767,7 +774,7 @@ class MeetingActor( val heightRatio: Double = presentationPage.heightRatio val whiteboardHistory: Array[AnnotationVO] = liveMeeting.wbModel.getHistory(whiteboardId) - storeAnnotationPages(resultingPage) = new PresentationPageForExport(resultingPage + 1, xOffset, yOffset, widthRatio, heightRatio, whiteboardHistory) + storeAnnotationPages(resultingPage) = new PresentationPageForExport(pageNumber, xOffset, yOffset, widthRatio, heightRatio, whiteboardHistory) resultingPage += 1 } @@ -784,13 +791,53 @@ class MeetingActor( outGW.send(job) } - def handleExportPresentationWithAnnotationReqMsg(liveMeeting: LiveMeeting): Unit = { - val jobType = "PresentationWithAnnotationExportJob" + def handleExportPresentationWithAnnotationReqMsg(m: ExportPresentationWithAnnotationReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = { - // 1) Insert Export Job to Redis + val userId = m.header.userId + val presId: String = getMeetingInfoPresentationDetails.id + val parentMeetingId: String = m.body.parentMeetingId + val allPages: Boolean = m.body.allPages - // 2) Export Annotations to Redis + val presentationPods: Vector[PresentationPod] = state.presentationPodManager.getAllPresentationPodsInMeeting() + val currentPres = presentationPods.flatMap(_.getCurrentPresentation()).head + val currentPage: PresentationPage = PresentationInPod.getCurrentPage(currentPres).get + val pageCount = currentPres.pages.size + val pagesRange: List[Int] = if (allPages) (1 to pageCount).toList else List(currentPage.num) + + var storeAnnotationPages = new Array[PresentationPageForExport](pagesRange.size) + var resultingPage = 0 + + for (pageNumber <- pagesRange) { + + var whiteboardId = s"${presId}/${pageNumber.toString}" + val presentationPage: PresentationPage = currentPres.pages(whiteboardId) + val xOffset: Double = presentationPage.xOffset + val yOffset: Double = presentationPage.yOffset + val widthRatio: Double = presentationPage.widthRatio + val heightRatio: Double = presentationPage.heightRatio + val whiteboardHistory: Array[AnnotationVO] = liveMeeting.wbModel.getHistory(whiteboardId) + + storeAnnotationPages(resultingPage) = new PresentationPageForExport(pageNumber, xOffset, yOffset, widthRatio, heightRatio, whiteboardHistory) + resultingPage += 1 + } + + val presentationUploadToken: String = PresentationPodsApp.generateToken("DEFAULT_PRESENTATION_POD", userId) + + // 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 + outGW.send(buildPresentationUploadTokenSysPubMsg(parentMeetingId, userId, presentationUploadToken, currentPres.name)) + + // 1) Send Annotations to Redis + var annotations = new StoredAnnotations(presId, storeAnnotationPages) + outGW.send(buildStoreAnnotationsInRedisSysMsg(annotations)) + + // 2) Insert Export Job in Redis + val jobId = RandomStringGenerator.randomAlphanumericString(16) + val jobType: String = "PresentationWithAnnotationExportJob" + val presLocation = s"/var/bigbluebutton/${presId}" + val exportJob = new ExportJob(jobId, jobType, presId, presLocation, allPages, storeAnnotationPages, parentMeetingId, presentationUploadToken) + var job = buildStoreExportJobInRedisSysMsg(exportJob) + outGW.send(job) } def handleDeskShareGetDeskShareInfoRequest(msg: DeskShareGetDeskShareInfoRequest): Unit = { diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js b/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js index 13b2eb579d..07d81af15e 100644 --- a/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js +++ b/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js @@ -8,6 +8,7 @@ export default function makePresentationWithAnnotationDownloadReqMsg() { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'MakePresentationWithAnnotationDownloadReqMsg'; + const SECOND_EVENT_NAME = 'ExportPresentationWithAnnotationReqMsg'; try { const { meetingId, requesterUserId } = extractCredentials(this.userId); @@ -16,11 +17,16 @@ export default function makePresentationWithAnnotationDownloadReqMsg() { check(requesterUserId, String); const payload = { - presId: "placeholder-val", + presId: "placeholder-pres-id", allPages: true, pages: [], }; + const payload2 = { + parentMeetingId: "placeholder-parent-meeting-id", + allPages: false, + }; + Logger.warn('************'); Logger.warn(CHANNEL) Logger.warn(EVENT_NAME) @@ -28,7 +34,9 @@ export default function makePresentationWithAnnotationDownloadReqMsg() { Logger.warn(requesterUserId) Logger.warn('************'); - return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); + return RedisPubSub.publishUserMessage(CHANNEL, SECOND_EVENT_NAME, meetingId, requesterUserId, payload2); + + // return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); } catch (err) { Logger.error(`Exception while invoking method makePresentationWithAnnotationDownloadReqMsg ${err.stack}`); } From e5dec08ab969755252c8a1eada47ce8f97b9dbc7 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Tue, 1 Feb 2022 17:24:57 +0100 Subject: [PATCH 013/268] List/Hash data structure in Redis for messages; add jobId in annotations format --- ...rePresentationAnnotationsRecordEvent.scala | 5 ++++ .../core/running/MeetingActor.scala | 9 +++--- .../bigbluebutton/core2/AnalyticsActor.scala | 4 +-- .../endpoint/redis/RedisRecorderActor.scala | 1 + .../common2/redis/RedisStorageService.java | 28 ++++++++++++++++--- .../common2/msgs/WhiteboardMsgs.scala | 1 + 6 files changed, 38 insertions(+), 10 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StorePresentationAnnotationsRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StorePresentationAnnotationsRecordEvent.scala index 5fd09e5dee..4120fbc8b5 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StorePresentationAnnotationsRecordEvent.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StorePresentationAnnotationsRecordEvent.scala @@ -27,6 +27,10 @@ class StorePresentationAnnotationsRecordEvent extends AbstractPresentationWithAn setEvent("StorePresentationAnnotationsRecordEvent") + def setJobId(jobId: String) { + eventMap.put(JOB_ID, jobId) + } + def setPresId(presId: String) { eventMap.put(PRES_ID, presId) } @@ -37,6 +41,7 @@ class StorePresentationAnnotationsRecordEvent extends AbstractPresentationWithAn } object StorePresentationAnnotationsRecordEvent { + protected final val JOB_ID = "jobId" protected final val PRES_ID = "presId" protected final val PAGES = "pages" } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index faab7364b1..e8f34b9a03 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -778,12 +778,13 @@ class MeetingActor( resultingPage += 1 } + val jobId = RandomStringGenerator.randomAlphanumericString(16) + // 1) Send Annotations to Redis - var annotations = new StoredAnnotations(presId, storeAnnotationPages) + var annotations = new StoredAnnotations(jobId, presId, storeAnnotationPages) outGW.send(buildStoreAnnotationsInRedisSysMsg(annotations)) // 2) Insert Export Job in Redis - val jobId = RandomStringGenerator.randomAlphanumericString(16) val jobType = "PresentationWithAnnotationDownloadJob" val presLocation = s"/var/bigbluebutton/${presId}" val exportJob = new ExportJob(jobId, jobType, presId, presLocation, allPages, storeAnnotationPages, "", "") @@ -823,16 +824,16 @@ class MeetingActor( } val presentationUploadToken: String = PresentationPodsApp.generateToken("DEFAULT_PRESENTATION_POD", userId) + val jobId = RandomStringGenerator.randomAlphanumericString(16) // 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 outGW.send(buildPresentationUploadTokenSysPubMsg(parentMeetingId, userId, presentationUploadToken, currentPres.name)) // 1) Send Annotations to Redis - var annotations = new StoredAnnotations(presId, storeAnnotationPages) + var annotations = new StoredAnnotations(jobId, presId, storeAnnotationPages) outGW.send(buildStoreAnnotationsInRedisSysMsg(annotations)) // 2) Insert Export Job in Redis - val jobId = RandomStringGenerator.randomAlphanumericString(16) val jobType: String = "PresentationWithAnnotationExportJob" val presLocation = s"/var/bigbluebutton/${presId}" val exportJob = new ExportJob(jobId, jobType, presId, presLocation, allPages, storeAnnotationPages, parentMeetingId, presentationUploadToken) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala index bee0cba874..08307475ab 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala @@ -114,8 +114,8 @@ 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: StoreAnnotationsInRedisSysMsg => logMessage(msg) + // case m: StoreExportJobInRedisSysMsg => logMessage(msg) case m: MakePresentationWithAnnotationDownloadReqMsg => logMessage(msg) case m: ExportPresentationWithAnnotationReqMsg => logMessage(msg) case m: PresentationPageConversionStartedSysMsg => logMessage(msg) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala index ecb459e05a..31005a14f9 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala @@ -142,6 +142,7 @@ class RedisRecorderActor( private def handleStoreAnnotationsInRedisSysMsg(msg: StoreAnnotationsInRedisSysMsg) { val ev = new StorePresentationAnnotationsRecordEvent() + ev.setJobId(msg.body.annotations.jobId) ev.setPresId(msg.body.annotations.presId) ev.setPages(msg.body.annotations.pages) diff --git a/bbb-common-message/src/main/java/org/bigbluebutton/common2/redis/RedisStorageService.java b/bbb-common-message/src/main/java/org/bigbluebutton/common2/redis/RedisStorageService.java index 0ff3781d54..0c26c29b46 100755 --- a/bbb-common-message/src/main/java/org/bigbluebutton/common2/redis/RedisStorageService.java +++ b/bbb-common-message/src/main/java/org/bigbluebutton/common2/redis/RedisStorageService.java @@ -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; @@ -113,11 +115,29 @@ public class RedisStorageService extends RedisAwareCommunicator { } public void storePresentationAnnotations(String meetingId, Map event, String msgType) { - RedisCommands commands = connection.sync(); - Long msgid = commands.incr("global:nextRecordedMsgId"); + RedisCommands commands = connection.sync(); + commands.multi(); - commands.hmset("store" + msgType + ":" + meetingId + ":" + msgid, event); - commands.rpush("meeting:" + meetingId + ":" + "store" + msgType, Long.toString(msgid)); + + switch (msgType) { + case "Annotations": { + 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: {} (expected 'Annotations' or 'ExportJob')", clientName); + break; + } + } + commands.exec(); } diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala index ed9381f807..abc6862bfb 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala @@ -13,6 +13,7 @@ case class PresentationPageForExport( ) case class StoredAnnotations( + jobId: String, presId: String, pages: Array[PresentationPageForExport], ) From 3f0fb558b177488f1b816422ba984283bd9b445a Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Wed, 2 Feb 2022 16:32:09 +0100 Subject: [PATCH 014/268] Add ExportAnnotationsActor --- .../main/scala/org/bigbluebutton/Boot.scala | 8 +- ...StoreAnnotationsInRedisPresAnnEvent.scala} | 8 +- ...> StoreExportJobInRedisPresAnnEvent.scala} | 8 +- .../redis/ExportAnnotationsActor.scala | 87 +++++++++++++++++++ .../endpoint/redis/RedisRecorderActor.scala | 31 ------- 5 files changed, 102 insertions(+), 40 deletions(-) rename akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/{StorePresentationAnnotationsRecordEvent.scala => StoreAnnotationsInRedisPresAnnEvent.scala} (85%) rename akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/{StoreExportJobInRedisRecordEvent.scala => StoreExportJobInRedisPresAnnEvent.scala} (90%) create mode 100755 akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/ExportAnnotationsActor.scala diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/Boot.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/Boot.scala index da8118b7ee..7054a5509b 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/Boot.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/Boot.scala @@ -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) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StorePresentationAnnotationsRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StoreAnnotationsInRedisPresAnnEvent.scala similarity index 85% rename from akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StorePresentationAnnotationsRecordEvent.scala rename to akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StoreAnnotationsInRedisPresAnnEvent.scala index 4120fbc8b5..f92c649047 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StorePresentationAnnotationsRecordEvent.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StoreAnnotationsInRedisPresAnnEvent.scala @@ -22,10 +22,10 @@ package org.bigbluebutton.core.record.events import org.bigbluebutton.common2.msgs.{ AnnotationVO, ExportJob, StoredAnnotations, PresentationPageForExport } import org.bigbluebutton.common2.util.JsonUtil -class StorePresentationAnnotationsRecordEvent extends AbstractPresentationWithAnnotations { - import StorePresentationAnnotationsRecordEvent._ +class StoreAnnotationsInRedisPresAnnEvent extends AbstractPresentationWithAnnotations { + import StoreAnnotationsInRedisPresAnnEvent._ - setEvent("StorePresentationAnnotationsRecordEvent") + setEvent("StoreAnnotationsInRedisPresAnnEvent") def setJobId(jobId: String) { eventMap.put(JOB_ID, jobId) @@ -40,7 +40,7 @@ class StorePresentationAnnotationsRecordEvent extends AbstractPresentationWithAn } } -object StorePresentationAnnotationsRecordEvent { +object StoreAnnotationsInRedisPresAnnEvent { protected final val JOB_ID = "jobId" protected final val PRES_ID = "presId" protected final val PAGES = "pages" diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StoreExportJobInRedisRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StoreExportJobInRedisPresAnnEvent.scala similarity index 90% rename from akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StoreExportJobInRedisRecordEvent.scala rename to akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StoreExportJobInRedisPresAnnEvent.scala index d3b22f7ae3..c5c4f919f0 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StoreExportJobInRedisRecordEvent.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StoreExportJobInRedisPresAnnEvent.scala @@ -22,10 +22,10 @@ package org.bigbluebutton.core.record.events import org.bigbluebutton.common2.msgs.{ AnnotationVO, ExportJob, StoredAnnotations, PresentationPageForExport } import org.bigbluebutton.common2.util.JsonUtil -class StoreExportJobInRedisRecordEvent extends AbstractPresentationWithAnnotations { - import StoreExportJobInRedisRecordEvent._ +class StoreExportJobInRedisPresAnnEvent extends AbstractPresentationWithAnnotations { + import StoreExportJobInRedisPresAnnEvent._ - setEvent("StoreExportJobInRedisRecordEvent") + setEvent("StoreExportJobInRedisPresAnnEvent") def setJobId(jobId: String) { eventMap.put(JOB_ID, jobId) @@ -60,7 +60,7 @@ class StoreExportJobInRedisRecordEvent extends AbstractPresentationWithAnnotatio } } -object StoreExportJobInRedisRecordEvent { +object StoreExportJobInRedisPresAnnEvent { protected final val JOB_ID = "jobId" protected final val JOB_TYPE = "jobType" protected final val PRES_ID = "presId" diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/ExportAnnotationsActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/ExportAnnotationsActor.scala new file mode 100755 index 0000000000..30acdf6205 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/ExportAnnotationsActor.scala @@ -0,0 +1,87 @@ +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, "Annotations") + } + + private def handleStoreExportJobInRedisSysMsg(msg: StoreExportJobInRedisSysMsg) { + val ev = new StoreExportJobInRedisPresAnnEvent() + + ev.setJobId(msg.body.exportJob.jobId) + ev.setJobType(msg.body.exportJob.jobType) + 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") + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala index 31005a14f9..09d56b2a7c 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala @@ -49,10 +49,6 @@ class RedisRecorderActor( redis.recordAndExpire(session, message) } - 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 @@ -77,8 +73,6 @@ class RedisRecorderActor( case m: CreateNewPresentationPodEvtMsg => handleCreateNewPresentationPodEvtMsg(m) case m: RemovePresentationPodEvtMsg => handleRemovePresentationPodEvtMsg(m) case m: SetPresenterInPodRespMsg => handleSetPresenterInPodRespMsg(m) - case m: StoreAnnotationsInRedisSysMsg => handleStoreAnnotationsInRedisSysMsg(m) - case m: StoreExportJobInRedisSysMsg => handleStoreExportJobInRedisSysMsg(m) // Whiteboard case m: SendWhiteboardAnnotationEvtMsg => handleSendWhiteboardAnnotationEvtMsg(m) @@ -139,31 +133,6 @@ class RedisRecorderActor( } } - private def handleStoreAnnotationsInRedisSysMsg(msg: StoreAnnotationsInRedisSysMsg) { - val ev = new StorePresentationAnnotationsRecordEvent() - - 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, "Annotations") - } - - private def handleStoreExportJobInRedisSysMsg(msg: StoreExportJobInRedisSysMsg) { - val ev = new StoreExportJobInRedisRecordEvent() - - ev.setJobId(msg.body.exportJob.jobId) - ev.setJobType(msg.body.exportJob.jobType) - 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") - } - private def handleGroupChatMessageBroadcastEvtMsg(msg: GroupChatMessageBroadcastEvtMsg) { if (msg.body.chatId == GroupChatApp.MAIN_PUBLIC_CHAT) { val ev = new PublicChatRecordEvent() From 721edebd425cc6ea60b0845662efd89dc1a8949b Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Tue, 8 Feb 2022 20:32:03 +0100 Subject: [PATCH 015/268] Initial NodeJS app --- export-annotations/.gitignore | 1 + export-annotations/config/index.js | 3 + export-annotations/config/settings.json | 20 + export-annotations/index.js | 34 + export-annotations/lib/utils/logger.js | 51 + export-annotations/package-lock.json | 3734 +++++++++++++++++++++++ export-annotations/package.json | 17 + 7 files changed, 3860 insertions(+) create mode 100644 export-annotations/.gitignore create mode 100644 export-annotations/config/index.js create mode 100644 export-annotations/config/settings.json create mode 100644 export-annotations/index.js create mode 100644 export-annotations/lib/utils/logger.js create mode 100644 export-annotations/package-lock.json create mode 100644 export-annotations/package.json diff --git a/export-annotations/.gitignore b/export-annotations/.gitignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/export-annotations/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/export-annotations/config/index.js b/export-annotations/config/index.js new file mode 100644 index 0000000000..cf125020f9 --- /dev/null +++ b/export-annotations/config/index.js @@ -0,0 +1,3 @@ +const settings = require('./settings'); +const config = settings; +module.exports = config; diff --git a/export-annotations/config/settings.json b/export-annotations/config/settings.json new file mode 100644 index 0000000000..37ab421a9e --- /dev/null +++ b/export-annotations/config/settings.json @@ -0,0 +1,20 @@ +{ + "log": { + "level": "info" + }, + "master": { + "presAnnDropboxDir": "/tmp/pres-ann-dropbox" + }, + "collector": { + "presFileDir": "/var/bigbluebutton" + }, + "redis": { + "host": "127.0.0.1", + "port": 6379, + "password": null, + "interval": 1000, + "channels": { + "queue": "exportJobs" + } + } +} diff --git a/export-annotations/index.js b/export-annotations/index.js new file mode 100644 index 0000000000..9a50b102ff --- /dev/null +++ b/export-annotations/index.js @@ -0,0 +1,34 @@ +const Logger = require('./lib/utils/logger'); +const config = require('./config'); +const redis = require('redis'); + +const logger = new Logger('presAnn'); +logger.info("Started presAnn Master"); + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +(async () => { + const client = redis.createClient({ + host: config.redis.host, + port: config.redis.port, + password: config.redis.password + }); + + client.on('error', (err) => console.log('Redis Client Error', err)); + + await client.connect(); + + while (true) { + await sleep(config.redis.interval); + + var job = await client.LPOP(config.redis.channels.queue) + + if(job != null) { + logger.info('Received new job', job) + // Drop job into dropbox as JSON + + } + } +})(); diff --git a/export-annotations/lib/utils/logger.js b/export-annotations/lib/utils/logger.js new file mode 100644 index 0000000000..7e65980b01 --- /dev/null +++ b/export-annotations/lib/utils/logger.js @@ -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)); + } +}; diff --git a/export-annotations/package-lock.json b/export-annotations/package-lock.json new file mode 100644 index 0000000000..85a6e7afe9 --- /dev/null +++ b/export-annotations/package-lock.json @@ -0,0 +1,3734 @@ +{ + "name": "export-annotations", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@ampproject/remapping": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.1.0.tgz", + "integrity": "sha512-d5RysTlJ7hmw5Tw4UxgxcY3lkMe92n8sXCcuLPAyIAHK6j8DefDwtGnVVDgOnv+RnEosulDJ9NPKQL27bDId0g==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.0" + } + }, + "@babel/code-frame": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.16.7" + } + }, + "@babel/compat-data": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.0.tgz", + "integrity": "sha512-392byTlpGWXMv4FbyWw3sAZ/FrW/DrwqLGXpy0mbyNe9Taqv1mg9yON5/o0cnr8XYCkFTZbC1eV+c+LAROgrng==", + "dev": true + }, + "@babel/core": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.0.tgz", + "integrity": "sha512-x/5Ea+RO5MvF9ize5DeVICJoVrNv0Mi2RnIABrZEKYvPEpldXwauPkgvYA17cKa6WpU3LoYvYbuEMFtSNFsarA==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.0.0", + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.17.0", + "@babel/helper-compilation-targets": "^7.16.7", + "@babel/helper-module-transforms": "^7.16.7", + "@babel/helpers": "^7.17.0", + "@babel/parser": "^7.17.0", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.0", + "@babel/types": "^7.17.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.1.2", + "semver": "^6.3.0" + }, + "dependencies": { + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.0.tgz", + "integrity": "sha512-I3Omiv6FGOC29dtlZhkfXO6pgkmukJSlT26QjVvS1DGZe/NzSVCPG41X0tS21oZkJYlovfj9qDWgKP+Cn4bXxw==", + "dev": true, + "requires": { + "@babel/types": "^7.17.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/helper-compilation-targets": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz", + "integrity": "sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.16.4", + "@babel/helper-validator-option": "^7.16.7", + "browserslist": "^4.17.5", + "semver": "^6.3.0" + } + }, + "@babel/helper-environment-visitor": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz", + "integrity": "sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-function-name": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz", + "integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.16.7", + "@babel/template": "^7.16.7", + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz", + "integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", + "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-module-imports": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", + "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-module-transforms": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz", + "integrity": "sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-simple-access": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/helper-validator-identifier": "^7.16.7", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.16.7", + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz", + "integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==", + "dev": true + }, + "@babel/helper-simple-access": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz", + "integrity": "sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", + "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", + "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", + "dev": true + }, + "@babel/helpers": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.0.tgz", + "integrity": "sha512-Xe/9NFxjPwELUvW2dsukcMZIp6XwPSbI4ojFBJuX5ramHuVE22SVcZIwqzdWo5uCgeTXW8qV97lMvSOjq+1+nQ==", + "dev": true, + "requires": { + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.0", + "@babel/types": "^7.17.0" + } + }, + "@babel/highlight": { + "version": "7.16.10", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz", + "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.0.tgz", + "integrity": "sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw==", + "dev": true + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.16.7.tgz", + "integrity": "sha512-YhUIJHHGkqPgEcMYkPCKTyGUdoGKWtopIycQyjJH8OjvRgOYsXsaKehLVPScKJWAULPxMa4N1vCe6szREFlZ7A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/template": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", + "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.16.7", + "@babel/parser": "^7.16.7", + "@babel/types": "^7.16.7" + } + }, + "@babel/traverse": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.0.tgz", + "integrity": "sha512-fpFIXvqD6kC7c7PUNnZ0Z8cQXlarCLtCUpt2S1Dx7PjoRtCFffvOkHHSom+m5HIxMZn5bIBVb71lhabcmjEsqg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.17.0", + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-function-name": "^7.16.7", + "@babel/helper-hoist-variables": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/parser": "^7.17.0", + "@babel/types": "^7.17.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "dependencies": { + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", + "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "to-fast-properties": "^2.0.0" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.5.tgz", + "integrity": "sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.2.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@humanwhocodes/config-array": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.3.tgz", + "integrity": "sha512-3xSMlXHh03hCcCmFc0rbKp3Ivt2PFEJnQUJDDMTJQ2wkECZWdq4GePs2ctc5H8zV+cHPaq8k2vU8mrQjA6iHdQ==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "dependencies": { + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jest/console": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz", + "integrity": "sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==", + "dev": true, + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^27.5.1", + "jest-util": "^27.5.1", + "slash": "^3.0.0" + } + }, + "@jest/core": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-27.5.1.tgz", + "integrity": "sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ==", + "dev": true, + "requires": { + "@jest/console": "^27.5.1", + "@jest/reporters": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.8.1", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^27.5.1", + "jest-config": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-resolve-dependencies": "^27.5.1", + "jest-runner": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "jest-watcher": "^27.5.1", + "micromatch": "^4.0.4", + "rimraf": "^3.0.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "@jest/environment": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz", + "integrity": "sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==", + "dev": true, + "requires": { + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1" + } + }, + "@jest/fake-timers": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", + "integrity": "sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==", + "dev": true, + "requires": { + "@jest/types": "^27.5.1", + "@sinonjs/fake-timers": "^8.0.1", + "@types/node": "*", + "jest-message-util": "^27.5.1", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1" + } + }, + "@jest/globals": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-27.5.1.tgz", + "integrity": "sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==", + "dev": true, + "requires": { + "@jest/environment": "^27.5.1", + "@jest/types": "^27.5.1", + "expect": "^27.5.1" + } + }, + "@jest/reporters": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-27.5.1.tgz", + "integrity": "sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.2", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-haste-map": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "slash": "^3.0.0", + "source-map": "^0.6.0", + "string-length": "^4.0.1", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^8.1.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@jest/source-map": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz", + "integrity": "sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==", + "dev": true, + "requires": { + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@jest/test-result": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-27.5.1.tgz", + "integrity": "sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==", + "dev": true, + "requires": { + "@jest/console": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz", + "integrity": "sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ==", + "dev": true, + "requires": { + "@jest/test-result": "^27.5.1", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-runtime": "^27.5.1" + } + }, + "@jest/transform": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz", + "integrity": "sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/types": "^27.5.1", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-util": "^27.5.1", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.4.tgz", + "integrity": "sha512-cz8HFjOFfUBtvN+NXYSFMHYRdxZMaEl0XypVrhzxBgadKIXhIkRd8aMeHhmF56Sl7SuS8OnUpQ73/k9LE4VnLg==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.10.tgz", + "integrity": "sha512-Ht8wIW5v165atIX1p+JvKR5ONzUyF4Ac8DZIQ5kZs9zrb6M8SJNXpx1zn04rn65VjBMygRoMXcyYwNK0fT7bEg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.2.tgz", + "integrity": "sha512-9KzzH4kMjA2XmBRHfqG2/Vtl7s92l6uNDd0wW7frDE+EUvQFGqNXhWp0UGJjSkt3v2AYjzOZn1QO9XaTNJIt1Q==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@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==" + }, + "@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==" + }, + "@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==" + }, + "@node-redis/search": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@node-redis/search/-/search-1.0.2.tgz", + "integrity": "sha512-gWhEeji+kTAvzZeguUNJdMSZNH2c5dv3Bci8Nn2f7VGuf6IvvwuZDSBOuOlirLVgayVuWzAG7EhwaZWK1VDnWQ==" + }, + "@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==" + }, + "@sinonjs/commons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", + "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true + }, + "@types/babel__core": { + "version": "7.1.18", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.18.tgz", + "integrity": "sha512-S7unDjm/C7z2A2R9NzfKCK1I+BAALDtxEmsJBwlB3EzNfb929ykjL++1CK9LO++EIp2fQrC8O+BwjKvz6UeDyQ==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", + "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.14.2.tgz", + "integrity": "sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA==", + "dev": true, + "requires": { + "@babel/types": "^7.3.0" + } + }, + "@types/graceful-fs": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", + "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/node": { + "version": "17.0.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.16.tgz", + "integrity": "sha512-ydLaGVfQOQ6hI1xK2A5nVh8bl0OGoIfYMxPWHqqYe9bTkWCfqiVvZoh2I/QF2sNSkZzZyROBoTefIEI+PB6iIA==", + "dev": true + }, + "@types/prettier": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.4.3.tgz", + "integrity": "sha512-QzSuZMBuG5u8HqYz01qtMdg/Jfctlnvj1z/lYnIDXs/golxw0fxtRAHd9KrzjR7Yxz1qVeI00o0kiO3PmVdJ9w==", + "dev": true + }, + "@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "dev": true + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "20.2.1", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.1.tgz", + "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==", + "dev": true + }, + "abab": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true + }, + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "requires": { + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + }, + "dependencies": { + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + } + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "babel-jest": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz", + "integrity": "sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==", + "dev": true, + "requires": { + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^27.5.1", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + } + }, + "babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + } + }, + "babel-plugin-jest-hoist": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz", + "integrity": "sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==", + "dev": true, + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.0.0", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz", + "integrity": "sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^27.5.1", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", + "dev": true + }, + "browserslist": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.1.tgz", + "integrity": "sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001286", + "electron-to-chromium": "^1.4.17", + "escalade": "^3.1.1", + "node-releases": "^2.0.1", + "picocolors": "^1.0.0" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001309", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001309.tgz", + "integrity": "sha512-Pl8vfigmBXXq+/yUz1jUwULeq9xhMJznzdc/xwl4WclDAuebcTHVefpz8lE/bMI+UN7TOkSSe7B7RnZd6+dzjA==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true + }, + "ci-info": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz", + "integrity": "sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw==", + "dev": true + }, + "cjs-module-lexer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", + "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", + "dev": true + }, + "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==" + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "collect-v8-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", + "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "convert-source-map": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", + "dev": true + }, + "cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "requires": { + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + } + } + }, + "data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "dev": true, + "requires": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + } + }, + "decimal.js": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", + "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==", + "dev": true + }, + "dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", + "dev": true + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true + }, + "diff-sequences": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", + "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "domexception": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", + "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "dev": true, + "requires": { + "webidl-conversions": "^5.0.0" + }, + "dependencies": { + "webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "dev": true + } + } + }, + "electron-to-chromium": { + "version": "1.4.66", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.66.tgz", + "integrity": "sha512-f1RXFMsvwufWLwYUxTiP7HmjprKXrqEWHiQkjAYa9DJeVIlZk5v8gBGcaV+FhtXLly6C1OTVzQY+2UQrACiLlg==", + "dev": true + }, + "emittery": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", + "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "escodegen": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", + "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + } + } + }, + "eslint": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.8.0.tgz", + "integrity": "sha512-H3KXAzQGBH1plhYS3okDix2ZthuYJlQQEGE5k0IKuEqUSiyu4AmxxlJ2MtTYeJ3xB4jDhcYCwGOg2TXYdnDXlQ==", + "dev": true, + "requires": { + "@eslint/eslintrc": "^1.0.5", + "@humanwhocodes/config-array": "^0.9.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.0", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.2.0", + "espree": "^9.3.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^6.0.1", + "globals": "^13.6.0", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "eslint-scope": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz", + "integrity": "sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz", + "integrity": "sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ==", + "dev": true + }, + "espree": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.0.tgz", + "integrity": "sha512-d/5nCsb0JcqsSEeQzFZ8DH1RmxPcglRWh24EFTlUEmCKoehXGdpsx0RkHDubqUI8LSAIKMQp4r9SzQ3n+sm4HQ==", + "dev": true, + "requires": { + "acorn": "^8.7.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^3.1.0" + }, + "dependencies": { + "acorn": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", + "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "dev": true + } + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "expect": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", + "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==", + "dev": true, + "requires": { + "@jest/types": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fb-watchman": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", + "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", + "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", + "dev": true + }, + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "generic-pool": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.8.2.tgz", + "integrity": "sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg==" + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "13.12.1", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz", + "integrity": "sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "graceful-fs": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", + "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "html-encoding-sniffer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", + "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "dev": true, + "requires": { + "whatwg-encoding": "^1.0.5" + } + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dev": true, + "requires": { + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, + "ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-core-module": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", + "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.1.0.tgz", + "integrity": "sha512-czwUz525rkOFDJxfKK6mYfIs9zBKILyrZQxjz3ABhjQXhbhFsSbo1HW/BFcsDnfJYJWA6thRR5/TUY2qs5W99Q==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz", + "integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jest": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", + "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", + "dev": true, + "requires": { + "@jest/core": "^27.5.1", + "import-local": "^3.0.2", + "jest-cli": "^27.5.1" + }, + "dependencies": { + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "jest-cli": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.5.1.tgz", + "integrity": "sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==", + "dev": true, + "requires": { + "@jest/core": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "import-local": "^3.0.2", + "jest-config": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "prompts": "^2.0.1", + "yargs": "^16.2.0" + } + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + } + } + }, + "jest-changed-files": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz", + "integrity": "sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==", + "dev": true, + "requires": { + "@jest/types": "^27.5.1", + "execa": "^5.0.0", + "throat": "^6.0.1" + } + }, + "jest-circus": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.5.1.tgz", + "integrity": "sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw==", + "dev": true, + "requires": { + "@jest/environment": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^0.7.0", + "expect": "^27.5.1", + "is-generator-fn": "^2.0.0", + "jest-each": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3", + "throat": "^6.0.1" + } + }, + "jest-config": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.5.1.tgz", + "integrity": "sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==", + "dev": true, + "requires": { + "@babel/core": "^7.8.0", + "@jest/test-sequencer": "^27.5.1", + "@jest/types": "^27.5.1", + "babel-jest": "^27.5.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.1", + "graceful-fs": "^4.2.9", + "jest-circus": "^27.5.1", + "jest-environment-jsdom": "^27.5.1", + "jest-environment-node": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-jasmine2": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-runner": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + } + }, + "jest-diff": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", + "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + } + }, + "jest-docblock": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.5.1.tgz", + "integrity": "sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==", + "dev": true, + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.5.1.tgz", + "integrity": "sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==", + "dev": true, + "requires": { + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "jest-get-type": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1" + } + }, + "jest-environment-jsdom": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz", + "integrity": "sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==", + "dev": true, + "requires": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1", + "jsdom": "^16.6.0" + } + }, + "jest-environment-node": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.5.1.tgz", + "integrity": "sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw==", + "dev": true, + "requires": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1" + } + }, + "jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", + "dev": true + }, + "jest-haste-map": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz", + "integrity": "sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==", + "dev": true, + "requires": { + "@jest/types": "^27.5.1", + "@types/graceful-fs": "^4.1.2", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.3.2", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^27.5.1", + "jest-serializer": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "micromatch": "^4.0.4", + "walker": "^1.0.7" + } + }, + "jest-jasmine2": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz", + "integrity": "sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==", + "dev": true, + "requires": { + "@jest/environment": "^27.5.1", + "@jest/source-map": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "expect": "^27.5.1", + "is-generator-fn": "^2.0.0", + "jest-each": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1", + "throat": "^6.0.1" + } + }, + "jest-leak-detector": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz", + "integrity": "sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ==", + "dev": true, + "requires": { + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + } + }, + "jest-matcher-utils": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", + "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + } + }, + "jest-message-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", + "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-mock": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", + "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", + "dev": true, + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*" + } + }, + "jest-pnp-resolver": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", + "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", + "dev": true + }, + "jest-regex-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz", + "integrity": "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==", + "dev": true + }, + "jest-resolve": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz", + "integrity": "sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==", + "dev": true, + "requires": { + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "resolve": "^1.20.0", + "resolve.exports": "^1.1.0", + "slash": "^3.0.0" + } + }, + "jest-resolve-dependencies": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz", + "integrity": "sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==", + "dev": true, + "requires": { + "@jest/types": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-snapshot": "^27.5.1" + } + }, + "jest-runner": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-27.5.1.tgz", + "integrity": "sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==", + "dev": true, + "requires": { + "@jest/console": "^27.5.1", + "@jest/environment": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.8.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^27.5.1", + "jest-environment-jsdom": "^27.5.1", + "jest-environment-node": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-leak-detector": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "source-map-support": "^0.5.6", + "throat": "^6.0.1" + } + }, + "jest-runtime": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.5.1.tgz", + "integrity": "sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==", + "dev": true, + "requires": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/globals": "^27.5.1", + "@jest/source-map": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "execa": "^5.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-mock": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + } + }, + "jest-serializer": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz", + "integrity": "sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==", + "dev": true, + "requires": { + "@types/node": "*", + "graceful-fs": "^4.2.9" + } + }, + "jest-snapshot": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz", + "integrity": "sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==", + "dev": true, + "requires": { + "@babel/core": "^7.7.2", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", + "@babel/types": "^7.0.0", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/babel__traverse": "^7.0.4", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^27.5.1", + "graceful-fs": "^4.2.9", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-util": "^27.5.1", + "natural-compare": "^1.4.0", + "pretty-format": "^27.5.1", + "semver": "^7.3.2" + }, + "dependencies": { + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "dev": true, + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "jest-validate": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-27.5.1.tgz", + "integrity": "sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==", + "dev": true, + "requires": { + "@jest/types": "^27.5.1", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^27.5.1", + "leven": "^3.1.0", + "pretty-format": "^27.5.1" + }, + "dependencies": { + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + } + } + }, + "jest-watcher": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.5.1.tgz", + "integrity": "sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==", + "dev": true, + "requires": { + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "jest-util": "^27.5.1", + "string-length": "^4.0.1" + } + }, + "jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "jsdom": { + "version": "16.7.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", + "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", + "dev": true, + "requires": { + "abab": "^2.0.5", + "acorn": "^8.2.4", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.3.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.1", + "domexception": "^2.0.1", + "escodegen": "^2.0.0", + "form-data": "^3.0.0", + "html-encoding-sniffer": "^2.0.1", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.0", + "parse5": "6.0.1", + "saxes": "^5.0.1", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.0.0", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.5.0", + "ws": "^7.4.6", + "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "acorn": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", + "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "dev": true + }, + "acorn-globals": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", + "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + } + } + } + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "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==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "requires": { + "tmpl": "1.0.5" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + } + }, + "mime-db": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", + "dev": true + }, + "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==", + "dev": true, + "requires": { + "mime-db": "1.51.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", + "dev": true + }, + "node-releases": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz", + "integrity": "sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "nwsapi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pirates": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", + "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, + "prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "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" + } + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "dev": true, + "requires": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "resolve.exports": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz", + "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "dev": true, + "requires": { + "xmlchars": "^2.2.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "stack-utils": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz", + "integrity": "sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + } + } + }, + "string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-hyperlinks": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", + "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", + "dev": true, + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "throat": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz", + "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==", + "dev": true + }, + "tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "tough-cookie": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "dev": true, + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.1.2" + } + }, + "tr46": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", + "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "dev": true, + "requires": { + "punycode": "^2.1.1" + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "v8-to-istanbul": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", + "integrity": "sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + } + } + }, + "w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "dev": true, + "requires": { + "browser-process-hrtime": "^1.0.0" + } + }, + "w3c-xmlserializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", + "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "dev": true, + "requires": { + "xml-name-validator": "^3.0.0" + } + }, + "walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "requires": { + "makeerror": "1.0.12" + } + }, + "webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "dev": true + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "dev": true, + "requires": { + "iconv-lite": "0.4.24" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "dev": true + }, + "whatwg-url": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", + "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "dev": true, + "requires": { + "lodash": "^4.7.0", + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "ws": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz", + "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==", + "dev": true + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true + } + } +} diff --git a/export-annotations/package.json b/export-annotations/package.json new file mode 100644 index 0000000000..8e86a94063 --- /dev/null +++ b/export-annotations/package.json @@ -0,0 +1,17 @@ +{ + "name": "export-annotations", + "version": "0.0.1", + "description": "BigBlueButton's Annotation Exporter", + "scripts": { + "start": "node index.js", + "lint": "./node_modules/.bin/eslint lib/**", + "test": "jest" + }, + "dependencies": { + "redis": "^4.0.3" + }, + "devDependencies": { + "eslint": "^8.7.0", + "jest": "^27.4.7" + } +} From fdb46e5547f3f100f77623fc9891d067ba78d601 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Wed, 9 Feb 2022 13:45:02 +0100 Subject: [PATCH 016/268] presAnn master stores job as JSON, exportJob not containing annotations, presId fallback in meetingActor --- .../events/StoreExportJobInRedisPresAnnEvent.scala | 2 +- .../bigbluebutton/core/running/MeetingActor.scala | 11 ++++++++--- .../bigbluebutton/common2/msgs/WhiteboardMsgs.scala | 2 +- .../makePresentationWithAnnotationDownloadReqMsg.js | 12 +++++------- export-annotations/index.js | 10 +++++++++- 5 files changed, 24 insertions(+), 13 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StoreExportJobInRedisPresAnnEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StoreExportJobInRedisPresAnnEvent.scala index c5c4f919f0..79332f71d8 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StoreExportJobInRedisPresAnnEvent.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StoreExportJobInRedisPresAnnEvent.scala @@ -47,7 +47,7 @@ class StoreExportJobInRedisPresAnnEvent extends AbstractPresentationWithAnnotati eventMap.put(ALL_PAGES, allPages) } - def setPages(pages: Array[PresentationPageForExport]) { + def setPages(pages: List[Int]) { eventMap.put(PAGES, JsonUtil.toJson(pages)) } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index e8f34b9a03..edfeb12494 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -750,7 +750,12 @@ class MeetingActor( def handleMakePresentationWithAnnotationDownloadReqMsg(m: MakePresentationWithAnnotationDownloadReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = { - val presId: String = m.body.presId // Whiteboard ID + // Whiteboard ID + val presId: String = m.body.presId match { + case "" => getMeetingInfoPresentationDetails().id + case _ => m.body.presId + } + val allPages: Boolean = m.body.allPages // Whether or not all pages of the presentation should be exported val pages: List[Int] = m.body.pages // Desired presentation pages for export @@ -787,7 +792,7 @@ class MeetingActor( // 2) Insert Export Job in Redis val jobType = "PresentationWithAnnotationDownloadJob" val presLocation = s"/var/bigbluebutton/${presId}" - val exportJob = new ExportJob(jobId, jobType, presId, presLocation, allPages, storeAnnotationPages, "", "") + val exportJob = new ExportJob(jobId, jobType, presId, presLocation, allPages, pagesRange, "", "") var job = buildStoreExportJobInRedisSysMsg(exportJob) outGW.send(job) } @@ -836,7 +841,7 @@ class MeetingActor( // 2) Insert Export Job in Redis val jobType: String = "PresentationWithAnnotationExportJob" val presLocation = s"/var/bigbluebutton/${presId}" - val exportJob = new ExportJob(jobId, jobType, presId, presLocation, allPages, storeAnnotationPages, parentMeetingId, presentationUploadToken) + val exportJob = new ExportJob(jobId, jobType, presId, presLocation, allPages, pagesRange, parentMeetingId, presentationUploadToken) var job = buildStoreExportJobInRedisSysMsg(exportJob) outGW.send(job) } diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala index abc6862bfb..45eed23001 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala @@ -24,7 +24,7 @@ case class ExportJob( presId: String, presLocation: String, allPages: Boolean, - pages: Array[PresentationPageForExport], + pages: List[Int], parentMeetingId: String, presUploadToken: String, ) diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js b/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js index 07d81af15e..c3f393a3e0 100644 --- a/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js +++ b/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js @@ -17,13 +17,13 @@ export default function makePresentationWithAnnotationDownloadReqMsg() { check(requesterUserId, String); const payload = { - presId: "placeholder-pres-id", - allPages: true, - pages: [], + presId: "", + allPages: false, + pages: [2, 3, 8], }; const payload2 = { - parentMeetingId: "placeholder-parent-meeting-id", + parentMeetingId: "", allPages: false, }; @@ -34,9 +34,7 @@ export default function makePresentationWithAnnotationDownloadReqMsg() { Logger.warn(requesterUserId) Logger.warn('************'); - return RedisPubSub.publishUserMessage(CHANNEL, SECOND_EVENT_NAME, meetingId, requesterUserId, payload2); - - // return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); + return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); } catch (err) { Logger.error(`Exception while invoking method makePresentationWithAnnotationDownloadReqMsg ${err.stack}`); } diff --git a/export-annotations/index.js b/export-annotations/index.js index 9a50b102ff..48b7a20dd3 100644 --- a/export-annotations/index.js +++ b/export-annotations/index.js @@ -1,5 +1,6 @@ const Logger = require('./lib/utils/logger'); const config = require('./config'); +const fs = require('fs'); const redis = require('redis'); const logger = new Logger('presAnn'); @@ -25,10 +26,17 @@ function sleep(ms) { var job = await client.LPOP(config.redis.channels.queue) + const exportJob = JSON.parse(job); + if(job != null) { logger.info('Received new job', job) - // Drop job into dropbox as JSON + // Drop job into dropbox as JSON + fs.writeFile(config.master.presAnnDropboxDir + '/' + exportJob.jobId, job, function(err) { + if(err) { return logger.error(err); } + }); + + // Kicks processing by launching the Collector Worker passing JobId } } })(); From 410af1779f88f1b530e1d49c04a4dd8a94673732 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Wed, 9 Feb 2022 18:14:52 +0100 Subject: [PATCH 017/268] Start implementation of the collector worker --- export-annotations/index.js | 42 ------------------- export-annotations/master.js | 56 +++++++++++++++++++++++++ export-annotations/workers/collector.js | 23 ++++++++++ 3 files changed, 79 insertions(+), 42 deletions(-) delete mode 100644 export-annotations/index.js create mode 100644 export-annotations/master.js create mode 100644 export-annotations/workers/collector.js diff --git a/export-annotations/index.js b/export-annotations/index.js deleted file mode 100644 index 48b7a20dd3..0000000000 --- a/export-annotations/index.js +++ /dev/null @@ -1,42 +0,0 @@ -const Logger = require('./lib/utils/logger'); -const config = require('./config'); -const fs = require('fs'); -const redis = require('redis'); - -const logger = new Logger('presAnn'); -logger.info("Started presAnn Master"); - -function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -(async () => { - const client = redis.createClient({ - host: config.redis.host, - port: config.redis.port, - password: config.redis.password - }); - - client.on('error', (err) => console.log('Redis Client Error', err)); - - await client.connect(); - - while (true) { - await sleep(config.redis.interval); - - var job = await client.LPOP(config.redis.channels.queue) - - const exportJob = JSON.parse(job); - - if(job != null) { - logger.info('Received new job', job) - - // Drop job into dropbox as JSON - fs.writeFile(config.master.presAnnDropboxDir + '/' + exportJob.jobId, job, function(err) { - if(err) { return logger.error(err); } - }); - - // Kicks processing by launching the Collector Worker passing JobId - } - } -})(); diff --git a/export-annotations/master.js b/export-annotations/master.js new file mode 100644 index 0000000000..1e313d7b72 --- /dev/null +++ b/export-annotations/master.js @@ -0,0 +1,56 @@ +const Logger = require('./lib/utils/logger'); +const config = require('./config'); +const fs = require('fs'); +const redis = require('redis'); +const { Worker } = require('worker_threads') + +const logger = new Logger('presAnn Master'); +logger.info("Running export-annotations"); + +const kickOffCollectorWorker = (jobId) => { + return new Promise((resolve, reject) => { + const worker = new Worker('./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}`)); + }) + }) +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +(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(); + + while (true) { + await sleep(config.redis.interval); + + var job = await client.LPOP(config.redis.channels.queue) + + const exportJob = JSON.parse(job); + + if(job != null) { + logger.info('Received job', job) + + // Drop job into dropbox as JSON + fs.writeFile(config.shared.presAnnDropboxDir + '/' + exportJob.jobId, job, function(err) { + if(err) { return logger.error(err); } + }); + + const result = await kickOffCollectorWorker(exportJob.jobId) + logger.info(result); + } + } +})(); diff --git a/export-annotations/workers/collector.js b/export-annotations/workers/collector.js new file mode 100644 index 0000000000..6b240b738f --- /dev/null +++ b/export-annotations/workers/collector.js @@ -0,0 +1,23 @@ +const Logger = require('../lib/utils/logger'); +const config = require('../config'); +const fs = require('fs'); +const redis = require('redis'); + +const { workerData, parentPort } = require('worker_threads') + +const jobId = workerData; + +const logger = new Logger('presAnn Collector'); +logger.info("Collecting job " + jobId); + +// Takes the Job from the dropbox +console.log() + +let job = fs.readFileSync(config.shared.presAnnDropboxDir + '/' + jobId); +let exportJob = JSON.parse(job); + +// Collect the annotations from Redis + +// Collect the Presentation Page files from the presentation dir + +parentPort.postMessage({ message: workerData }) From d6eb8b602f50bd6a0e624125cedd70ed7d44f0c2 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Thu, 10 Feb 2022 12:48:54 +0100 Subject: [PATCH 018/268] Point PresLocation to SVG files, collector taking annotations from Redis --- .../core/running/MeetingActor.scala | 6 ++++- export-annotations/master.js | 2 +- export-annotations/workers/collector.js | 27 +++++++++++++++++-- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index edfeb12494..4b331dc1dd 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -750,6 +750,8 @@ class MeetingActor( def handleMakePresentationWithAnnotationDownloadReqMsg(m: MakePresentationWithAnnotationDownloadReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = { + val meetingId = liveMeeting.props.meetingProp.intId + // Whiteboard ID val presId: String = m.body.presId match { case "" => getMeetingInfoPresentationDetails().id @@ -791,7 +793,7 @@ class MeetingActor( // 2) Insert Export Job in Redis val jobType = "PresentationWithAnnotationDownloadJob" - val presLocation = s"/var/bigbluebutton/${presId}" + val presLocation = s"/var/bigbluebutton/${meetingId}/${meetingId}/${presId}/svgs" val exportJob = new ExportJob(jobId, jobType, presId, presLocation, allPages, pagesRange, "", "") var job = buildStoreExportJobInRedisSysMsg(exportJob) outGW.send(job) @@ -799,6 +801,8 @@ class MeetingActor( def handleExportPresentationWithAnnotationReqMsg(m: ExportPresentationWithAnnotationReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = { + val meetingId = liveMeeting.props.meetingProp.intId + val userId = m.header.userId val presId: String = getMeetingInfoPresentationDetails.id val parentMeetingId: String = m.body.parentMeetingId diff --git a/export-annotations/master.js b/export-annotations/master.js index 1e313d7b72..85e7b17ec8 100644 --- a/export-annotations/master.js +++ b/export-annotations/master.js @@ -37,7 +37,7 @@ function sleep(ms) { while (true) { await sleep(config.redis.interval); - var job = await client.LPOP(config.redis.channels.queue) + let job = await client.LPOP(config.redis.channels.queue) const exportJob = JSON.parse(job); diff --git a/export-annotations/workers/collector.js b/export-annotations/workers/collector.js index 6b240b738f..6bd97f3759 100644 --- a/export-annotations/workers/collector.js +++ b/export-annotations/workers/collector.js @@ -16,8 +16,31 @@ console.log() let job = fs.readFileSync(config.shared.presAnnDropboxDir + '/' + jobId); let exportJob = JSON.parse(job); -// Collect the annotations from Redis +// presId +// pages +// presLocation +// jobType + +// 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); + const annotations = JSON.parse(JSON.stringify(presAnn)); + + // Drop annotations as JSON in the dropbox + console.log(annotations) +})() + +// Collect the Presentation Page files from the presentation directory -// Collect the Presentation Page files from the presentation dir parentPort.postMessage({ message: workerData }) From f299947216be17a766e3f35c492542f5c3ed0a9b Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Sat, 12 Feb 2022 21:03:07 +0100 Subject: [PATCH 019/268] Collector worker --- .../core/running/MeetingActor.scala | 2 +- export-annotations/config/settings.json | 8 ++- export-annotations/master.js | 9 ++-- export-annotations/package.json | 2 +- export-annotations/workers/collector.js | 53 +++++++++++++------ export-annotations/workers/process.js | 23 ++++++++ 6 files changed, 70 insertions(+), 27 deletions(-) create mode 100644 export-annotations/workers/process.js diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index 4b331dc1dd..16e419ccb3 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -844,7 +844,7 @@ class MeetingActor( // 2) Insert Export Job in Redis val jobType: String = "PresentationWithAnnotationExportJob" - val presLocation = s"/var/bigbluebutton/${presId}" + val presLocation = s"/var/bigbluebutton/${meetingId}/${meetingId}/${presId}/svgs" val exportJob = new ExportJob(jobId, jobType, presId, presLocation, allPages, pagesRange, parentMeetingId, presentationUploadToken) var job = buildStoreExportJobInRedisSysMsg(exportJob) outGW.send(job) diff --git a/export-annotations/config/settings.json b/export-annotations/config/settings.json index 37ab421a9e..12b09c8ad3 100644 --- a/export-annotations/config/settings.json +++ b/export-annotations/config/settings.json @@ -2,11 +2,9 @@ "log": { "level": "info" }, - "master": { - "presAnnDropboxDir": "/tmp/pres-ann-dropbox" - }, - "collector": { - "presFileDir": "/var/bigbluebutton" + "shared": { + "presDir": "/var/bigbluebutton", + "presAnnDropboxDir": "/tmp/pres-ann-dropbox" }, "redis": { "host": "127.0.0.1", diff --git a/export-annotations/master.js b/export-annotations/master.js index 85e7b17ec8..7def6658bd 100644 --- a/export-annotations/master.js +++ b/export-annotations/master.js @@ -44,13 +44,16 @@ function sleep(ms) { if(job != null) { logger.info('Received job', job) + // Create folder in dropbox + let dropbox = config.shared.presAnnDropboxDir + '/' + exportJob.jobId + fs.mkdirSync(dropbox, { recursive: true }) + // Drop job into dropbox as JSON - fs.writeFile(config.shared.presAnnDropboxDir + '/' + exportJob.jobId, job, function(err) { + fs.writeFile(dropbox + '/job', job, function(err) { if(err) { return logger.error(err); } }); - const result = await kickOffCollectorWorker(exportJob.jobId) - logger.info(result); + kickOffCollectorWorker(exportJob.jobId) } } })(); diff --git a/export-annotations/package.json b/export-annotations/package.json index 8e86a94063..eab8150703 100644 --- a/export-annotations/package.json +++ b/export-annotations/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "description": "BigBlueButton's Annotation Exporter", "scripts": { - "start": "node index.js", + "start": "node master.js", "lint": "./node_modules/.bin/eslint lib/**", "test": "jest" }, diff --git a/export-annotations/workers/collector.js b/export-annotations/workers/collector.js index 6bd97f3759..c3d14dde34 100644 --- a/export-annotations/workers/collector.js +++ b/export-annotations/workers/collector.js @@ -3,24 +3,31 @@ const config = require('../config'); const fs = require('fs'); const redis = require('redis'); -const { workerData, parentPort } = require('worker_threads') +const { Worker, workerData, parentPort } = require('worker_threads') 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}`)); + }) + }) +} + +let dropbox = config.shared.presAnnDropboxDir + '/' + jobId + // Takes the Job from the dropbox -console.log() - -let job = fs.readFileSync(config.shared.presAnnDropboxDir + '/' + jobId); +let job = fs.readFileSync(dropbox + '/job'); let exportJob = JSON.parse(job); -// presId -// pages -// presLocation -// jobType - // Collect the annotations from Redis (async () => { const client = redis.createClient({ @@ -32,15 +39,27 @@ let exportJob = JSON.parse(job); client.on('error', (err) => logger.info('Redis Client Error', err)); await client.connect(); - - let presAnn = await client.hGetAll(exportJob.jobId); - const annotations = JSON.parse(JSON.stringify(presAnn)); - // Drop annotations as JSON in the dropbox - console.log(annotations) + let presAnn = await client.hGetAll(exportJob.jobId); + let annotations = JSON.stringify(presAnn); + + let whiteboard = JSON.parse(annotations); + let pages = JSON.parse(whiteboard.pages); + + fs.writeFile(dropbox + '/whiteboard', annotations, function(err) { + if(err) { return logger.error(err); } + }); + + // Collect the Presentation Page files from the presentation directory + for (let i = 0; i < pages.length; i++) { + let pageNumber = pages[i].page + let slide = exportJob.presLocation + '/slide' + pageNumber + '.svg' + let file = dropbox + '/slide' + pageNumber + '.svg' + + fs.copyFile(slide, file, (err) => { if (err) throw err; } ); + } + + kickOffProcessWorker(exportJob.jobId) })() -// Collect the Presentation Page files from the presentation directory - - parentPort.postMessage({ message: workerData }) diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js new file mode 100644 index 0000000000..52222cacb0 --- /dev/null +++ b/export-annotations/workers/process.js @@ -0,0 +1,23 @@ +const Logger = require('../lib/utils/logger'); +const config = require('../config'); +const fs = require('fs'); + +const { workerData, parentPort } = require('worker_threads') + +const jobId = workerData; + +const logger = new Logger('presAnn Process Worker'); +logger.info("Processing PDF for job " + jobId); + +// Process the presentation pages and annotations into a PDF file + +// 1. Get the job +// 2. Get the annotations +// 3. Convert annotations to SVG +// 4. Overlay annotations onto slides + +// Resulting PDF file is stored in the presentation dir + +// Launch Notifier Worker depending on job type + +parentPort.postMessage({ message: workerData }) From f3dbd06ddeb414c0124e6e2cd1fcde9a35545426 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Sun, 13 Feb 2022 18:10:37 +0100 Subject: [PATCH 020/268] Generate slide SVGs (without annotations) in Process Worker --- export-annotations/master.js | 4 +-- export-annotations/package.json | 3 +- export-annotations/workers/collector.js | 10 +++---- export-annotations/workers/process.js | 38 +++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 8 deletions(-) diff --git a/export-annotations/master.js b/export-annotations/master.js index 7def6658bd..240d7419cb 100644 --- a/export-annotations/master.js +++ b/export-annotations/master.js @@ -45,11 +45,11 @@ function sleep(ms) { logger.info('Received job', job) // Create folder in dropbox - let dropbox = config.shared.presAnnDropboxDir + '/' + exportJob.jobId + let dropbox = `${config.shared.presAnnDropboxDir}/${exportJob.jobId}` fs.mkdirSync(dropbox, { recursive: true }) // Drop job into dropbox as JSON - fs.writeFile(dropbox + '/job', job, function(err) { + fs.writeFile(`${dropbox}/job`, job, function(err) { if(err) { return logger.error(err); } }); diff --git a/export-annotations/package.json b/export-annotations/package.json index eab8150703..668b949ca2 100644 --- a/export-annotations/package.json +++ b/export-annotations/package.json @@ -8,7 +8,8 @@ "test": "jest" }, "dependencies": { - "redis": "^4.0.3" + "redis": "^4.0.3", + "xmlbuilder2": "^3.0.2" }, "devDependencies": { "eslint": "^8.7.0", diff --git a/export-annotations/workers/collector.js b/export-annotations/workers/collector.js index c3d14dde34..8f77232f69 100644 --- a/export-annotations/workers/collector.js +++ b/export-annotations/workers/collector.js @@ -22,10 +22,10 @@ const kickOffProcessWorker = (jobId) => { }) } -let dropbox = config.shared.presAnnDropboxDir + '/' + jobId +let dropbox = `${config.shared.presAnnDropboxDir}/${jobId}` // Takes the Job from the dropbox -let job = fs.readFileSync(dropbox + '/job'); +let job = fs.readFileSync(`${dropbox}/job`); let exportJob = JSON.parse(job); // Collect the annotations from Redis @@ -46,15 +46,15 @@ let exportJob = JSON.parse(job); let whiteboard = JSON.parse(annotations); let pages = JSON.parse(whiteboard.pages); - fs.writeFile(dropbox + '/whiteboard', annotations, function(err) { + fs.writeFile(`${dropbox}/whiteboard`, annotations, function(err) { if(err) { return logger.error(err); } }); // Collect the Presentation Page files from the presentation directory for (let i = 0; i < pages.length; i++) { let pageNumber = pages[i].page - let slide = exportJob.presLocation + '/slide' + pageNumber + '.svg' - let file = dropbox + '/slide' + pageNumber + '.svg' + let slide = `${exportJob.presLocation}/slide${pageNumber}.svg` + let file = `${dropbox}/slide${pageNumber}.svg` fs.copyFile(slide, file, (err) => { if (err) throw err; } ); } diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js index 52222cacb0..c5bae6bc38 100644 --- a/export-annotations/workers/process.js +++ b/export-annotations/workers/process.js @@ -1,6 +1,7 @@ const Logger = require('../lib/utils/logger'); const config = require('../config'); const fs = require('fs'); +const { create } = require('xmlbuilder2', { encoding: 'utf-8' }); const { workerData, parentPort } = require('worker_threads') @@ -12,8 +13,45 @@ logger.info("Processing PDF for job " + jobId); // Process the presentation pages and annotations into a PDF file // 1. Get the job +let dropbox = `${config.shared.presAnnDropboxDir}/${jobId}` +let job = fs.readFileSync(`${dropbox}/job`); +let exportJob = JSON.parse(job); + // 2. Get the annotations +let annotations = fs.readFileSync(`${dropbox}/whiteboard`); +let whiteboard = JSON.parse(annotations); +let pages = JSON.parse(whiteboard.pages); + // 3. Convert annotations to SVG +for (let i = 0; i < pages.length; i++) { + let currentSlide = pages[i] + + // Create the SVG slide with the background image + const 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', + viewBox: `${currentSlide.xOffset} ${currentSlide.yOffset} ${currentSlide.widthRatio}% ${currentSlide.heightRatio}%` + }) + .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${pages[i].page}.svg`, + width: '100%', + height: '100%' + }); + + const xml = svg.end({ prettyPrint: true }); + console.log(xml); + + // Write annotated SVG file + fs.writeFile(`${dropbox}/annotated-slide${pages[i].page}.svg`, xml, function(err) { + if(err) { return logger.error(err); } + }); +} + // 4. Overlay annotations onto slides // Resulting PDF file is stored in the presentation dir From 31c2a2bc42305697dd56413883286b30d80e4843 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Tue, 15 Feb 2022 13:40:08 +0100 Subject: [PATCH 021/268] Get dimensions of slide --- export-annotations/package.json | 1 + export-annotations/workers/process.js | 30 +++++++++++++++++++++------ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/export-annotations/package.json b/export-annotations/package.json index 668b949ca2..edf41e28c9 100644 --- a/export-annotations/package.json +++ b/export-annotations/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "redis": "^4.0.3", + "xml-js": "^1.6.11", "xmlbuilder2": "^3.0.2" }, "devDependencies": { diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js index c5bae6bc38..66816f4d33 100644 --- a/export-annotations/workers/process.js +++ b/export-annotations/workers/process.js @@ -1,6 +1,7 @@ const Logger = require('../lib/utils/logger'); const config = require('../config'); const fs = require('fs'); +const convert = require('xml-js'); const { create } = require('xmlbuilder2', { encoding: 'utf-8' }); const { workerData, parentPort } = require('worker_threads') @@ -24,14 +25,28 @@ let pages = JSON.parse(whiteboard.pages); // 3. Convert annotations to SVG for (let i = 0; i < pages.length; i++) { + + // Get the current slide (without annotations) let currentSlide = pages[i] + var backgroundSlide = fs.readFileSync(`${dropbox}/slide${pages[i].page}.svg`).toString(); + + // Read background slide in as JSON to determine dimensions + // TODO: find a better way to get width and height of slide (e.g. as part of message) + backgroundSlide = JSON.parse(convert.xml2json(backgroundSlide)); + + // There's a bug with older versions of rsvg which defaults SVG output to pixels. + // See: https://gitlab.gnome.org/GNOME/librsvg/-/issues/766 + var slideWidth = Number(backgroundSlide.elements[0].attributes.width.replace(/\D/g, "")) + var slideHeight = Number(backgroundSlide.elements[0].attributes.height.replace(/\D/g, "")) // Create the SVG slide with the background image - const svg = create({ version: '1.0', encoding: 'UTF-8' }) + 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', - viewBox: `${currentSlide.xOffset} ${currentSlide.yOffset} ${currentSlide.widthRatio}% ${currentSlide.heightRatio}%` + width: slideWidth, + height: slideHeight, + viewBox: `${currentSlide.xOffset} ${currentSlide.yOffset} ${slideWidth * currentSlide.widthRatio} ${slideHeight * currentSlide.heightRatio}` }) .dtd({ pubID: '-//W3C//DTD SVG 1.1//EN', @@ -43,18 +58,21 @@ for (let i = 0; i < pages.length; i++) { height: '100%' }); - const xml = svg.end({ prettyPrint: true }); - console.log(xml); + svg = svg.end({ prettyPrint: true }); // Write annotated SVG file - fs.writeFile(`${dropbox}/annotated-slide${pages[i].page}.svg`, xml, function(err) { + fs.writeFile(`${dropbox}/annotated-slide${pages[i].page}.svg`, svg, function(err) { if(err) { return logger.error(err); } }); + + // 4. Overlay annotations onto slides + + // rsvg-convert annotated-slide2.svg -f pdf -o out.pdf } -// 4. Overlay annotations onto slides // Resulting PDF file is stored in the presentation dir +// rsvg-convert annotated-slide2.svg annotated-slide3.svg ... -f pdf -o out.pdf // Launch Notifier Worker depending on job type From 925daca3ef53e8c2cbaa026b9e791f3215e61328 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Tue, 15 Feb 2022 16:48:58 +0100 Subject: [PATCH 022/268] Show pencil annotations --- export-annotations/workers/process.js | 105 ++++++++++++++++++++++++-- 1 file changed, 100 insertions(+), 5 deletions(-) diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js index 66816f4d33..3a5daf58c8 100644 --- a/export-annotations/workers/process.js +++ b/export-annotations/workers/process.js @@ -11,6 +11,92 @@ const jobId = workerData; const logger = new Logger('presAnn Process Worker'); logger.info("Processing PDF for job " + jobId); +function shape_scale(dimension, coord){ + return (coord / 100.0 * dimension) +} + +function overlay_pencil(svg, annotation, w, h) { + console.log('color') + console.log(annotation.color) + console.log('thickness') + console.log(annotation.thickness) + console.log('points') + console.log(annotation.points) + console.log('dimensions') + console.log(annotation.dimensions) + console.log('commands') + console.log(annotation.commands) + + const shapeColor = Number(annotation.color).toString(16) + + if (annotation.points.length < 2) { + logger.info("Pencil doesn't have enough points") + return; + } + + else if (annotation.points.length == 2) { + svg.ele('g', { + style: `stroke:none;fill:#${shapeColor}`, + }).ele('circle', { + cx: shape_scale(w, annotation.points[0]), + cy: shape_scale(h, annotation.points[1]), + r: shape_scale(w, annotation.thickness) / 2 + }).up() + } + + else { + let path = "" + let dataPoints = annotation.points + + for(let i = 0; i < annotation.commands.length; i++) { + switch(annotation.commands[i]){ + case 1: // MOVE TO + var x = shape_scale(w, dataPoints.shift()) + var y = shape_scale(h, dataPoints.shift()) + path = `${path} M${x} ${y}` + break; + case 2: // LINE TO + var x = shape_scale(w, dataPoints.shift()) + var y = shape_scale(h, dataPoints.shift()) + path = `${path} L${x} ${y}` + break; + case 4: // C_CURVE_TO + var cx1 = shape_scale(w, dataPoints.shift()) + var cy1 = shape_scale(h, dataPoints.shift()) + var cx2 = shape_scale(w, dataPoints.shift()) + var cy2 = shape_scale(h, dataPoints.shift()) + var x = shape_scale(w, dataPoints.shift()) + var y = shape_scale(h, dataPoints.shift()) + path = `${path} C${cx1} ${cy1},${cx2} ${cy2},${x} ${y}` + + break; + default: + logger.error(`Unknown pencil command: ${annotation.commands[i]}`) + } + } + + svg.ele('g', { + style: `stroke:#${shapeColor};stroke-linecap:round;stroke-linejoin:round;stroke-width:${shape_scale(w, annotation.thickness)};fill:none` + }).ele('path', { + d: path + }).up() + } + + console.log("-------------------------------------") +} + +function overlay_annotations(svg, annotations, w, h) { + for(let i = 0; i < annotations.length; i++){ + switch (annotations[i].annotationType) { + case 'pencil': + overlay_pencil(svg, annotations[i].annotationInfo, w, h) + break; + default: + logger.error(`Unknown annotation type ${annotations[i].annotationType}.`); + } + } +} + // Process the presentation pages and annotations into a PDF file // 1. Get the job @@ -46,7 +132,7 @@ for (let i = 0; i < pages.length; i++) { 'xmlns:xlink': 'http://www.w3.org/1999/xlink', width: slideWidth, height: slideHeight, - viewBox: `${currentSlide.xOffset} ${currentSlide.yOffset} ${slideWidth * currentSlide.widthRatio} ${slideHeight * currentSlide.heightRatio}` + viewBox: `${currentSlide.xOffset} ${currentSlide.yOffset} ${slideWidth * currentSlide.widthRatio / 100} ${slideHeight * currentSlide.heightRatio / 100}` }) .dtd({ pubID: '-//W3C//DTD SVG 1.1//EN', @@ -56,21 +142,30 @@ for (let i = 0; i < pages.length; i++) { 'xlink:href': `file://${dropbox}/slide${pages[i].page}.svg`, width: '100%', height: '100%' + }) + .up() + .ele('g', { + class: 'canvas' }); + // 4. Overlay annotations onto slides + console.log("=====================================") + + overlay_annotations(svg, pages[i].annotations, slideWidth, slideHeight) + + console.log("=====================================") + console.log() + svg = svg.end({ prettyPrint: true }); + console.log (svg) // Write annotated SVG file fs.writeFile(`${dropbox}/annotated-slide${pages[i].page}.svg`, svg, function(err) { if(err) { return logger.error(err); } }); - - // 4. Overlay annotations onto slides - // rsvg-convert annotated-slide2.svg -f pdf -o out.pdf } - // Resulting PDF file is stored in the presentation dir // rsvg-convert annotated-slide2.svg annotated-slide3.svg ... -f pdf -o out.pdf From 6455a8e738a38b3b6dde3d9304db765f86c941cf Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Tue, 15 Feb 2022 18:11:13 +0100 Subject: [PATCH 023/268] Implement panzooms --- export-annotations/workers/process.js | 31 +++++++++------------------ 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js index 3a5daf58c8..8a161a4b2b 100644 --- a/export-annotations/workers/process.js +++ b/export-annotations/workers/process.js @@ -7,6 +7,7 @@ const { create } = require('xmlbuilder2', { encoding: 'utf-8' }); const { workerData, parentPort } = require('worker_threads') const jobId = workerData; +const MAGIC_MYSTERY_NUMBER = 2; const logger = new Logger('presAnn Process Worker'); logger.info("Processing PDF for job " + jobId); @@ -16,17 +17,6 @@ function shape_scale(dimension, coord){ } function overlay_pencil(svg, annotation, w, h) { - console.log('color') - console.log(annotation.color) - console.log('thickness') - console.log(annotation.thickness) - console.log('points') - console.log(annotation.points) - console.log('dimensions') - console.log(annotation.dimensions) - console.log('commands') - console.log(annotation.commands) - const shapeColor = Number(annotation.color).toString(16) if (annotation.points.length < 2) { @@ -81,8 +71,6 @@ function overlay_pencil(svg, annotation, w, h) { d: path }).up() } - - console.log("-------------------------------------") } function overlay_annotations(svg, annotations, w, h) { @@ -125,6 +113,11 @@ for (let i = 0; i < pages.length; i++) { var slideWidth = Number(backgroundSlide.elements[0].attributes.width.replace(/\D/g, "")) var slideHeight = Number(backgroundSlide.elements[0].attributes.height.replace(/\D/g, "")) + var panzoom_x = -currentSlide.xOffset * MAGIC_MYSTERY_NUMBER / 100.0 * slideWidth + var panzoom_y = -currentSlide.yOffset * MAGIC_MYSTERY_NUMBER / 100.0 * slideHeight + var panzoom_w = shape_scale(slideWidth, currentSlide.widthRatio) + var panzoom_h = shape_scale(slideHeight, currentSlide.heightRatio) + // Create the SVG slide with the background image let svg = create({ version: '1.0', encoding: 'UTF-8' }) .ele('svg', { @@ -132,7 +125,7 @@ for (let i = 0; i < pages.length; i++) { 'xmlns:xlink': 'http://www.w3.org/1999/xlink', width: slideWidth, height: slideHeight, - viewBox: `${currentSlide.xOffset} ${currentSlide.yOffset} ${slideWidth * currentSlide.widthRatio / 100} ${slideHeight * currentSlide.heightRatio / 100}` + viewBox: `${panzoom_x} ${panzoom_y} ${panzoom_w} ${panzoom_h}` }) .dtd({ pubID: '-//W3C//DTD SVG 1.1//EN', @@ -140,8 +133,8 @@ for (let i = 0; i < pages.length; i++) { }) .ele('image', { 'xlink:href': `file://${dropbox}/slide${pages[i].page}.svg`, - width: '100%', - height: '100%' + width: slideWidth, + height: slideHeight, }) .up() .ele('g', { @@ -149,13 +142,8 @@ for (let i = 0; i < pages.length; i++) { }); // 4. Overlay annotations onto slides - console.log("=====================================") - overlay_annotations(svg, pages[i].annotations, slideWidth, slideHeight) - console.log("=====================================") - console.log() - svg = svg.end({ prettyPrint: true }); console.log (svg) @@ -163,6 +151,7 @@ for (let i = 0; i < pages.length; i++) { fs.writeFile(`${dropbox}/annotated-slide${pages[i].page}.svg`, svg, function(err) { if(err) { return logger.error(err); } }); + // rsvg-convert annotated-slide2.svg -f pdf -o out.pdf } From 9aaa8ec71e6d546529e653e2f0bbeabe9d10abb9 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Tue, 15 Feb 2022 19:14:14 +0100 Subject: [PATCH 024/268] Generate (merged) annotated PDF --- export-annotations/package-lock.json | 101 ++++++++++++++++++++++++-- export-annotations/workers/process.js | 19 ++++- 2 files changed, 110 insertions(+), 10 deletions(-) diff --git a/export-annotations/package-lock.json b/export-annotations/package-lock.json index 85a6e7afe9..d41c465cfc 100644 --- a/export-annotations/package-lock.json +++ b/export-annotations/package-lock.json @@ -1,6 +1,6 @@ { "name": "export-annotations", - "version": "0.0.0", + "version": "0.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -868,6 +868,38 @@ "resolved": "https://registry.npmjs.org/@node-redis/time-series/-/time-series-1.0.1.tgz", "integrity": "sha512-+nTn6EewVj3GlUXPuD3dgheWqo219jTxlo6R+pg24OeVvFHx9aFGGiyOgj3vBPhWUdRZ0xMcujXV5ki4fbLyMw==" }, + "@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==" + }, "@sinonjs/commons": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", @@ -969,8 +1001,7 @@ "@types/node": { "version": "17.0.16", "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.16.tgz", - "integrity": "sha512-ydLaGVfQOQ6hI1xK2A5nVh8bl0OGoIfYMxPWHqqYe9bTkWCfqiVvZoh2I/QF2sNSkZzZyROBoTefIEI+PB6iIA==", - "dev": true + "integrity": "sha512-ydLaGVfQOQ6hI1xK2A5nVh8bl0OGoIfYMxPWHqqYe9bTkWCfqiVvZoh2I/QF2sNSkZzZyROBoTefIEI+PB6iIA==" }, "@types/prettier": { "version": "2.4.3", @@ -1663,8 +1694,7 @@ "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "esquery": { "version": "1.4.0", @@ -3269,6 +3299,11 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, "saxes": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", @@ -3338,8 +3373,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "stack-utils": { "version": "2.0.5", @@ -3701,12 +3735,65 @@ "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==", "dev": true }, + "xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "requires": { + "sax": "^1.2.4" + } + }, "xml-name-validator": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", "dev": true }, + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + }, + "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" + } + } + } + }, "xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js index 8a161a4b2b..f82cc561e0 100644 --- a/export-annotations/workers/process.js +++ b/export-annotations/workers/process.js @@ -3,6 +3,7 @@ const config = require('../config'); const fs = require('fs'); const convert = require('xml-js'); const { create } = require('xmlbuilder2', { encoding: 'utf-8' }); +const { exec } = require("child_process"); const { workerData, parentPort } = require('worker_threads') @@ -96,6 +97,7 @@ let exportJob = JSON.parse(job); let annotations = fs.readFileSync(`${dropbox}/whiteboard`); let whiteboard = JSON.parse(annotations); let pages = JSON.parse(whiteboard.pages); +let rsvgConvertInput = "" // 3. Convert annotations to SVG for (let i = 0; i < pages.length; i++) { @@ -148,15 +150,26 @@ for (let i = 0; i < pages.length; i++) { console.log (svg) // Write annotated SVG file - fs.writeFile(`${dropbox}/annotated-slide${pages[i].page}.svg`, svg, function(err) { + let file = `${dropbox}/annotated-slide${pages[i].page}.svg` + fs.writeFile(file, svg, function(err) { if(err) { return logger.error(err); } }); - // rsvg-convert annotated-slide2.svg -f pdf -o out.pdf + rsvgConvertInput += `${file} ` } // Resulting PDF file is stored in the presentation dir -// rsvg-convert annotated-slide2.svg annotated-slide3.svg ... -f pdf -o out.pdf +// TODO: change presLocation so it doesn't point to the 'svgs' directory +exec(`rsvg-convert ${rsvgConvertInput} -f pdf -o ${exportJob.presLocation}/../annotated_slides_${jobId}.pdf`, (error, stderr) => { + if (error) { + console.log(`SVG to PDF export failed with error: ${error.message}`); + return; + } + if (stderr) { + logger.error(`SVG to PDF export failed with stderr: ${stderr}`); + return; + } +}); // Launch Notifier Worker depending on job type From 539190bc470766ab38b36e91b0aeaa28bedcc855 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Wed, 16 Feb 2022 12:06:12 +0100 Subject: [PATCH 025/268] Implement rectangles --- export-annotations/workers/process.js | 30 +++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js index f82cc561e0..ef99f7636c 100644 --- a/export-annotations/workers/process.js +++ b/export-annotations/workers/process.js @@ -18,7 +18,7 @@ function shape_scale(dimension, coord){ } function overlay_pencil(svg, annotation, w, h) { - const shapeColor = Number(annotation.color).toString(16) + let shapeColor = Number(annotation.color).toString(16) if (annotation.points.length < 2) { logger.info("Pencil doesn't have enough points") @@ -74,12 +74,36 @@ function overlay_pencil(svg, annotation, w, h) { } } +function overlay_rectangle(svg, annotation, w, h){ + let shapeColor = Number(annotation.color).toString(16) + + let fill = annotation.fill ? `#${shapeColor}` : 'none'; + + let x1 = shape_scale(w, annotation.points[0]) + let y1 = shape_scale(h, annotation.points[1]) + let x2 = shape_scale(w, annotation.points[2]) + let y2 = shape_scale(h, annotation.points[3]) + + let path = `M ${x1} ${y1} L ${x2} ${y1} L ${x2} ${y2} L ${x1} ${y2} Z` + + svg.ele('g', { + style: `stroke:#${shapeColor};stroke-width:${shape_scale(w, annotation.thickness)};fill:${fill};stroke-linejoin:miter` + }).ele('path', { + d: path + }).up() +} + function overlay_annotations(svg, annotations, w, h) { - for(let i = 0; i < annotations.length; i++){ + console.log(annotations) + + for(let i = 0; i < annotations.length; i++) { switch (annotations[i].annotationType) { case 'pencil': overlay_pencil(svg, annotations[i].annotationInfo, w, h) break; + case 'rectangle': + overlay_rectangle(svg, annotations[i].annotationInfo, w, h) + break; default: logger.error(`Unknown annotation type ${annotations[i].annotationType}.`); } @@ -111,6 +135,7 @@ for (let i = 0; i < pages.length; i++) { backgroundSlide = JSON.parse(convert.xml2json(backgroundSlide)); // There's a bug with older versions of rsvg which defaults SVG output to pixels. + // So we ignore the units here as well. // See: https://gitlab.gnome.org/GNOME/librsvg/-/issues/766 var slideWidth = Number(backgroundSlide.elements[0].attributes.width.replace(/\D/g, "")) var slideHeight = Number(backgroundSlide.elements[0].attributes.height.replace(/\D/g, "")) @@ -172,5 +197,6 @@ exec(`rsvg-convert ${rsvgConvertInput} -f pdf -o ${exportJob.presLocation}/../an }); // Launch Notifier Worker depending on job type +logger.info(`Saved PDF at ${exportJob.presLocation}/../annotated_slides_${jobId}.pdf`) parentPort.postMessage({ message: workerData }) From 313d13fe89753db0eaa66a3178de111597c84a86 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Wed, 16 Feb 2022 14:28:35 +0100 Subject: [PATCH 026/268] Implement ellipses --- export-annotations/workers/process.js | 56 +++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js index ef99f7636c..c07bb81a6a 100644 --- a/export-annotations/workers/process.js +++ b/export-annotations/workers/process.js @@ -17,6 +17,44 @@ function shape_scale(dimension, coord){ return (coord / 100.0 * dimension) } +function overlay_ellipse(svg, annotation, w, h) { + let shapeColor = Number(annotation.color).toString(16) + let fill = annotation.fill ? `#${shapeColor}` : 'none'; + + let x1 = shape_scale(w, annotation.points[0]) + let y1 = shape_scale(h, annotation.points[1]) + let x2 = shape_scale(w, annotation.points[2]) + let y2 = shape_scale(h, annotation.points[3]) + + let width_r = Math.abs(x2 - x1) / 2 + let height_r = Math.abs(y2 - y1) / 2 + let hx = Math.abs(x1 + x2) / 2 + let hy = Math.abs(y1 + y2) / 2 + + // Normalize the x,y coordinates + if (x1 > x2) { + [x1, x2] = [x2, x1] + } + + if (y1 > y2) { + [y1, y2] = [y2, y1] + } + + path = `M${x1} ${hy} + A${width_r} ${height_r} 0 0 1 ${hx} ${y1} + A${width_r} ${height_r} 0 0 1 ${x2} ${hy} + A${width_r} ${height_r} 0 0 1 ${hx} ${y2} + A${width_r} ${height_r} 0 0 1 ${x1} ${hy} + Z` + + svg.ele('g', { + style: `stroke:#${shapeColor};stroke-width:${shape_scale(w, annotation.thickness)}; + fill:${fill};stroke-linejoin:miter;stroke-miterlimit:8` + }).ele('path', { + d: path + }).up() +} + function overlay_pencil(svg, annotation, w, h) { let shapeColor = Number(annotation.color).toString(16) @@ -67,16 +105,16 @@ function overlay_pencil(svg, annotation, w, h) { } svg.ele('g', { - style: `stroke:#${shapeColor};stroke-linecap:round;stroke-linejoin:round;stroke-width:${shape_scale(w, annotation.thickness)};fill:none` + style: `stroke:#${shapeColor};stroke-linecap:round;stroke-linejoin:round; + stroke-width:${shape_scale(w, annotation.thickness)};fill:none` }).ele('path', { d: path }).up() } } -function overlay_rectangle(svg, annotation, w, h){ +function overlay_rectangle(svg, annotation, w, h) { let shapeColor = Number(annotation.color).toString(16) - let fill = annotation.fill ? `#${shapeColor}` : 'none'; let x1 = shape_scale(w, annotation.points[0]) @@ -98,12 +136,23 @@ function overlay_annotations(svg, annotations, w, h) { for(let i = 0; i < annotations.length; i++) { switch (annotations[i].annotationType) { + case 'ellipse': + overlay_ellipse(svg, annotations[i].annotationInfo, w, h) + break; + case 'line': + break; + case 'poll_result': + break; case 'pencil': overlay_pencil(svg, annotations[i].annotationInfo, w, h) break; case 'rectangle': overlay_rectangle(svg, annotations[i].annotationInfo, w, h) break; + case 'text': + break; + case 'triangle': + break; default: logger.error(`Unknown annotation type ${annotations[i].annotationType}.`); } @@ -169,6 +218,7 @@ for (let i = 0; i < pages.length; i++) { }); // 4. Overlay annotations onto slides + // Based on /record-and-playback/presentation/scripts/publish/presentation.rb overlay_annotations(svg, pages[i].annotations, slideWidth, slideHeight) svg = svg.end({ prettyPrint: true }); From 6c74205f9ec5eb667bc6a2258fae4ad4c5dbf5a6 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Wed, 16 Feb 2022 14:49:41 +0100 Subject: [PATCH 027/268] export-annotations/workers/process.js --- export-annotations/workers/process.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js index c07bb81a6a..b7372fd4d6 100644 --- a/export-annotations/workers/process.js +++ b/export-annotations/workers/process.js @@ -131,23 +131,37 @@ function overlay_rectangle(svg, annotation, w, h) { }).up() } +function overlay_line(svg, annotation, w, h) { + let shapeColor = Number(annotation.color).toString(16) + + svg.ele('g', { + style: `stroke:#${shapeColor};stroke-width:${shape_scale(w, annotation.thickness)};stroke-linecap:butt` + }).ele('line', { + x1: shape_scale(w, annotation.points[0]), + y1: shape_scale(h, annotation.points[1]), + x2: shape_scale(w, annotation.points[2]), + y2: shape_scale(h, annotation.points[3]), + }).up() +} + function overlay_annotations(svg, annotations, w, h) { console.log(annotations) for(let i = 0; i < annotations.length; i++) { switch (annotations[i].annotationType) { case 'ellipse': - overlay_ellipse(svg, annotations[i].annotationInfo, w, h) + overlay_ellipse(svg, annotations[i].annotationInfo, w, h); break; case 'line': + overlay_line(svg, annotations[i].annotationInfo, w, h); break; case 'poll_result': break; case 'pencil': - overlay_pencil(svg, annotations[i].annotationInfo, w, h) + overlay_pencil(svg, annotations[i].annotationInfo, w, h); break; case 'rectangle': - overlay_rectangle(svg, annotations[i].annotationInfo, w, h) + overlay_rectangle(svg, annotations[i].annotationInfo, w, h); break; case 'text': break; From 0492e410a4bd6f1b0991b08a636240720ea8cd6b Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Wed, 16 Feb 2022 14:49:41 +0100 Subject: [PATCH 028/268] Implement triangles --- export-annotations/workers/process.js | 41 +++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js index c07bb81a6a..bfb944abbe 100644 --- a/export-annotations/workers/process.js +++ b/export-annotations/workers/process.js @@ -55,6 +55,19 @@ function overlay_ellipse(svg, annotation, w, h) { }).up() } +function overlay_line(svg, annotation, w, h) { + let shapeColor = Number(annotation.color).toString(16) + + svg.ele('g', { + style: `stroke:#${shapeColor};stroke-width:${shape_scale(w, annotation.thickness)};stroke-linecap:butt` + }).ele('line', { + x1: shape_scale(w, annotation.points[0]), + y1: shape_scale(h, annotation.points[1]), + x2: shape_scale(w, annotation.points[2]), + y2: shape_scale(h, annotation.points[3]), + }).up() +} + function overlay_pencil(svg, annotation, w, h) { let shapeColor = Number(annotation.color).toString(16) @@ -131,27 +144,49 @@ function overlay_rectangle(svg, annotation, w, h) { }).up() } +function overlay_triangle(svg, annotation, w, h) { + let shapeColor = Number(annotation.color).toString(16) + let fill = annotation.fill ? `#${shapeColor}` : 'none'; + + let x1 = shape_scale(w, annotation.points[0]) + let y1 = shape_scale(h, annotation.points[1]) + let x2 = shape_scale(w, annotation.points[2]) + let y2 = shape_scale(h, annotation.points[3]) + + let px = (x1 + x2) / 2 + + let path = `M${px} ${y1} L${x2} ${y2} L${x1} ${y2} Z` + + svg.ele('g', { + style: `stroke:#${shapeColor};stroke-width:${shape_scale(w, annotation.thickness)};fill:${fill};stroke-linejoin:miter;stroke-miterlimit:8` + }).ele('path', { + d: path + }).up() +} + function overlay_annotations(svg, annotations, w, h) { console.log(annotations) for(let i = 0; i < annotations.length; i++) { switch (annotations[i].annotationType) { case 'ellipse': - overlay_ellipse(svg, annotations[i].annotationInfo, w, h) + overlay_ellipse(svg, annotations[i].annotationInfo, w, h); break; case 'line': + overlay_line(svg, annotations[i].annotationInfo, w, h); break; case 'poll_result': break; case 'pencil': - overlay_pencil(svg, annotations[i].annotationInfo, w, h) + overlay_pencil(svg, annotations[i].annotationInfo, w, h); break; case 'rectangle': - overlay_rectangle(svg, annotations[i].annotationInfo, w, h) + overlay_rectangle(svg, annotations[i].annotationInfo, w, h); break; case 'text': break; case 'triangle': + overlay_triangle(svg, annotations[i].annotationInfo, w, h); break; default: logger.error(`Unknown annotation type ${annotations[i].annotationType}.`); From 7d437e17f4c3f3528b3417f8b1f06c3d3799c0b8 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Wed, 16 Feb 2022 19:42:46 +0100 Subject: [PATCH 029/268] Whiteboard text --- export-annotations/workers/process.js | 107 ++++++++++++++++---------- 1 file changed, 65 insertions(+), 42 deletions(-) diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js index dc7d41b1d1..383cccf1d1 100644 --- a/export-annotations/workers/process.js +++ b/export-annotations/workers/process.js @@ -13,7 +13,7 @@ const MAGIC_MYSTERY_NUMBER = 2; const logger = new Logger('presAnn Process Worker'); logger.info("Processing PDF for job " + jobId); -function shape_scale(dimension, coord){ +function scale_shape(dimension, coord){ return (coord / 100.0 * dimension) } @@ -21,10 +21,10 @@ function overlay_ellipse(svg, annotation, w, h) { let shapeColor = Number(annotation.color).toString(16) let fill = annotation.fill ? `#${shapeColor}` : 'none'; - let x1 = shape_scale(w, annotation.points[0]) - let y1 = shape_scale(h, annotation.points[1]) - let x2 = shape_scale(w, annotation.points[2]) - let y2 = shape_scale(h, annotation.points[3]) + let x1 = scale_shape(w, annotation.points[0]) + let y1 = scale_shape(h, annotation.points[1]) + let x2 = scale_shape(w, annotation.points[2]) + let y2 = scale_shape(h, annotation.points[3]) let width_r = Math.abs(x2 - x1) / 2 let height_r = Math.abs(y2 - y1) / 2 @@ -48,7 +48,7 @@ function overlay_ellipse(svg, annotation, w, h) { Z` svg.ele('g', { - style: `stroke:#${shapeColor};stroke-width:${shape_scale(w, annotation.thickness)}; + style: `stroke:#${shapeColor};stroke-width:${scale_shape(w, annotation.thickness)}; fill:${fill};stroke-linejoin:miter;stroke-miterlimit:8` }).ele('path', { d: path @@ -59,12 +59,12 @@ function overlay_line(svg, annotation, w, h) { let shapeColor = Number(annotation.color).toString(16) svg.ele('g', { - style: `stroke:#${shapeColor};stroke-width:${shape_scale(w, annotation.thickness)};stroke-linecap:butt` + style: `stroke:#${shapeColor};stroke-width:${scale_shape(w, annotation.thickness)};stroke-linecap:butt` }).ele('line', { - x1: shape_scale(w, annotation.points[0]), - y1: shape_scale(h, annotation.points[1]), - x2: shape_scale(w, annotation.points[2]), - y2: shape_scale(h, annotation.points[3]), + x1: scale_shape(w, annotation.points[0]), + y1: scale_shape(h, annotation.points[1]), + x2: scale_shape(w, annotation.points[2]), + y2: scale_shape(h, annotation.points[3]), }).up() } @@ -80,9 +80,9 @@ function overlay_pencil(svg, annotation, w, h) { svg.ele('g', { style: `stroke:none;fill:#${shapeColor}`, }).ele('circle', { - cx: shape_scale(w, annotation.points[0]), - cy: shape_scale(h, annotation.points[1]), - r: shape_scale(w, annotation.thickness) / 2 + cx: scale_shape(w, annotation.points[0]), + cy: scale_shape(h, annotation.points[1]), + r: scale_shape(w, annotation.thickness) / 2 }).up() } @@ -93,22 +93,22 @@ function overlay_pencil(svg, annotation, w, h) { for(let i = 0; i < annotation.commands.length; i++) { switch(annotation.commands[i]){ case 1: // MOVE TO - var x = shape_scale(w, dataPoints.shift()) - var y = shape_scale(h, dataPoints.shift()) + var x = scale_shape(w, dataPoints.shift()) + var y = scale_shape(h, dataPoints.shift()) path = `${path} M${x} ${y}` break; case 2: // LINE TO - var x = shape_scale(w, dataPoints.shift()) - var y = shape_scale(h, dataPoints.shift()) + var x = scale_shape(w, dataPoints.shift()) + var y = scale_shape(h, dataPoints.shift()) path = `${path} L${x} ${y}` break; case 4: // C_CURVE_TO - var cx1 = shape_scale(w, dataPoints.shift()) - var cy1 = shape_scale(h, dataPoints.shift()) - var cx2 = shape_scale(w, dataPoints.shift()) - var cy2 = shape_scale(h, dataPoints.shift()) - var x = shape_scale(w, dataPoints.shift()) - var y = shape_scale(h, dataPoints.shift()) + var cx1 = scale_shape(w, dataPoints.shift()) + var cy1 = scale_shape(h, dataPoints.shift()) + var cx2 = scale_shape(w, dataPoints.shift()) + var cy2 = scale_shape(h, dataPoints.shift()) + var x = scale_shape(w, dataPoints.shift()) + var y = scale_shape(h, dataPoints.shift()) path = `${path} C${cx1} ${cy1},${cx2} ${cy2},${x} ${y}` break; @@ -119,7 +119,7 @@ function overlay_pencil(svg, annotation, w, h) { svg.ele('g', { style: `stroke:#${shapeColor};stroke-linecap:round;stroke-linejoin:round; - stroke-width:${shape_scale(w, annotation.thickness)};fill:none` + stroke-width:${scale_shape(w, annotation.thickness)};fill:none` }).ele('path', { d: path }).up() @@ -130,15 +130,15 @@ function overlay_rectangle(svg, annotation, w, h) { let shapeColor = Number(annotation.color).toString(16) let fill = annotation.fill ? `#${shapeColor}` : 'none'; - let x1 = shape_scale(w, annotation.points[0]) - let y1 = shape_scale(h, annotation.points[1]) - let x2 = shape_scale(w, annotation.points[2]) - let y2 = shape_scale(h, annotation.points[3]) + let x1 = scale_shape(w, annotation.points[0]) + let y1 = scale_shape(h, annotation.points[1]) + let x2 = scale_shape(w, annotation.points[2]) + let y2 = scale_shape(h, annotation.points[3]) let path = `M ${x1} ${y1} L ${x2} ${y1} L ${x2} ${y2} L ${x1} ${y2} Z` svg.ele('g', { - style: `stroke:#${shapeColor};stroke-width:${shape_scale(w, annotation.thickness)};fill:${fill};stroke-linejoin:miter` + style: `stroke:#${shapeColor};stroke-width:${scale_shape(w, annotation.thickness)};fill:${fill};stroke-linejoin:miter` }).ele('path', { d: path }).up() @@ -148,32 +148,54 @@ function overlay_triangle(svg, annotation, w, h) { let shapeColor = Number(annotation.color).toString(16) let fill = annotation.fill ? `#${shapeColor}` : 'none'; - let x1 = shape_scale(w, annotation.points[0]) - let y1 = shape_scale(h, annotation.points[1]) - let x2 = shape_scale(w, annotation.points[2]) - let y2 = shape_scale(h, annotation.points[3]) + let x1 = scale_shape(w, annotation.points[0]) + let y1 = scale_shape(h, annotation.points[1]) + let x2 = scale_shape(w, annotation.points[2]) + let y2 = scale_shape(h, annotation.points[3]) let px = (x1 + x2) / 2 let path = `M${px} ${y1} L${x2} ${y2} L${x1} ${y2} Z` svg.ele('g', { - style: `stroke:#${shapeColor};stroke-width:${shape_scale(w, annotation.thickness)};fill:${fill};stroke-linejoin:miter;stroke-miterlimit:8` + style: `stroke:#${shapeColor};stroke-width:${scale_shape(w, annotation.thickness)};fill:${fill};stroke-linejoin:miter;stroke-miterlimit:8` }).ele('path', { d: path }).up() } +function overlay_text(svg, annotation, w, h) { + let fontColor = Number(annotation.fontColor).toString(16) + + let textBox_x = scale_shape(w, annotation.x); + let textBox_y = scale_shape(h, annotation.y); + + let fontSize = scale_shape(h, annotation.calcedFontSize) + let lines = annotation.text.replace(/\r\n|\n\r|\n|\r/g,'\n').split('\n'); + + let textBox = svg.ele('g', { + style: `fill:#${fontColor};font-family:Arial;font-size:${fontSize}px`, + }) + + for(let i = 0; i < lines.length; i++) { + textBox.ele('text', { + x: textBox_x, + y: textBox_y, + dy: `${i}em` + }).txt(lines[i]).up() + } +} + function overlay_line(svg, annotation, w, h) { let shapeColor = Number(annotation.color).toString(16) svg.ele('g', { - style: `stroke:#${shapeColor};stroke-width:${shape_scale(w, annotation.thickness)};stroke-linecap:butt` + style: `stroke:#${shapeColor};stroke-width:${scale_shape(w, annotation.thickness)};stroke-linecap:butt` }).ele('line', { - x1: shape_scale(w, annotation.points[0]), - y1: shape_scale(h, annotation.points[1]), - x2: shape_scale(w, annotation.points[2]), - y2: shape_scale(h, annotation.points[3]), + x1: scale_shape(w, annotation.points[0]), + y1: scale_shape(h, annotation.points[1]), + x2: scale_shape(w, annotation.points[2]), + y2: scale_shape(h, annotation.points[3]), }).up() } @@ -197,6 +219,7 @@ function overlay_annotations(svg, annotations, w, h) { overlay_rectangle(svg, annotations[i].annotationInfo, w, h); break; case 'text': + overlay_text(svg, annotations[i].annotationInfo, w, h); break; case 'triangle': overlay_triangle(svg, annotations[i].annotationInfo, w, h); @@ -239,8 +262,8 @@ for (let i = 0; i < pages.length; i++) { var panzoom_x = -currentSlide.xOffset * MAGIC_MYSTERY_NUMBER / 100.0 * slideWidth var panzoom_y = -currentSlide.yOffset * MAGIC_MYSTERY_NUMBER / 100.0 * slideHeight - var panzoom_w = shape_scale(slideWidth, currentSlide.widthRatio) - var panzoom_h = shape_scale(slideHeight, currentSlide.heightRatio) + var panzoom_w = scale_shape(slideWidth, currentSlide.widthRatio) + var panzoom_h = scale_shape(slideHeight, currentSlide.heightRatio) // Create the SVG slide with the background image let svg = create({ version: '1.0', encoding: 'UTF-8' }) From 40428903486a2ad9de6f47ff4c07615c9be91334 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Sat, 19 Feb 2022 16:43:11 +0100 Subject: [PATCH 030/268] Implement polls --- ...resentationWithAnnotationDownloadReqMsg.js | 18 +----- bigbluebutton-html5/public/locales/en.json | 2 +- export-annotations/workers/process.js | 64 +++++++++++++++++-- 3 files changed, 61 insertions(+), 23 deletions(-) diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js b/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js index c3f393a3e0..3fe44131be 100644 --- a/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js +++ b/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js @@ -8,7 +8,6 @@ export default function makePresentationWithAnnotationDownloadReqMsg() { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'MakePresentationWithAnnotationDownloadReqMsg'; - const SECOND_EVENT_NAME = 'ExportPresentationWithAnnotationReqMsg'; try { const { meetingId, requesterUserId } = extractCredentials(this.userId); @@ -18,23 +17,12 @@ export default function makePresentationWithAnnotationDownloadReqMsg() { const payload = { presId: "", - allPages: false, - pages: [2, 3, 8], + allPages: true, + pages: [], }; - const payload2 = { - parentMeetingId: "", - allPages: false, - }; - - Logger.warn('************'); - Logger.warn(CHANNEL) - Logger.warn(EVENT_NAME) - Logger.warn(meetingId) - Logger.warn(requesterUserId) - Logger.warn('************'); - return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); + } catch (err) { Logger.error(`Exception while invoking method makePresentationWithAnnotationDownloadReqMsg ${err.stack}`); } diff --git a/bigbluebutton-html5/public/locales/en.json b/bigbluebutton-html5/public/locales/en.json index f87c87fc5f..c8f188dc2d 100755 --- a/bigbluebutton-html5/public/locales/en.json +++ b/bigbluebutton-html5/public/locales/en.json @@ -169,7 +169,7 @@ "app.presentation.presentationToolbar.prevSlideDesc": "Change the presentation to the previous slide", "app.presentation.presentationToolbar.nextSlideLabel": "Next slide", "app.presentation.presentationToolbar.nextSlideDesc": "Change the presentation to the next slide", - "app.presentation.presentationToolbar.downloadSlideWithAnnotations": "Download slide with annotations", + "app.presentation.presentationToolbar.downloadSlideWithAnnotations": "Download slides with annotations", "app.presentation.presentationToolbar.skipSlideLabel": "Skip slide", "app.presentation.presentationToolbar.skipSlideDesc": "Change the presentation to a specific slide", "app.presentation.presentationToolbar.fitWidthLabel": "Fit to width", diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js index 383cccf1d1..82270d56a1 100644 --- a/export-annotations/workers/process.js +++ b/export-annotations/workers/process.js @@ -3,7 +3,7 @@ const config = require('../config'); const fs = require('fs'); const convert = require('xml-js'); const { create } = require('xmlbuilder2', { encoding: 'utf-8' }); -const { exec } = require("child_process"); +const { exec, execSync } = require("child_process"); const { workerData, parentPort } = require('worker_threads') @@ -126,6 +126,58 @@ function overlay_pencil(svg, annotation, w, h) { } } +function overlay_poll(svg, annotation, w, h) { + if (annotation.result.length == 0) { + return; + } + + let poll_x = scale_shape(w, annotation.points[0]); + let poll_y = scale_shape(h, annotation.points[1]); + let poll_width = Math.round(scale_shape(w, annotation.points[2])); + let poll_height = Math.round(scale_shape(h, annotation.points[3])); + let pollId = annotation.id.replace(/\//g, ''); + let pollSVG = `${dropbox}/poll-${pollId}.svg` + let pollJSON = `${dropbox}/poll-${pollId}.json` + + // Rename 'numVotes' key to 'num_votes' + let pollJSONContent = annotation.result.map(result => { + result.num_votes = result.numVotes; + delete result.numVotes; + return result; + }); + + // Store the poll result in a JSON file + fs.writeFileSync(pollJSON, JSON.stringify(pollJSONContent), function(err) { + if(err) { return logger.error(err); } + }); + + // Create empty SVG poll + fs.writeFileSync(pollSVG, '', function(err) { + if(err) { return logger.error(err); } + }); + + // Render the poll SVG using gen_poll_svg script + execSync(`${config.genPollSVG.path} -i ${pollJSON} -w ${poll_width} -h ${poll_height} -n ${annotation.numResponders} -o ${pollSVG}`, (error, stderr) => { + if (error) { + logger.error(`Poll generation failed with error: ${error.message}`); + return; + } + if (stderr) { + logger.error(`Poll generation failed with stderr: ${stderr}`); + return; + } + }); + + // Add poll image element + svg.ele('image', { + 'xlink:href': `file://${pollSVG}`, + x: poll_x, + y: poll_y, + width: poll_width, + height: poll_height, + }) +} + function overlay_rectangle(svg, annotation, w, h) { let shapeColor = Number(annotation.color).toString(16) let fill = annotation.fill ? `#${shapeColor}` : 'none'; @@ -135,7 +187,7 @@ function overlay_rectangle(svg, annotation, w, h) { let x2 = scale_shape(w, annotation.points[2]) let y2 = scale_shape(h, annotation.points[3]) - let path = `M ${x1} ${y1} L ${x2} ${y1} L ${x2} ${y2} L ${x1} ${y2} Z` + let path = `M${x1} ${y1} L${x2} ${y1} L${x2} ${y2} L${x1} ${y2} Z` svg.ele('g', { style: `stroke:#${shapeColor};stroke-width:${scale_shape(w, annotation.thickness)};fill:${fill};stroke-linejoin:miter` @@ -200,8 +252,6 @@ function overlay_line(svg, annotation, w, h) { } function overlay_annotations(svg, annotations, w, h) { - console.log(annotations) - for(let i = 0; i < annotations.length; i++) { switch (annotations[i].annotationType) { case 'ellipse': @@ -211,6 +261,7 @@ function overlay_annotations(svg, annotations, w, h) { overlay_line(svg, annotations[i].annotationInfo, w, h); break; case 'poll_result': + overlay_poll(svg, annotations[i].annotationInfo, w, h); break; case 'pencil': overlay_pencil(svg, annotations[i].annotationInfo, w, h); @@ -233,7 +284,7 @@ function overlay_annotations(svg, annotations, w, h) { // Process the presentation pages and annotations into a PDF file // 1. Get the job -let dropbox = `${config.shared.presAnnDropboxDir}/${jobId}` +const dropbox = `${config.shared.presAnnDropboxDir}/${jobId}` let job = fs.readFileSync(`${dropbox}/job`); let exportJob = JSON.parse(job); @@ -293,7 +344,6 @@ for (let i = 0; i < pages.length; i++) { overlay_annotations(svg, pages[i].annotations, slideWidth, slideHeight) svg = svg.end({ prettyPrint: true }); - console.log (svg) // Write annotated SVG file let file = `${dropbox}/annotated-slide${pages[i].page}.svg` @@ -308,7 +358,7 @@ for (let i = 0; i < pages.length; i++) { // TODO: change presLocation so it doesn't point to the 'svgs' directory exec(`rsvg-convert ${rsvgConvertInput} -f pdf -o ${exportJob.presLocation}/../annotated_slides_${jobId}.pdf`, (error, stderr) => { if (error) { - console.log(`SVG to PDF export failed with error: ${error.message}`); + logger.error(`SVG to PDF export failed with error: ${error.message}`); return; } if (stderr) { From 3dffffcae1c04fb870a340ba93cf72267c4b0cc6 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Sun, 20 Feb 2022 17:29:39 +0100 Subject: [PATCH 031/268] Text positioning, textbox bounds --- export-annotations/workers/process.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js index 82270d56a1..9b4dae449f 100644 --- a/export-annotations/workers/process.js +++ b/export-annotations/workers/process.js @@ -219,21 +219,25 @@ function overlay_triangle(svg, annotation, w, h) { function overlay_text(svg, annotation, w, h) { let fontColor = Number(annotation.fontColor).toString(16) + let textBoxWidth = scale_shape(w, annotation.textBoxWidth); + let textBoxHeight = scale_shape(h, annotation.textBoxHeight); let textBox_x = scale_shape(w, annotation.x); let textBox_y = scale_shape(h, annotation.y); let fontSize = scale_shape(h, annotation.calcedFontSize) let lines = annotation.text.replace(/\r\n|\n\r|\n|\r/g,'\n').split('\n'); - let textBox = svg.ele('g', { - style: `fill:#${fontColor};font-family:Arial;font-size:${fontSize}px`, - }) + let textBox = svg.ele('svg', { + x: textBox_x, + y: textBox_y, + width: textBoxWidth, + height: textBoxHeight + }); for(let i = 0; i < lines.length; i++) { textBox.ele('text', { - x: textBox_x, - y: textBox_y, - dy: `${i}em` + style: `fill:#${fontColor};font-family:Arial;font-size:${fontSize}px`, + dy: `${i + 1}em`, }).txt(lines[i]).up() } } @@ -344,7 +348,6 @@ for (let i = 0; i < pages.length; i++) { overlay_annotations(svg, pages[i].annotations, slideWidth, slideHeight) svg = svg.end({ prettyPrint: true }); - // Write annotated SVG file let file = `${dropbox}/annotated-slide${pages[i].page}.svg` fs.writeFile(file, svg, function(err) { From acdcd4d89a755a25cc5f49209afd940ba8e1bbc0 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Tue, 22 Feb 2022 17:02:54 +0100 Subject: [PATCH 032/268] Text linebreaks for long words --- .../core/running/MeetingActor.scala | 4 +- export-annotations/workers/collector.js | 2 +- export-annotations/workers/process.js | 125 ++++++++++++++++-- 3 files changed, 117 insertions(+), 14 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index 16e419ccb3..27f12e1d82 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -793,7 +793,7 @@ class MeetingActor( // 2) Insert Export Job in Redis val jobType = "PresentationWithAnnotationDownloadJob" - val presLocation = s"/var/bigbluebutton/${meetingId}/${meetingId}/${presId}/svgs" + val presLocation = s"/var/bigbluebutton/${meetingId}/${meetingId}/${presId}" val exportJob = new ExportJob(jobId, jobType, presId, presLocation, allPages, pagesRange, "", "") var job = buildStoreExportJobInRedisSysMsg(exportJob) outGW.send(job) @@ -844,7 +844,7 @@ class MeetingActor( // 2) Insert Export Job in Redis val jobType: String = "PresentationWithAnnotationExportJob" - val presLocation = s"/var/bigbluebutton/${meetingId}/${meetingId}/${presId}/svgs" + val presLocation = s"/var/bigbluebutton/${meetingId}/${meetingId}/${presId}" val exportJob = new ExportJob(jobId, jobType, presId, presLocation, allPages, pagesRange, parentMeetingId, presentationUploadToken) var job = buildStoreExportJobInRedisSysMsg(exportJob) outGW.send(job) diff --git a/export-annotations/workers/collector.js b/export-annotations/workers/collector.js index 8f77232f69..d32b89f4c5 100644 --- a/export-annotations/workers/collector.js +++ b/export-annotations/workers/collector.js @@ -53,7 +53,7 @@ let exportJob = JSON.parse(job); // Collect the Presentation Page files from the presentation directory for (let i = 0; i < pages.length; i++) { let pageNumber = pages[i].page - let slide = `${exportJob.presLocation}/slide${pageNumber}.svg` + let slide = `${exportJob.presLocation}/svgs/slide${pageNumber}.svg` let file = `${dropbox}/slide${pageNumber}.svg` fs.copyFile(slide, file, (err) => { if (err) throw err; } ); diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js index 9b4dae449f..6b030e279c 100644 --- a/export-annotations/workers/process.js +++ b/export-annotations/workers/process.js @@ -3,8 +3,7 @@ const config = require('../config'); const fs = require('fs'); const convert = require('xml-js'); const { create } = require('xmlbuilder2', { encoding: 'utf-8' }); -const { exec, execSync } = require("child_process"); - +const { execSync } = require("child_process"); const { workerData, parentPort } = require('worker_threads') const jobId = workerData; @@ -14,7 +13,32 @@ const logger = new Logger('presAnn Process Worker'); logger.info("Processing PDF for job " + jobId); function scale_shape(dimension, coord){ - return (coord / 100.0 * dimension) + return (coord / 100.0 * dimension); +} + +function measure_length(string, fontSize) { + // TODO: find faster way to measure string length + if (string.length == 0) { + return 0; + } + + var output; + output = execSync(`convert xc: -font ${config.process.font} -pointsize ${fontSize} -debug annotate -annotate 0 "${string}" null: 2>&1`, (error, stderr) => { + if (error) { + logger.error(`Error when measuring length of string ${string} with ImageMagick: ${error.message}`); + return; + } + if (stderr) { + logger.error(`stderr when measuring lenght of string "${string}" with ImageMagick ${stderr}`); + return; + } + }) + + output = String(output).split(" ") + const textWidth = (element) => element == 'width:'; + var index = output.findIndex(textWidth); + + return Number(output[index + 1].replace(/[^0-9]/g,'')); } function overlay_ellipse(svg, annotation, w, h) { @@ -234,11 +258,91 @@ function overlay_text(svg, annotation, w, h) { height: textBoxHeight }); + var yOffset = 1; // in em + var wrappedLine = []; + var wrappedLineLength = 0; + for(let i = 0; i < lines.length; i++) { - textBox.ele('text', { - style: `fill:#${fontColor};font-family:Arial;font-size:${fontSize}px`, - dy: `${i + 1}em`, - }).txt(lines[i]).up() + var lineLength = measure_length(lines[i], fontSize); + + if (lineLength < textBoxWidth) { + // Line fits in text box. Can be displayed as-is + textBox.ele('text', { + style: `fill:#${fontColor};font-family:Arial;font-size:${fontSize}px`, + dy: `${yOffset}em`, + }).txt(lines[i]).up() + + yOffset += 1; + } + + else { + // Split line into words, keeping the whitespace + words = lines[i].split(/(\s+)/); + + // Generate line breaks due to word wrapping + for(let j = 0; j < words.length; j++) { + wordLength = measure_length(words[j], fontSize); + + // If word fits in line, add it + if (wrappedLineLength + wordLength <= textBoxWidth) { + wrappedLine.push(words[j]) + wrappedLineLength += wordLength; + + } else if (wordLength > textBoxWidth) { + // If the word itself is wider than the textbox, place the characters individually + var chars = words[j].split(''); + + for(let k = 0; k < chars.length; k++){ + var charWidth = measure_length(chars[k], fontSize); + + // If the character fits, add it + if (charWidth + wrappedLineLength <= textBoxWidth) { + wrappedLine.push(chars[k]); + wrappedLineLength += charWidth; + } + + else { + // Line became too long due to the new character: add the characters that fit + var leftoverWord = chars.slice(k).join(''); + + textBox.ele('text', { + style: `fill:#${fontColor};font-family:Arial;font-size:${fontSize}px`, + dy: `${yOffset}em`, + }).txt(wrappedLine.join('')).up() + + yOffset += 1; + wrappedLine = [leftoverWord]; + wrappedLineLength = wordLength; + break; + } + } + + // Add remaining part of the word on a new line + textBox.ele('text', { + style: `fill:#${fontColor};font-family:Arial;font-size:${fontSize}px`, + dy: `${yOffset}em`, + }).txt(wrappedLine.join('')).up() + + } else { + // If the line became too long, add the words that came previously + // and add a linebreak starting with the word that didn't fit + textBox.ele('text', { + style: `fill:#${fontColor};font-family:Arial;font-size:${fontSize}px`, + dy: `${yOffset}em`, + }).txt(wrappedLine.join('')).up() + + yOffset += 1; + wrappedLine = [words[j]] + wrappedLineLength = wordLength; + } + } + + // Add remaining text elements + textBox.ele('text', { + style: `fill:#${fontColor};font-family:Arial;font-size:${fontSize}px`, + dy: `${yOffset}em`, + }).txt(wrappedLine.join('')).up() + } } } @@ -350,7 +454,7 @@ for (let i = 0; i < pages.length; i++) { svg = svg.end({ prettyPrint: true }); // Write annotated SVG file let file = `${dropbox}/annotated-slide${pages[i].page}.svg` - fs.writeFile(file, svg, function(err) { + fs.writeFileSync(file, svg, function(err) { if(err) { return logger.error(err); } }); @@ -358,8 +462,7 @@ for (let i = 0; i < pages.length; i++) { } // Resulting PDF file is stored in the presentation dir -// TODO: change presLocation so it doesn't point to the 'svgs' directory -exec(`rsvg-convert ${rsvgConvertInput} -f pdf -o ${exportJob.presLocation}/../annotated_slides_${jobId}.pdf`, (error, stderr) => { +execSync(`rsvg-convert ${rsvgConvertInput} -f pdf -o ${exportJob.presLocation}/annotated_slides_${jobId}.pdf`, (error, stderr) => { if (error) { logger.error(`SVG to PDF export failed with error: ${error.message}`); return; @@ -371,6 +474,6 @@ exec(`rsvg-convert ${rsvgConvertInput} -f pdf -o ${exportJob.presLocation}/../an }); // Launch Notifier Worker depending on job type -logger.info(`Saved PDF at ${exportJob.presLocation}/../annotated_slides_${jobId}.pdf`) +logger.info(`Saved PDF at ${exportJob.presLocation}/annotated_slides_${jobId}.pdf`) parentPort.postMessage({ message: workerData }) From 7af415206747c37457c743f1a50ca59eae0ba10e Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Wed, 23 Feb 2022 18:20:58 +0100 Subject: [PATCH 033/268] Rasterize text as HTML with wkhtmltoimage --- export-annotations/workers/process.js | 197 ++++++++++---------------- 1 file changed, 76 insertions(+), 121 deletions(-) diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js index 6b030e279c..356203080c 100644 --- a/export-annotations/workers/process.js +++ b/export-annotations/workers/process.js @@ -4,7 +4,7 @@ const fs = require('fs'); const convert = require('xml-js'); const { create } = require('xmlbuilder2', { encoding: 'utf-8' }); const { execSync } = require("child_process"); -const { workerData, parentPort } = require('worker_threads') +const { workerData, parentPort } = require('worker_threads'); const jobId = workerData; const MAGIC_MYSTERY_NUMBER = 2; @@ -12,37 +12,54 @@ const MAGIC_MYSTERY_NUMBER = 2; const logger = new Logger('presAnn Process Worker'); logger.info("Processing PDF for job " + jobId); -function scale_shape(dimension, coord){ +const kickOffNotifierWorker = (jobType) => { + return new Promise((resolve, reject) => { + const worker = new Worker('./workers/notifier.js', { workerData: jobType }); + 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}`)); + }) + }) +} + +function color_to_hex(color) { + color = parseInt(color).toString(16) + return '0'.repeat(6 - color.length) + color +} + +function scale_shape(dimension, coord) { return (coord / 100.0 * dimension); } -function measure_length(string, fontSize) { - // TODO: find faster way to measure string length - if (string.length == 0) { - return 0; - } +function render_HTMLTextBox(htmlFilePath, id, width, height) { + commands = [ + 'wkhtmltoimage', + '--format', 'png', + '--encoding', `${config.process.whiteboardTextEncoding}`, + '--transparent', + '--crop-w', width, + '--crop-h', height, + '--log-level', 'none', + htmlFilePath, `${dropbox}/text${id}.png` + ] - var output; - output = execSync(`convert xc: -font ${config.process.font} -pointsize ${fontSize} -debug annotate -annotate 0 "${string}" null: 2>&1`, (error, stderr) => { + execSync(commands.join(' '), (error, stderr) => { if (error) { - logger.error(`Error when measuring length of string ${string} with ImageMagick: ${error.message}`); + logger.error(`Error when rendering text box for string "${string}" with wkhtmltoimage: ${error.message}`); return; } + if (stderr) { - logger.error(`stderr when measuring lenght of string "${string}" with ImageMagick ${stderr}`); + logger.error(`stderr when rendering text box for string "${string}" with wkhtmltoimage: ${stderr}`); return; } }) - - output = String(output).split(" ") - const textWidth = (element) => element == 'width:'; - var index = output.findIndex(textWidth); - - return Number(output[index + 1].replace(/[^0-9]/g,'')); } function overlay_ellipse(svg, annotation, w, h) { - let shapeColor = Number(annotation.color).toString(16) + let shapeColor = color_to_hex(annotation.color); let fill = annotation.fill ? `#${shapeColor}` : 'none'; let x1 = scale_shape(w, annotation.points[0]) @@ -80,7 +97,7 @@ function overlay_ellipse(svg, annotation, w, h) { } function overlay_line(svg, annotation, w, h) { - let shapeColor = Number(annotation.color).toString(16) + let shapeColor = color_to_hex(annotation.color); svg.ele('g', { style: `stroke:#${shapeColor};stroke-width:${scale_shape(w, annotation.thickness)};stroke-linecap:butt` @@ -93,7 +110,7 @@ function overlay_line(svg, annotation, w, h) { } function overlay_pencil(svg, annotation, w, h) { - let shapeColor = Number(annotation.color).toString(16) + let shapeColor = color_to_hex(annotation.color); if (annotation.points.length < 2) { logger.info("Pencil doesn't have enough points") @@ -203,7 +220,7 @@ function overlay_poll(svg, annotation, w, h) { } function overlay_rectangle(svg, annotation, w, h) { - let shapeColor = Number(annotation.color).toString(16) + let shapeColor = color_to_hex(annotation.color); let fill = annotation.fill ? `#${shapeColor}` : 'none'; let x1 = scale_shape(w, annotation.points[0]) @@ -221,7 +238,7 @@ function overlay_rectangle(svg, annotation, w, h) { } function overlay_triangle(svg, annotation, w, h) { - let shapeColor = Number(annotation.color).toString(16) + let shapeColor = color_to_hex(annotation.color); let fill = annotation.fill ? `#${shapeColor}` : 'none'; let x1 = scale_shape(w, annotation.points[0]) @@ -241,113 +258,50 @@ function overlay_triangle(svg, annotation, w, h) { } function overlay_text(svg, annotation, w, h) { - let fontColor = Number(annotation.fontColor).toString(16) - let textBoxWidth = scale_shape(w, annotation.textBoxWidth); - let textBoxHeight = scale_shape(h, annotation.textBoxHeight); - let textBox_x = scale_shape(w, annotation.x); - let textBox_y = scale_shape(h, annotation.y); + let fontColor = color_to_hex(annotation.fontColor); + let textBoxWidth = Math.ceil(scale_shape(w, annotation.textBoxWidth)); + let textBoxHeight = Math.ceil(scale_shape(h, annotation.textBoxHeight)); + let textBox_x = Math.ceil(scale_shape(w, annotation.x)); + let textBox_y = Math.ceil(scale_shape(h, annotation.y)); let fontSize = scale_shape(h, annotation.calcedFontSize) - let lines = annotation.text.replace(/\r\n|\n\r|\n|\r/g,'\n').split('\n'); + let style = [ + `width:${textBoxWidth}px;`, + `height:${textBoxHeight}px;`, + `color:#${fontColor};`, + "word-wrap:break-word;", + "font-family:Arial;", + `font-size:${fontSize}px` + ] - let textBox = svg.ele('svg', { + var html = + ` + +

+ ${annotation.text.split('\n').join('
')} +

+ `; + + var htmlFilePath = `${dropbox}/text${annotation.id}.html` + + fs.writeFileSync(htmlFilePath, html, function (err) { + if (err) logger.error(err) + }) + + render_HTMLTextBox(htmlFilePath, annotation.id, textBoxWidth, textBoxHeight) + + svg.ele('image', { + 'xlink:href': `file://${dropbox}/text${annotation.id}.png`, x: textBox_x, y: textBox_y, width: textBoxWidth, - height: textBoxHeight - }); - - var yOffset = 1; // in em - var wrappedLine = []; - var wrappedLineLength = 0; - - for(let i = 0; i < lines.length; i++) { - var lineLength = measure_length(lines[i], fontSize); - - if (lineLength < textBoxWidth) { - // Line fits in text box. Can be displayed as-is - textBox.ele('text', { - style: `fill:#${fontColor};font-family:Arial;font-size:${fontSize}px`, - dy: `${yOffset}em`, - }).txt(lines[i]).up() - - yOffset += 1; - } - - else { - // Split line into words, keeping the whitespace - words = lines[i].split(/(\s+)/); - - // Generate line breaks due to word wrapping - for(let j = 0; j < words.length; j++) { - wordLength = measure_length(words[j], fontSize); - - // If word fits in line, add it - if (wrappedLineLength + wordLength <= textBoxWidth) { - wrappedLine.push(words[j]) - wrappedLineLength += wordLength; - - } else if (wordLength > textBoxWidth) { - // If the word itself is wider than the textbox, place the characters individually - var chars = words[j].split(''); - - for(let k = 0; k < chars.length; k++){ - var charWidth = measure_length(chars[k], fontSize); - - // If the character fits, add it - if (charWidth + wrappedLineLength <= textBoxWidth) { - wrappedLine.push(chars[k]); - wrappedLineLength += charWidth; - } - - else { - // Line became too long due to the new character: add the characters that fit - var leftoverWord = chars.slice(k).join(''); - - textBox.ele('text', { - style: `fill:#${fontColor};font-family:Arial;font-size:${fontSize}px`, - dy: `${yOffset}em`, - }).txt(wrappedLine.join('')).up() - - yOffset += 1; - wrappedLine = [leftoverWord]; - wrappedLineLength = wordLength; - break; - } - } - - // Add remaining part of the word on a new line - textBox.ele('text', { - style: `fill:#${fontColor};font-family:Arial;font-size:${fontSize}px`, - dy: `${yOffset}em`, - }).txt(wrappedLine.join('')).up() - - } else { - // If the line became too long, add the words that came previously - // and add a linebreak starting with the word that didn't fit - textBox.ele('text', { - style: `fill:#${fontColor};font-family:Arial;font-size:${fontSize}px`, - dy: `${yOffset}em`, - }).txt(wrappedLine.join('')).up() - - yOffset += 1; - wrappedLine = [words[j]] - wrappedLineLength = wordLength; - } - } - - // Add remaining text elements - textBox.ele('text', { - style: `fill:#${fontColor};font-family:Arial;font-size:${fontSize}px`, - dy: `${yOffset}em`, - }).txt(wrappedLine.join('')).up() - } - } + height: textBoxHeight, + }).up(); } function overlay_line(svg, annotation, w, h) { - let shapeColor = Number(annotation.color).toString(16) + let shapeColor = color_to_hex(annotation.color); svg.ele('g', { style: `stroke:#${shapeColor};stroke-width:${scale_shape(w, annotation.thickness)};stroke-linecap:butt` @@ -408,7 +362,6 @@ for (let i = 0; i < pages.length; i++) { // Get the current slide (without annotations) let currentSlide = pages[i] var backgroundSlide = fs.readFileSync(`${dropbox}/slide${pages[i].page}.svg`).toString(); - // Read background slide in as JSON to determine dimensions // TODO: find a better way to get width and height of slide (e.g. as part of message) backgroundSlide = JSON.parse(convert.xml2json(backgroundSlide)); @@ -474,6 +427,8 @@ execSync(`rsvg-convert ${rsvgConvertInput} -f pdf -o ${exportJob.presLocation}/a }); // Launch Notifier Worker depending on job type -logger.info(`Saved PDF at ${exportJob.presLocation}/annotated_slides_${jobId}.pdf`) +logger.info(`Saved PDF at ${exportJob.presLocation}/annotated_slides_${jobId}.pdf`); + +// kickOffNotifierWorker(exportJob.jobType); parentPort.postMessage({ message: workerData }) From ac83d56cf702b0a8c50ef0e2cfbd60412df1e8f6 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Sat, 26 Feb 2022 17:52:05 +0100 Subject: [PATCH 034/268] Notifier worker: automatic PDF upload for breakout rooms --- export-annotations/config/settings.json | 17 +- export-annotations/package-lock.json | 3621 +---------------------- export-annotations/package.json | 12 +- export-annotations/workers/notifier.js | 43 + export-annotations/workers/process.js | 14 +- 5 files changed, 85 insertions(+), 3622 deletions(-) create mode 100644 export-annotations/workers/notifier.js diff --git a/export-annotations/config/settings.json b/export-annotations/config/settings.json index 12b09c8ad3..1ef571947f 100644 --- a/export-annotations/config/settings.json +++ b/export-annotations/config/settings.json @@ -6,13 +6,28 @@ "presDir": "/var/bigbluebutton", "presAnnDropboxDir": "/tmp/pres-ann-dropbox" }, + "process": { + "whiteboardTextEncoding": "utf-8" + }, + "notifier": { + "pod_id": "DEFAULT_PRESENTATION_POD", + "is_downloadable": "false" + }, + "genPollSVG": { + "path": "/home/petriroc/dev/bigbluebutton/record-and-playback/core/scripts/utils/gen_poll_svg" + }, + "bbbWeb": { + "host": "127.0.0.1", + "port": 8090 + }, "redis": { "host": "127.0.0.1", "port": 6379, "password": null, "interval": 1000, "channels": { - "queue": "exportJobs" + "queue": "exportJobs", + "publish": "to-akka-apps-redis-channel" } } } diff --git a/export-annotations/package-lock.json b/export-annotations/package-lock.json index d41c465cfc..9e5edb1e1e 100644 --- a/export-annotations/package-lock.json +++ b/export-annotations/package-lock.json @@ -4,834 +4,6 @@ "lockfileVersion": 1, "requires": true, "dependencies": { - "@ampproject/remapping": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.1.0.tgz", - "integrity": "sha512-d5RysTlJ7hmw5Tw4UxgxcY3lkMe92n8sXCcuLPAyIAHK6j8DefDwtGnVVDgOnv+RnEosulDJ9NPKQL27bDId0g==", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "^0.3.0" - } - }, - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/compat-data": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.0.tgz", - "integrity": "sha512-392byTlpGWXMv4FbyWw3sAZ/FrW/DrwqLGXpy0mbyNe9Taqv1mg9yON5/o0cnr8XYCkFTZbC1eV+c+LAROgrng==", - "dev": true - }, - "@babel/core": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.0.tgz", - "integrity": "sha512-x/5Ea+RO5MvF9ize5DeVICJoVrNv0Mi2RnIABrZEKYvPEpldXwauPkgvYA17cKa6WpU3LoYvYbuEMFtSNFsarA==", - "dev": true, - "requires": { - "@ampproject/remapping": "^2.0.0", - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.0", - "@babel/helper-compilation-targets": "^7.16.7", - "@babel/helper-module-transforms": "^7.16.7", - "@babel/helpers": "^7.17.0", - "@babel/parser": "^7.17.0", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.0", - "@babel/types": "^7.17.0", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.1.2", - "semver": "^6.3.0" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@babel/generator": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.0.tgz", - "integrity": "sha512-I3Omiv6FGOC29dtlZhkfXO6pgkmukJSlT26QjVvS1DGZe/NzSVCPG41X0tS21oZkJYlovfj9qDWgKP+Cn4bXxw==", - "dev": true, - "requires": { - "@babel/types": "^7.17.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "@babel/helper-compilation-targets": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz", - "integrity": "sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.16.4", - "@babel/helper-validator-option": "^7.16.7", - "browserslist": "^4.17.5", - "semver": "^6.3.0" - } - }, - "@babel/helper-environment-visitor": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz", - "integrity": "sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-function-name": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz", - "integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz", - "integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-module-transforms": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz", - "integrity": "sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz", - "integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==", - "dev": true - }, - "@babel/helper-simple-access": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz", - "integrity": "sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "requires": { - "@babel/types": "^7.16.7" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true - }, - "@babel/helpers": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.0.tgz", - "integrity": "sha512-Xe/9NFxjPwELUvW2dsukcMZIp6XwPSbI4ojFBJuX5ramHuVE22SVcZIwqzdWo5uCgeTXW8qV97lMvSOjq+1+nQ==", - "dev": true, - "requires": { - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.0", - "@babel/types": "^7.17.0" - } - }, - "@babel/highlight": { - "version": "7.16.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz", - "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/parser": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.0.tgz", - "integrity": "sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw==", - "dev": true - }, - "@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-typescript": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.16.7.tgz", - "integrity": "sha512-YhUIJHHGkqPgEcMYkPCKTyGUdoGKWtopIycQyjJH8OjvRgOYsXsaKehLVPScKJWAULPxMa4N1vCe6szREFlZ7A==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.16.7" - } - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - } - }, - "@babel/traverse": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.0.tgz", - "integrity": "sha512-fpFIXvqD6kC7c7PUNnZ0Z8cQXlarCLtCUpt2S1Dx7PjoRtCFffvOkHHSom+m5HIxMZn5bIBVb71lhabcmjEsqg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.0", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.0", - "@babel/types": "^7.17.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true - }, - "@eslint/eslintrc": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.5.tgz", - "integrity": "sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ==", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.2.0", - "globals": "^13.9.0", - "ignore": "^4.0.6", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.0.4", - "strip-json-comments": "^3.1.1" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@humanwhocodes/config-array": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.3.tgz", - "integrity": "sha512-3xSMlXHh03hCcCmFc0rbKp3Ivt2PFEJnQUJDDMTJQ2wkECZWdq4GePs2ctc5H8zV+cHPaq8k2vU8mrQjA6iHdQ==", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.4" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true - }, - "@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "dependencies": { - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } - } - }, - "@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true - }, - "@jest/console": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz", - "integrity": "sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^27.5.1", - "jest-util": "^27.5.1", - "slash": "^3.0.0" - } - }, - "@jest/core": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-27.5.1.tgz", - "integrity": "sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ==", - "dev": true, - "requires": { - "@jest/console": "^27.5.1", - "@jest/reporters": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.8.1", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^27.5.1", - "jest-config": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-resolve-dependencies": "^27.5.1", - "jest-runner": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "jest-watcher": "^27.5.1", - "micromatch": "^4.0.4", - "rimraf": "^3.0.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "@jest/environment": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz", - "integrity": "sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==", - "dev": true, - "requires": { - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1" - } - }, - "@jest/fake-timers": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", - "integrity": "sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "@sinonjs/fake-timers": "^8.0.1", - "@types/node": "*", - "jest-message-util": "^27.5.1", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1" - } - }, - "@jest/globals": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-27.5.1.tgz", - "integrity": "sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==", - "dev": true, - "requires": { - "@jest/environment": "^27.5.1", - "@jest/types": "^27.5.1", - "expect": "^27.5.1" - } - }, - "@jest/reporters": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-27.5.1.tgz", - "integrity": "sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw==", - "dev": true, - "requires": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.2", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-haste-map": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "slash": "^3.0.0", - "source-map": "^0.6.0", - "string-length": "^4.0.1", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^8.1.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "@jest/source-map": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz", - "integrity": "sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==", - "dev": true, - "requires": { - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "@jest/test-result": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-27.5.1.tgz", - "integrity": "sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==", - "dev": true, - "requires": { - "@jest/console": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - } - }, - "@jest/test-sequencer": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz", - "integrity": "sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ==", - "dev": true, - "requires": { - "@jest/test-result": "^27.5.1", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-runtime": "^27.5.1" - } - }, - "@jest/transform": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz", - "integrity": "sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/types": "^27.5.1", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-util": "^27.5.1", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.4.tgz", - "integrity": "sha512-cz8HFjOFfUBtvN+NXYSFMHYRdxZMaEl0XypVrhzxBgadKIXhIkRd8aMeHhmF56Sl7SuS8OnUpQ73/k9LE4VnLg==", - "dev": true - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.10.tgz", - "integrity": "sha512-Ht8wIW5v165atIX1p+JvKR5ONzUyF4Ac8DZIQ5kZs9zrb6M8SJNXpx1zn04rn65VjBMygRoMXcyYwNK0fT7bEg==", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.2.tgz", - "integrity": "sha512-9KzzH4kMjA2XmBRHfqG2/Vtl7s92l6uNDd0wW7frDE+EUvQFGqNXhWp0UGJjSkt3v2AYjzOZn1QO9XaTNJIt1Q==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, "@node-redis/bloom": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@node-redis/bloom/-/bloom-1.0.1.tgz", @@ -900,2306 +72,80 @@ "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.8.tgz", "integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==" }, - "@sinonjs/commons": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", - "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - }, - "@sinonjs/fake-timers": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", - "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" - } - }, - "@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "dev": true - }, - "@types/babel__core": { - "version": "7.1.18", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.18.tgz", - "integrity": "sha512-S7unDjm/C7z2A2R9NzfKCK1I+BAALDtxEmsJBwlB3EzNfb929ykjL++1CK9LO++EIp2fQrC8O+BwjKvz6UeDyQ==", - "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "@types/babel__generator": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", - "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@types/babel__template": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", - "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", - "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@types/babel__traverse": { - "version": "7.14.2", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.14.2.tgz", - "integrity": "sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA==", - "dev": true, - "requires": { - "@babel/types": "^7.3.0" - } - }, - "@types/graceful-fs": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", - "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", - "dev": true - }, - "@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*" - } - }, - "@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", - "dev": true, - "requires": { - "@types/istanbul-lib-report": "*" - } - }, "@types/node": { "version": "17.0.16", "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.16.tgz", "integrity": "sha512-ydLaGVfQOQ6hI1xK2A5nVh8bl0OGoIfYMxPWHqqYe9bTkWCfqiVvZoh2I/QF2sNSkZzZyROBoTefIEI+PB6iIA==" }, - "@types/prettier": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.4.3.tgz", - "integrity": "sha512-QzSuZMBuG5u8HqYz01qtMdg/Jfctlnvj1z/lYnIDXs/golxw0fxtRAHd9KrzjR7Yxz1qVeI00o0kiO3PmVdJ9w==", - "dev": true - }, - "@types/stack-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", - "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", - "dev": true - }, - "@types/yargs": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", - "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "@types/yargs-parser": { - "version": "20.2.1", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.1.tgz", - "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==", - "dev": true - }, - "abab": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", - "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true - }, - "acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "dev": true - }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "requires": { - "debug": "4" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "requires": { - "type-fest": "^0.21.3" - }, - "dependencies": { - "type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true - } - } - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, - "babel-jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz", - "integrity": "sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==", - "dev": true, + "axios": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz", + "integrity": "sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==", "requires": { - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^27.5.1", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" + "follow-redirects": "^1.14.8" } }, - "babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - } - }, - "babel-plugin-jest-hoist": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz", - "integrity": "sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==", - "dev": true, - "requires": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.0.0", - "@types/babel__traverse": "^7.0.6" - } - }, - "babel-preset-current-node-syntax": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", - "dev": true, - "requires": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" - } - }, - "babel-preset-jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz", - "integrity": "sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==", - "dev": true, - "requires": { - "babel-plugin-jest-hoist": "^27.5.1", - "babel-preset-current-node-syntax": "^1.0.0" - } - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", - "dev": true - }, - "browserslist": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.1.tgz", - "integrity": "sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001286", - "electron-to-chromium": "^1.4.17", - "escalade": "^3.1.1", - "node-releases": "^2.0.1", - "picocolors": "^1.0.0" - } - }, - "bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "requires": { - "node-int64": "^0.4.0" - } - }, - "buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "caniuse-lite": { - "version": "1.0.30001309", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001309.tgz", - "integrity": "sha512-Pl8vfigmBXXq+/yUz1jUwULeq9xhMJznzdc/xwl4WclDAuebcTHVefpz8lE/bMI+UN7TOkSSe7B7RnZd6+dzjA==", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true - }, - "ci-info": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz", - "integrity": "sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw==", - "dev": true - }, - "cjs-module-lexer": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", - "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", - "dev": true - }, "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==" }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "collect-v8-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", - "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", - "dev": true - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "convert-source-map": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", - "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", - "dev": true - }, - "cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "dev": true, - "requires": { - "cssom": "~0.3.6" - }, - "dependencies": { - "cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - } - } - }, - "data-urls": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", - "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", - "dev": true, - "requires": { - "abab": "^2.0.3", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.0.0" - } - }, - "decimal.js": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", - "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==", - "dev": true - }, - "dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", - "dev": true - }, - "deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true - }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true - }, - "diff-sequences": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", - "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", - "dev": true - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "domexception": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", - "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", - "dev": true, - "requires": { - "webidl-conversions": "^5.0.0" - }, - "dependencies": { - "webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", - "dev": true - } - } - }, - "electron-to-chromium": { - "version": "1.4.66", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.66.tgz", - "integrity": "sha512-f1RXFMsvwufWLwYUxTiP7HmjprKXrqEWHiQkjAYa9DJeVIlZk5v8gBGcaV+FhtXLly6C1OTVzQY+2UQrACiLlg==", - "dev": true - }, - "emittery": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", - "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "escodegen": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", - "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", - "dev": true, - "requires": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - } - } - }, - "eslint": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.8.0.tgz", - "integrity": "sha512-H3KXAzQGBH1plhYS3okDix2ZthuYJlQQEGE5k0IKuEqUSiyu4AmxxlJ2MtTYeJ3xB4jDhcYCwGOg2TXYdnDXlQ==", - "dev": true, - "requires": { - "@eslint/eslintrc": "^1.0.5", - "@humanwhocodes/config-array": "^0.9.2", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.0", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.2.0", - "espree": "^9.3.0", - "esquery": "^1.4.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^6.0.1", - "globals": "^13.6.0", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "regexpp": "^3.2.0", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "eslint-scope": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz", - "integrity": "sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^2.0.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true - } - } - }, - "eslint-visitor-keys": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz", - "integrity": "sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ==", - "dev": true - }, - "espree": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.0.tgz", - "integrity": "sha512-d/5nCsb0JcqsSEeQzFZ8DH1RmxPcglRWh24EFTlUEmCKoehXGdpsx0RkHDubqUI8LSAIKMQp4r9SzQ3n+sm4HQ==", - "dev": true, - "requires": { - "acorn": "^8.7.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^3.1.0" - }, - "dependencies": { - "acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", - "dev": true - } - } + "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==" }, - "esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - } - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true - }, - "expect": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", - "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1" - } - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "fb-watchman": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", - "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", - "dev": true, - "requires": { - "bser": "2.1.1" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", - "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", - "dev": true + "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": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dev": true, + "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" } }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, "generic-pool": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.8.2.tgz", "integrity": "sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg==" }, - "gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true - }, - "get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true - }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.12.1", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz", - "integrity": "sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw==", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "html-encoding-sniffer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", - "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", - "dev": true, - "requires": { - "whatwg-encoding": "^1.0.5" - } - }, - "html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "dev": true, - "requires": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "dev": true, - "requires": { - "agent-base": "6", - "debug": "4" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true - }, - "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", - "dev": true, - "requires": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true - }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.1.0.tgz", - "integrity": "sha512-czwUz525rkOFDJxfKK6mYfIs9zBKILyrZQxjz3ABhjQXhbhFsSbo1HW/BFcsDnfJYJWA6thRR5/TUY2qs5W99Q==", - "dev": true, - "requires": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - } - }, - "istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" - } - }, - "istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "dependencies": { - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "istanbul-reports": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz", - "integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==", - "dev": true, - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - } - }, - "jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", - "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", - "dev": true, - "requires": { - "@jest/core": "^27.5.1", - "import-local": "^3.0.2", - "jest-cli": "^27.5.1" - }, - "dependencies": { - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "jest-cli": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.5.1.tgz", - "integrity": "sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==", - "dev": true, - "requires": { - "@jest/core": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "import-local": "^3.0.2", - "jest-config": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "prompts": "^2.0.1", - "yargs": "^16.2.0" - } - }, - "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - } - } - }, - "jest-changed-files": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz", - "integrity": "sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "execa": "^5.0.0", - "throat": "^6.0.1" - } - }, - "jest-circus": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.5.1.tgz", - "integrity": "sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw==", - "dev": true, - "requires": { - "@jest/environment": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^0.7.0", - "expect": "^27.5.1", - "is-generator-fn": "^2.0.0", - "jest-each": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3", - "throat": "^6.0.1" - } - }, - "jest-config": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.5.1.tgz", - "integrity": "sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==", - "dev": true, - "requires": { - "@babel/core": "^7.8.0", - "@jest/test-sequencer": "^27.5.1", - "@jest/types": "^27.5.1", - "babel-jest": "^27.5.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.1", - "graceful-fs": "^4.2.9", - "jest-circus": "^27.5.1", - "jest-environment-jsdom": "^27.5.1", - "jest-environment-node": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-jasmine2": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-runner": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - } - }, - "jest-diff": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", - "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - } - }, - "jest-docblock": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.5.1.tgz", - "integrity": "sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==", - "dev": true, - "requires": { - "detect-newline": "^3.0.0" - } - }, - "jest-each": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.5.1.tgz", - "integrity": "sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "jest-get-type": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1" - } - }, - "jest-environment-jsdom": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz", - "integrity": "sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==", - "dev": true, - "requires": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1", - "jsdom": "^16.6.0" - } - }, - "jest-environment-node": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.5.1.tgz", - "integrity": "sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw==", - "dev": true, - "requires": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1" - } - }, - "jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "dev": true - }, - "jest-haste-map": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz", - "integrity": "sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "@types/graceful-fs": "^4.1.2", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.3.2", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^27.5.1", - "jest-serializer": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - } - }, - "jest-jasmine2": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz", - "integrity": "sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==", - "dev": true, - "requires": { - "@jest/environment": "^27.5.1", - "@jest/source-map": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "expect": "^27.5.1", - "is-generator-fn": "^2.0.0", - "jest-each": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1", - "throat": "^6.0.1" - } - }, - "jest-leak-detector": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz", - "integrity": "sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ==", - "dev": true, - "requires": { - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - } - }, - "jest-matcher-utils": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", - "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - } - }, - "jest-message-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", - "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^27.5.1", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - } - }, - "jest-mock": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", - "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "@types/node": "*" - } - }, - "jest-pnp-resolver": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", - "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true - }, - "jest-regex-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz", - "integrity": "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==", - "dev": true - }, - "jest-resolve": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz", - "integrity": "sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "resolve": "^1.20.0", - "resolve.exports": "^1.1.0", - "slash": "^3.0.0" - } - }, - "jest-resolve-dependencies": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz", - "integrity": "sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-snapshot": "^27.5.1" - } - }, - "jest-runner": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-27.5.1.tgz", - "integrity": "sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==", - "dev": true, - "requires": { - "@jest/console": "^27.5.1", - "@jest/environment": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.8.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^27.5.1", - "jest-environment-jsdom": "^27.5.1", - "jest-environment-node": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-leak-detector": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "source-map-support": "^0.5.6", - "throat": "^6.0.1" - } - }, - "jest-runtime": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.5.1.tgz", - "integrity": "sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==", - "dev": true, - "requires": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/globals": "^27.5.1", - "@jest/source-map": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "execa": "^5.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-mock": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - } - }, - "jest-serializer": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz", - "integrity": "sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==", - "dev": true, - "requires": { - "@types/node": "*", - "graceful-fs": "^4.2.9" - } - }, - "jest-snapshot": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz", - "integrity": "sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==", - "dev": true, - "requires": { - "@babel/core": "^7.7.2", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.0.0", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/babel__traverse": "^7.0.4", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^27.5.1", - "graceful-fs": "^4.2.9", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-util": "^27.5.1", - "natural-compare": "^1.4.0", - "pretty-format": "^27.5.1", - "semver": "^7.3.2" - }, - "dependencies": { - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, - "jest-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", - "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - } - }, - "jest-validate": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-27.5.1.tgz", - "integrity": "sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==", - "dev": true, - "requires": { - "@jest/types": "^27.5.1", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^27.5.1", - "leven": "^3.1.0", - "pretty-format": "^27.5.1" - }, - "dependencies": { - "camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true - } - } - }, - "jest-watcher": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.5.1.tgz", - "integrity": "sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==", - "dev": true, - "requires": { - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "jest-util": "^27.5.1", - "string-length": "^4.0.1" - } - }, - "jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "requires": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "dependencies": { - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "jsdom": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", - "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", - "dev": true, - "requires": { - "abab": "^2.0.5", - "acorn": "^8.2.4", - "acorn-globals": "^6.0.0", - "cssom": "^0.4.4", - "cssstyle": "^2.3.0", - "data-urls": "^2.0.0", - "decimal.js": "^10.2.1", - "domexception": "^2.0.1", - "escodegen": "^2.0.0", - "form-data": "^3.0.0", - "html-encoding-sniffer": "^2.0.1", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.0", - "parse5": "6.0.1", - "saxes": "^5.0.1", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.0.0", - "w3c-hr-time": "^1.0.2", - "w3c-xmlserializer": "^2.0.0", - "webidl-conversions": "^6.1.0", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.5.0", - "ws": "^7.4.6", - "xml-name-validator": "^3.0.0" - }, - "dependencies": { - "acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", - "dev": true - }, - "acorn-globals": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", - "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", - "dev": true, - "requires": { - "acorn": "^7.1.1", - "acorn-walk": "^7.1.1" - }, - "dependencies": { - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true - } - } - } - } - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true - }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true - }, - "leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true - }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "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==", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - } - }, - "makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "requires": { - "tmpl": "1.0.5" - } - }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, "mime-db": { "version": "1.51.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", - "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", - "dev": true + "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==", - "dev": true, "requires": { "mime-db": "1.51.0" } }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", - "dev": true - }, - "node-releases": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz", - "integrity": "sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==", - "dev": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - }, - "nwsapi": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", - "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "dev": true - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "pirates": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", - "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", - "dev": true - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - } - }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true - }, - "pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } - } - }, - "prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "requires": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - } - }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", - "dev": true - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true - }, "redis": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/redis/-/redis-4.0.3.tgz", @@ -3226,515 +172,16 @@ "redis-errors": "^1.0.0" } }, - "regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "resolve": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", - "dev": true, - "requires": { - "is-core-module": "^2.8.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "requires": { - "resolve-from": "^5.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "resolve.exports": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz", - "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, - "saxes": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", - "dev": true, - "requires": { - "xmlchars": "^2.2.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, - "stack-utils": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz", - "integrity": "sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==", - "dev": true, - "requires": { - "escape-string-regexp": "^2.0.0" - }, - "dependencies": { - "escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true - } - } - }, - "string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "requires": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - } - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true - }, - "strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "supports-hyperlinks": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", - "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", - "dev": true, - "requires": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true - }, - "symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true - }, - "terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - } - }, - "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "throat": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz", - "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==", - "dev": true - }, - "tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", - "dev": true, - "requires": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.1.2" - } - }, - "tr46": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", - "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", - "dev": true, - "requires": { - "punycode": "^2.1.1" - } - }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true - }, - "typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "requires": { - "is-typedarray": "^1.0.0" - } - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, - "v8-to-istanbul": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", - "integrity": "sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0", - "source-map": "^0.7.3" - }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - } - } - }, - "w3c-hr-time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", - "dev": true, - "requires": { - "browser-process-hrtime": "^1.0.0" - } - }, - "w3c-xmlserializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", - "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", - "dev": true, - "requires": { - "xml-name-validator": "^3.0.0" - } - }, - "walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "requires": { - "makeerror": "1.0.12" - } - }, - "webidl-conversions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", - "dev": true - }, - "whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", - "dev": true, - "requires": { - "iconv-lite": "0.4.24" - }, - "dependencies": { - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - } - } - }, - "whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", - "dev": true - }, - "whatwg-url": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", - "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", - "dev": true, - "requires": { - "lodash": "^4.7.0", - "tr46": "^2.1.0", - "webidl-conversions": "^6.1.0" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "ws": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz", - "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==", - "dev": true - }, "xml-js": { "version": "1.6.11", "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", @@ -3743,26 +190,6 @@ "sax": "^1.2.4" } }, - "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", - "dev": true - }, - "xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - } - }, - "xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" - }, "xmlbuilder2": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-3.0.2.tgz", @@ -3794,28 +221,10 @@ } } }, - "xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true - }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true - }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true } } } diff --git a/export-annotations/package.json b/export-annotations/package.json index edf41e28c9..2f06ffde89 100644 --- a/export-annotations/package.json +++ b/export-annotations/package.json @@ -1,19 +1,15 @@ { "name": "export-annotations", "version": "0.0.1", - "description": "BigBlueButton's Annotation Exporter", + "description": "BigBlueButton's Presentation Annotation Exporter", "scripts": { - "start": "node master.js", - "lint": "./node_modules/.bin/eslint lib/**", - "test": "jest" + "start": "node master.js" }, "dependencies": { + "axios": "^0.26.0", + "form-data": "^4.0.0", "redis": "^4.0.3", "xml-js": "^1.6.11", "xmlbuilder2": "^3.0.2" - }, - "devDependencies": { - "eslint": "^8.7.0", - "jest": "^27.4.7" } } diff --git a/export-annotations/workers/notifier.js b/export-annotations/workers/notifier.js new file mode 100644 index 0000000000..eed0b9ed1e --- /dev/null +++ b/export-annotations/workers/notifier.js @@ -0,0 +1,43 @@ +const Logger = require('../lib/utils/logger'); +const config = require('../config'); +const fs = require('fs'); +const FormData = require('form-data'); +const axios = require('axios').default; + +const { workerData, parentPort } = require('worker_threads') + +const [jobType, jobId] = workerData; + +const logger = new Logger('presAnn Notifier Worker'); +logger.info("Processing PDF for job " + jobType); + +const dropbox = `${config.shared.presAnnDropboxDir}/${jobId}` +let job = fs.readFileSync(`${dropbox}/job`); +let exportJob = JSON.parse(job); + +async function upload(exportJob) { + let callbackUrl = `http://${config.bbbWeb.host}:${config.bbbWeb.port}/bigbluebutton/presentation/${exportJob.presentationUploadToken}/upload` + let formData = new FormData(); + + formData.append('presentation_name', 'annotated_slides.pdf'); + formData.append('Filename', 'annotated_slides'); + formData.append('conference', exportJob.parentMeetingId); + formData.append('room', exportJob.parentMeetingId); + formData.append('pod_id', config.notifier.pod_id); + formData.append('is_downloadable', config.notifier.is_downloadable); + formData.append('fileUpload', fs.createReadStream(`${exportJob.presLocation}/annotated_slides_${jobId}.pdf`)); + + let res = await axios.post(callbackUrl, formData, { headers: formData.getHeaders() }); + logger.info(`Upload of job ${exportJob.jobId} returned ${res.data}`); +} + +if (jobType == 'PresentationWithAnnotationDownloadJob') { + +} else if (jobType == 'PresentationWithAnnotationExportJob') { + upload(exportJob) + +} else { + logger.error(`Notifier received unknown job type ${jobType}`); +} + +parentPort.postMessage({ message: workerData }) diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js index 356203080c..5a00a53482 100644 --- a/export-annotations/workers/process.js +++ b/export-annotations/workers/process.js @@ -4,7 +4,7 @@ const fs = require('fs'); const convert = require('xml-js'); const { create } = require('xmlbuilder2', { encoding: 'utf-8' }); const { execSync } = require("child_process"); -const { workerData, parentPort } = require('worker_threads'); +const { Worker, workerData, parentPort } = require('worker_threads'); const jobId = workerData; const MAGIC_MYSTERY_NUMBER = 2; @@ -14,7 +14,7 @@ logger.info("Processing PDF for job " + jobId); const kickOffNotifierWorker = (jobType) => { return new Promise((resolve, reject) => { - const worker = new Worker('./workers/notifier.js', { workerData: jobType }); + const worker = new Worker('./workers/notifier.js', { workerData: [jobType, jobId] }); worker.on('message', resolve); worker.on('error', reject); worker.on('exit', (code) => { @@ -260,10 +260,10 @@ function overlay_triangle(svg, annotation, w, h) { function overlay_text(svg, annotation, w, h) { let fontColor = color_to_hex(annotation.fontColor); - let textBoxWidth = Math.ceil(scale_shape(w, annotation.textBoxWidth)); - let textBoxHeight = Math.ceil(scale_shape(h, annotation.textBoxHeight)); - let textBox_x = Math.ceil(scale_shape(w, annotation.x)); - let textBox_y = Math.ceil(scale_shape(h, annotation.y)); + let textBoxWidth = Math.round(scale_shape(w, annotation.textBoxWidth)); + let textBoxHeight = Math.round(scale_shape(h, annotation.textBoxHeight)); + let textBox_x = Math.round(scale_shape(w, annotation.x)); + let textBox_y = Math.round(scale_shape(h, annotation.y)); let fontSize = scale_shape(h, annotation.calcedFontSize) let style = [ @@ -429,6 +429,6 @@ execSync(`rsvg-convert ${rsvgConvertInput} -f pdf -o ${exportJob.presLocation}/a // Launch Notifier Worker depending on job type logger.info(`Saved PDF at ${exportJob.presLocation}/annotated_slides_${jobId}.pdf`); -// kickOffNotifierWorker(exportJob.jobType); +kickOffNotifierWorker(exportJob.jobType); parentPort.postMessage({ message: workerData }) From 3b77aef4c35db5dacc078cbf6681b8cd248d5760 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Tue, 1 Mar 2022 11:01:50 +0100 Subject: [PATCH 035/268] Notifier: notify MeetingActor --- .../senders/ReceivedJsonMsgHandlerActor.scala | 3 +- .../core/running/MeetingActor.scala | 4 +- .../bigbluebutton/core2/AnalyticsActor.scala | 1 + .../common2/msgs/PresentationMsgs.scala | 4 ++ export-annotations/config/settings.json | 3 +- export-annotations/workers/notifier.js | 39 ++++++++++++++++++- export-annotations/workers/process.js | 2 +- .../client/meeting/AllowedMessageNames.scala | 1 + 8 files changed, 50 insertions(+), 7 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala index e9a68cfeae..c2165d0f3a 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala @@ -270,6 +270,8 @@ class ReceivedJsonMsgHandlerActor( routeGenericMsg[MakePresentationWithAnnotationDownloadReqMsg](envelope, jsonNode) case ExportPresentationWithAnnotationReqMsg.NAME => routeGenericMsg[ExportPresentationWithAnnotationReqMsg](envelope, jsonNode) + case NewPresAnnFileAvailableMsg.NAME => + routeGenericMsg[NewPresAnnFileAvailableMsg](envelope, jsonNode) // Presentation Pods case CreateNewPresentationPodPubMsg.NAME => @@ -358,7 +360,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 diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index 27f12e1d82..83f26846cf 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -501,6 +501,7 @@ class MeetingActor( case m: AssignPresenterReqMsg => state = handlePresenterChange(m, state) case m: MakePresentationWithAnnotationDownloadReqMsg => handleMakePresentationWithAnnotationDownloadReqMsg(m, state, liveMeeting) case m: ExportPresentationWithAnnotationReqMsg => handleExportPresentationWithAnnotationReqMsg(m, state, liveMeeting) + case m: NewPresAnnFileAvailableMsg => log.info("***** New PDF with annotations available.") // Presentation Pods case m: CreateNewPresentationPodPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) @@ -794,7 +795,7 @@ class MeetingActor( // 2) Insert Export Job in Redis val jobType = "PresentationWithAnnotationDownloadJob" val presLocation = s"/var/bigbluebutton/${meetingId}/${meetingId}/${presId}" - val exportJob = new ExportJob(jobId, jobType, presId, presLocation, allPages, pagesRange, "", "") + val exportJob = new ExportJob(jobId, jobType, presId, presLocation, allPages, pagesRange, meetingId, "") var job = buildStoreExportJobInRedisSysMsg(exportJob) outGW.send(job) } @@ -802,7 +803,6 @@ class MeetingActor( def handleExportPresentationWithAnnotationReqMsg(m: ExportPresentationWithAnnotationReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = { val meetingId = liveMeeting.props.meetingProp.intId - val userId = m.header.userId val presId: String = getMeetingInfoPresentationDetails.id val parentMeetingId: String = m.body.parentMeetingId diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala index 08307475ab..5f8fb9a68f 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala @@ -118,6 +118,7 @@ class AnalyticsActor(val includeChat: Boolean) extends Actor with ActorLogging { // 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) diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationMsgs.scala index 4c508064d8..6d86cb028d 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationMsgs.scala @@ -18,6 +18,10 @@ object ExportPresentationWithAnnotationReqMsg { val NAME = "ExportPresentationWi 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 ------------ diff --git a/export-annotations/config/settings.json b/export-annotations/config/settings.json index 1ef571947f..4cf01ea6c0 100644 --- a/export-annotations/config/settings.json +++ b/export-annotations/config/settings.json @@ -11,7 +11,8 @@ }, "notifier": { "pod_id": "DEFAULT_PRESENTATION_POD", - "is_downloadable": "false" + "is_downloadable": "false", + "msgName": "NewPresAnnFileAvailableMsg" }, "genPollSVG": { "path": "/home/petriroc/dev/bigbluebutton/record-and-playback/core/scripts/utils/gen_poll_svg" diff --git a/export-annotations/workers/notifier.js b/export-annotations/workers/notifier.js index eed0b9ed1e..401ec150c0 100644 --- a/export-annotations/workers/notifier.js +++ b/export-annotations/workers/notifier.js @@ -2,6 +2,7 @@ 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 { workerData, parentPort } = require('worker_threads') @@ -9,12 +10,45 @@ const { workerData, parentPort } = require('worker_threads') const [jobType, jobId] = workerData; const logger = new Logger('presAnn Notifier Worker'); -logger.info("Processing PDF for job " + jobType); const dropbox = `${config.shared.presAnnDropboxDir}/${jobId}` let job = fs.readFileSync(`${dropbox}/job`); let exportJob = JSON.parse(job); +async function connectToRedis() { + 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)); + + // 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: `file://${exportJob.presLocation}/annotated_slides_${jobId}.pdf`, + }, + } + } + + await client.publish(config.redis.channels.publish, JSON.stringify(notification)); +} + async function upload(exportJob) { let callbackUrl = `http://${config.bbbWeb.host}:${config.bbbWeb.port}/bigbluebutton/presentation/${exportJob.presentationUploadToken}/upload` let formData = new FormData(); @@ -32,9 +66,10 @@ async function upload(exportJob) { } if (jobType == 'PresentationWithAnnotationDownloadJob') { + connectToRedis(); } else if (jobType == 'PresentationWithAnnotationExportJob') { - upload(exportJob) + upload(exportJob); } else { logger.error(`Notifier received unknown job type ${jobType}`); diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js index 5a00a53482..551ada1a82 100644 --- a/export-annotations/workers/process.js +++ b/export-annotations/workers/process.js @@ -294,7 +294,7 @@ function overlay_text(svg, annotation, w, h) { svg.ele('image', { 'xlink:href': `file://${dropbox}/text${annotation.id}.png`, x: textBox_x, - y: textBox_y, + y: textBox_y - fontSize, width: textBoxWidth, height: textBoxHeight, }).up(); diff --git a/labs/vertx-akka/src/main/scala/org/bigbluebutton/client/meeting/AllowedMessageNames.scala b/labs/vertx-akka/src/main/scala/org/bigbluebutton/client/meeting/AllowedMessageNames.scala index 6ad8a38490..26c22e42b7 100755 --- a/labs/vertx-akka/src/main/scala/org/bigbluebutton/client/meeting/AllowedMessageNames.scala +++ b/labs/vertx-akka/src/main/scala/org/bigbluebutton/client/meeting/AllowedMessageNames.scala @@ -63,6 +63,7 @@ object AllowedMessageNames { SetPresenterInPodReqMsg.NAME, MakePresentationWithAnnotationDownloadReqMsg.NAME, ExportPresentationWithAnnotationReqMsg.NAME, + NewPresAnnFileAvailableMsg.NAME, StoreAnnotationsInRedisSysMsg.NAME, StoreExportJobInRedisSysMsg.NAME, From 1bc6b44e557356cda210d36d791084898c403164 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Wed, 2 Mar 2022 17:02:08 +0100 Subject: [PATCH 036/268] Notifier: send out link to file for download --- .../nginx-confs/presentation-slides.nginx | 8 ++++++++ export-annotations/config/settings.json | 4 +++- export-annotations/workers/notifier.js | 8 +++++--- export-annotations/workers/process.js | 15 +++++++++++++-- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/bigbluebutton-web/nginx-confs/presentation-slides.nginx b/bigbluebutton-web/nginx-confs/presentation-slides.nginx index 1ea4c4fa28..a04ba59318 100644 --- a/bigbluebutton-web/nginx-confs/presentation-slides.nginx +++ b/bigbluebutton-web/nginx-confs/presentation-slides.nginx @@ -27,6 +27,14 @@ } } + location ~^\/bigbluebutton\/presentation\/(?[A-Za-z0-9\-]+)\/(?[A-Za-z0-9\-]+)\/(?[A-Za-z0-9\-]+)\/pdf\/(?[A-Za-z0-9]+)$ { + default_type application/pdf; + alias /var/bigbluebutton/$meeting_id_2/$meeting_id_2/$pres_id/pdfs/annotated_slides_$job_id.pdf; + if ($bbb_loadbalancer_node) { + add_header 'Access-Control-Allow-Origin' $bbb_loadbalancer_node always; + } + } + location ~^\/bigbluebutton\/presentation\/(?[A-Za-z0-9\-]+)\/(?[A-Za-z0-9\-]+)\/(?[A-Za-z0-9\-]+)\/slide\/(?\d+)$ { alias /var/bigbluebutton/$meeting_id_2/$meeting_id_2/$pres_id/slide-$page_num.swf; if ($bbb_loadbalancer_node) { diff --git a/export-annotations/config/settings.json b/export-annotations/config/settings.json index 4cf01ea6c0..31d57ddac8 100644 --- a/export-annotations/config/settings.json +++ b/export-annotations/config/settings.json @@ -12,7 +12,9 @@ "notifier": { "pod_id": "DEFAULT_PRESENTATION_POD", "is_downloadable": "false", - "msgName": "NewPresAnnFileAvailableMsg" + "msgName": "NewPresAnnFileAvailableMsg", + "protocol": "https", + "host": "localhost" }, "genPollSVG": { "path": "/home/petriroc/dev/bigbluebutton/record-and-playback/core/scripts/utils/gen_poll_svg" diff --git a/export-annotations/workers/notifier.js b/export-annotations/workers/notifier.js index 401ec150c0..b338f0ee3a 100644 --- a/export-annotations/workers/notifier.js +++ b/export-annotations/workers/notifier.js @@ -25,6 +25,7 @@ async function connectToRedis() { 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}`; // Notify Meeting Actor of file availability by sending a message through Redis PubSub const notification = { envelope: { @@ -41,11 +42,12 @@ async function connectToRedis() { userId: "" }, body: { - fileURI: `file://${exportJob.presLocation}/annotated_slides_${jobId}.pdf`, + fileURI: link }, - } + } } + logger.info(`Annotated PDF available at ${link}`); await client.publish(config.redis.channels.publish, JSON.stringify(notification)); } @@ -59,7 +61,7 @@ async function upload(exportJob) { formData.append('room', exportJob.parentMeetingId); formData.append('pod_id', config.notifier.pod_id); formData.append('is_downloadable', config.notifier.is_downloadable); - formData.append('fileUpload', fs.createReadStream(`${exportJob.presLocation}/annotated_slides_${jobId}.pdf`)); + formData.append('fileUpload', fs.createReadStream(`${exportJob.presLocation}/pdfs/annotated_slides_${jobId}.pdf`)); let res = await axios.post(callbackUrl, formData, { headers: formData.getHeaders() }); logger.info(`Upload of job ${exportJob.jobId} returned ${res.data}`); diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js index 551ada1a82..c22bb67251 100644 --- a/export-annotations/workers/process.js +++ b/export-annotations/workers/process.js @@ -42,6 +42,7 @@ function render_HTMLTextBox(htmlFilePath, id, width, height) { '--crop-w', width, '--crop-h', height, '--log-level', 'none', + '--quality', '100', htmlFilePath, `${dropbox}/text${id}.png` ] @@ -414,8 +415,18 @@ for (let i = 0; i < pages.length; i++) { rsvgConvertInput += `${file} ` } +// Create PDF output directory if it doesn't exist +let output_dir = `${exportJob.presLocation}/pdfs`; +if (!fs.existsSync(output_dir)) { fs.mkdirSync(output_dir); } + +let render = [ + 'rsvg-convert', rsvgConvertInput, + '-f', 'pdf', + '-o', `${output_dir}/annotated_slides_${jobId}.pdf` + ].join(' '); + // Resulting PDF file is stored in the presentation dir -execSync(`rsvg-convert ${rsvgConvertInput} -f pdf -o ${exportJob.presLocation}/annotated_slides_${jobId}.pdf`, (error, stderr) => { +execSync(render, (error, stderr) => { if (error) { logger.error(`SVG to PDF export failed with error: ${error.message}`); return; @@ -427,7 +438,7 @@ execSync(`rsvg-convert ${rsvgConvertInput} -f pdf -o ${exportJob.presLocation}/a }); // Launch Notifier Worker depending on job type -logger.info(`Saved PDF at ${exportJob.presLocation}/annotated_slides_${jobId}.pdf`); +logger.info(`Saved PDF at ${output_dir}/annotated_slides_${jobId}.pdf`); kickOffNotifierWorker(exportJob.jobType); From 7925826db273ff07d203751e17b3a09a79ef2da4 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Wed, 2 Mar 2022 18:40:39 +0100 Subject: [PATCH 037/268] Set path for poll generation --- export-annotations/config/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/export-annotations/config/settings.json b/export-annotations/config/settings.json index 31d57ddac8..8cc6f1c00f 100644 --- a/export-annotations/config/settings.json +++ b/export-annotations/config/settings.json @@ -17,7 +17,7 @@ "host": "localhost" }, "genPollSVG": { - "path": "/home/petriroc/dev/bigbluebutton/record-and-playback/core/scripts/utils/gen_poll_svg" + "path": "/usr/local/bigbluebutton/core/scripts/utils/gen_poll_svg" }, "bbbWeb": { "host": "127.0.0.1", From 130b38bb982627c6b8b54af8703fab7f64ba57c9 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Wed, 2 Mar 2022 19:15:24 +0100 Subject: [PATCH 038/268] Remove client-side implementation, leaving only the API --- .../imports/api/annotations/server/methods.js | 2 -- ...resentationWithAnnotationDownloadReqMsg.js | 29 ---------------- .../presentation-toolbar/component.jsx | 33 +------------------ .../presentation-toolbar/container.jsx | 2 -- .../presentation-toolbar/service.js | 5 --- .../presentation-toolbar/styles.js | 29 ---------------- bigbluebutton-html5/public/locales/en.json | 1 - 7 files changed, 1 insertion(+), 100 deletions(-) delete mode 100644 bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods.js b/bigbluebutton-html5/imports/api/annotations/server/methods.js index e535558237..beb862327f 100644 --- a/bigbluebutton-html5/imports/api/annotations/server/methods.js +++ b/bigbluebutton-html5/imports/api/annotations/server/methods.js @@ -3,12 +3,10 @@ import undoAnnotation from './methods/undoAnnotation'; import clearWhiteboard from './methods/clearWhiteboard'; import sendAnnotation from './methods/sendAnnotation'; import sendBulkAnnotations from './methods/sendBulkAnnotations'; -import makePresentationWithAnnotationDownloadReqMsg from './methods/makePresentationWithAnnotationDownloadReqMsg' Meteor.methods({ undoAnnotation, clearWhiteboard, sendAnnotation, sendBulkAnnotations, - makePresentationWithAnnotationDownloadReqMsg, }); diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js b/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js deleted file mode 100644 index 3fe44131be..0000000000 --- a/bigbluebutton-html5/imports/api/annotations/server/methods/makePresentationWithAnnotationDownloadReqMsg.js +++ /dev/null @@ -1,29 +0,0 @@ -import RedisPubSub from '/imports/startup/server/redis'; -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; -import { extractCredentials } from '/imports/api/common/server/helpers'; -import Logger from '/imports/startup/server/logger'; - -export default function makePresentationWithAnnotationDownloadReqMsg() { - const REDIS_CONFIG = Meteor.settings.private.redis; - const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; - const EVENT_NAME = 'MakePresentationWithAnnotationDownloadReqMsg'; - - try { - const { meetingId, requesterUserId } = extractCredentials(this.userId); - - check(meetingId, String); - check(requesterUserId, String); - - const payload = { - presId: "", - allPages: true, - pages: [], - }; - - return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); - - } catch (err) { - Logger.error(`Exception while invoking method makePresentationWithAnnotationDownloadReqMsg ${err.stack}`); - } -} diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx index 2178411e39..01ee542271 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx @@ -10,10 +10,6 @@ import TooltipContainer from '/imports/ui/components/tooltip/container'; import KEY_CODES from '/imports/utils/keyCodes'; const intlMessages = defineMessages({ - downloadAnnotatedSlidesLabel: { - id: 'app.presentation.presentationToolbar.downloadSlideWithAnnotations', - description: 'Download slide with annotations label' - }, previousSlideLabel: { id: 'app.presentation.presentationToolbar.prevSlideLabel', description: 'Previous slide button label', @@ -79,7 +75,7 @@ const intlMessages = defineMessages({ class PresentationToolbar extends PureComponent { constructor(props) { super(props); - this.handleDownloadAnnotatedSlides = this.handleDownloadAnnotatedSlides.bind(this); + this.handleSkipToSlideChange = this.handleSkipToSlideChange.bind(this); this.change = this.change.bind(this); this.renderAriaDescs = this.renderAriaDescs.bind(this); @@ -119,14 +115,6 @@ class PresentationToolbar extends PureComponent { } } - handleDownloadAnnotatedSlides() { - const { - downloadAnnotatedSlides - } = this.props; - - downloadAnnotatedSlides() - } - handleSkipToSlideChange(event) { const { skipToSlide, @@ -199,9 +187,6 @@ class PresentationToolbar extends PureComponent {
{intl.formatMessage(intlMessages.nextSlideDesc)}
-
- {intl.formatMessage(intlMessages.downloadAnnotatedSlidesLabel)} -
{intl.formatMessage(intlMessages.noNextSlideDesc)}
@@ -268,8 +253,6 @@ class PresentationToolbar extends PureComponent { ? intl.formatMessage(intlMessages.nextSlideLabel) : `${intl.formatMessage(intlMessages.nextSlideLabel)} (${currentSlideNum >= 1 ? (currentSlideNum + 1) : ''})`; - const downloadAnnotatedSlidesLabel = intl.formatMessage(intlMessages.downloadAnnotatedSlidesLabel); - return ( - } { @@ -411,7 +381,6 @@ PresentationToolbar.propTypes = { nextSlide: PropTypes.func.isRequired, previousSlide: PropTypes.func.isRequired, skipToSlide: PropTypes.func.isRequired, - downloadAnnotatedSlides: PropTypes.func.isRequired, intl: PropTypes.shape({ formatMessage: PropTypes.func.isRequired, }).isRequired, diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx index 44e2978b6b..d68647a6c7 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx @@ -53,7 +53,6 @@ export default withTracker((params) => { nextSlide: PresentationToolbarService.nextSlide, previousSlide: PresentationToolbarService.previousSlide, skipToSlide: PresentationToolbarService.skipToSlide, - downloadAnnotatedSlides: PresentationToolbarService.downloadAnnotatedSlides, isMeteorConnected: Meteor.status().connected, isPollingEnabled: POLLING_ENABLED, currentSlidHasContent: PresentationService.currentSlidHasContent(), @@ -75,5 +74,4 @@ PresentationToolbarContainer.propTypes = { nextSlide: PropTypes.func.isRequired, previousSlide: PropTypes.func.isRequired, skipToSlide: PropTypes.func.isRequired, - downloadAnnotatedSlides: PropTypes.func.isRequired, }; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js index 2829cb1350..e8ca731e59 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js @@ -29,10 +29,6 @@ const nextSlide = (currentSlideNum, numberOfSlides, podId) => { } }; -const downloadAnnotatedSlides = () => { - makeCall('makePresentationWithAnnotationDownloadReqMsg'); -}; - const zoomSlide = throttle((currentSlideNum, podId, widthRatio, heightRatio, xOffset, yOffset) => { makeCall('zoomSlide', currentSlideNum, podId, widthRatio, heightRatio, xOffset, yOffset); }, PAN_ZOOM_INTERVAL); @@ -44,7 +40,6 @@ const skipToSlide = (requestedSlideNum, podId) => { export default { getNumberOfSlides, nextSlide, - downloadAnnotatedSlides, previousSlide, skipToSlide, zoomSlide, diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.js b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.js index 7f5fa6916a..b71311aa7d 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.js @@ -140,34 +140,6 @@ const NextSlideButton = styled(Button)` } `; -const DownloadAnnotatedSlidesButton = styled(Button)` - border: none !important; - - & > i { - font-size: 1rem; - - [dir="rtl"] & { - -webkit-transform: scale(-1, 1); - -moz-transform: scale(-1, 1); - -ms-transform: scale(-1, 1); - -o-transform: scale(-1, 1); - transform: scale(-1, 1); - } - } - - position: relative; - color: ${toolbarButtonColor}; - background-color: ${colorOffWhite}; - border-radius: 0; - box-shadow: none !important; - border: 0; - - &:focus { - background-color: ${colorOffWhite}; - border: 0; - } -`; - const SkipSlideSelect = styled.select` padding: 0 ${smPaddingY}; margin: ${borderSize}; @@ -254,7 +226,6 @@ export default { PresentationSlideControls, PrevSlideButton, NextSlideButton, - DownloadAnnotatedSlidesButton, SkipSlideSelect, PresentationZoomControls, FitToWidthButton, diff --git a/bigbluebutton-html5/public/locales/en.json b/bigbluebutton-html5/public/locales/en.json index c8f188dc2d..48412c3c5b 100755 --- a/bigbluebutton-html5/public/locales/en.json +++ b/bigbluebutton-html5/public/locales/en.json @@ -169,7 +169,6 @@ "app.presentation.presentationToolbar.prevSlideDesc": "Change the presentation to the previous slide", "app.presentation.presentationToolbar.nextSlideLabel": "Next slide", "app.presentation.presentationToolbar.nextSlideDesc": "Change the presentation to the next slide", - "app.presentation.presentationToolbar.downloadSlideWithAnnotations": "Download slides with annotations", "app.presentation.presentationToolbar.skipSlideLabel": "Skip slide", "app.presentation.presentationToolbar.skipSlideDesc": "Change the presentation to a specific slide", "app.presentation.presentationToolbar.fitWidthLabel": "Fit to width", From 9f833a47603d796c7d85fba810262a3da2c387ca Mon Sep 17 00:00:00 2001 From: Daniel Molkentin Date: Mon, 14 Feb 2022 18:49:11 +0100 Subject: [PATCH 039/268] Introduce bigbluebutton.target --- build/packages-template/bbb-config/bigbluebutton.target | 7 +++++++ build/packages-template/bbb-config/build.sh | 2 ++ build/packages-template/bbb-etherpad/etherpad.service | 3 ++- build/packages-template/bbb-html5/bbb-html5.service | 3 ++- .../bbb-html5/disable-transparent-huge-pages.service | 3 ++- build/packages-template/bbb-html5/mongod.service | 3 ++- .../bbb-libreoffice-docker/libreoffice.service | 4 ++-- build/packages-template/bbb-lti/bbb-lti.service | 3 ++- build/packages-template/bbb-web/bbb-web.service | 3 ++- build/packages-template/bbb-webhooks/bbb-webhooks.service | 3 ++- .../bbb-webrtc-sfu/bbb-webrtc-sfu.service | 3 ++- .../bbb-webrtc-sfu/kurento-media-server.service | 3 ++- 12 files changed, 29 insertions(+), 11 deletions(-) create mode 100644 build/packages-template/bbb-config/bigbluebutton.target diff --git a/build/packages-template/bbb-config/bigbluebutton.target b/build/packages-template/bbb-config/bigbluebutton.target new file mode 100644 index 0000000000..b24b4b09cd --- /dev/null +++ b/build/packages-template/bbb-config/bigbluebutton.target @@ -0,0 +1,7 @@ +[Unit] +Description=Big Blue Button System +Requires= +After=syslog.target network.target + +[Install] +WantedBy=multi-user.target diff --git a/build/packages-template/bbb-config/build.sh b/build/packages-template/bbb-config/build.sh index be008f33c7..7a17f706e4 100755 --- a/build/packages-template/bbb-config/build.sh +++ b/build/packages-template/bbb-config/build.sh @@ -14,6 +14,7 @@ rm -rf staging # # Create build directories for markign by fpm DIRS="/etc/bigbluebutton \ + /lib/systemd/system \ /var/bigbluebutton/blank \ /usr/share/bigbluebutton/blank \ /var/www/bigbluebutton-default" @@ -68,6 +69,7 @@ Wants=redis-server.service After=redis-server.service HERE +cp bigbluebutton.target staging/lib/systemd/system/ . ./opts-$DISTRO.sh diff --git a/build/packages-template/bbb-etherpad/etherpad.service b/build/packages-template/bbb-etherpad/etherpad.service index 7f844c379b..c1f01089ae 100644 --- a/build/packages-template/bbb-etherpad/etherpad.service +++ b/build/packages-template/bbb-etherpad/etherpad.service @@ -2,6 +2,7 @@ Description=Etherpad Server Wants=redis-server.service After=syslog.target network.target +packages-template/bbb-config/build.sh [Service] Type=simple @@ -14,5 +15,5 @@ Restart=always # use mysql plus a complete settings.json to avoid Service hold-off time over, scheduling restart. [Install] -WantedBy=multi-user.target +WantedBy=multi-user.target bigbluebutton.target diff --git a/build/packages-template/bbb-html5/bbb-html5.service b/build/packages-template/bbb-html5/bbb-html5.service index ef3bc8ca5a..c58d02b3a9 100644 --- a/build/packages-template/bbb-html5/bbb-html5.service +++ b/build/packages-template/bbb-html5/bbb-html5.service @@ -2,6 +2,7 @@ Description=BigBlueButton HTML5 service Wants=redis.service mongod.service disable-transparent-huge-pages.service bbb-pads.service After=redis.service mongod.service disable-transparent-huge-pages.service bbb-pads.service syslog.target network.target +PartOf=bigbluebutton.target [Service] Type=oneshot @@ -16,5 +17,5 @@ RemainAfterExit=yes User=root [Install] -WantedBy=multi-user.target +WantedBy=multi-user.target bigbluebutton.target diff --git a/build/packages-template/bbb-html5/disable-transparent-huge-pages.service b/build/packages-template/bbb-html5/disable-transparent-huge-pages.service index a50ff6476f..fafda696e9 100644 --- a/build/packages-template/bbb-html5/disable-transparent-huge-pages.service +++ b/build/packages-template/bbb-html5/disable-transparent-huge-pages.service @@ -1,5 +1,6 @@ [Unit] Description=Disable Transparent Huge Pages +PartOf=bigbluebutton.target [Service] Type=oneshot @@ -7,4 +8,4 @@ ExecStart=/bin/sh -c "/bin/echo "never" | tee /sys/kernel/mm/transparent_hugepag ExecStart=/bin/sh -c "/bin/echo "never" | tee /sys/kernel/mm/transparent_hugepage/defrag" [Install] -WantedBy=multi-user.target +WantedBy=multi-user.target bigbluebutton.target diff --git a/build/packages-template/bbb-html5/mongod.service b/build/packages-template/bbb-html5/mongod.service index 659f40878b..d84dc5abb6 100644 --- a/build/packages-template/bbb-html5/mongod.service +++ b/build/packages-template/bbb-html5/mongod.service @@ -5,6 +5,7 @@ Description=High-performance, schema-free document-oriented database After=network.target Documentation=https://docs.mongodb.org/manual +PartOf=bigbluebutton.target [Service] User=mongodb @@ -34,5 +35,5 @@ TasksAccounting=false # http://docs.mongodb.org/manual/reference/ulimit/#recommended-settings [Install] -WantedBy=multi-user.target +WantedBy=multi-user.target bigbluebutton.target diff --git a/build/packages-template/bbb-libreoffice-docker/libreoffice.service b/build/packages-template/bbb-libreoffice-docker/libreoffice.service index cb0fff0c9d..d1b84573f2 100644 --- a/build/packages-template/bbb-libreoffice-docker/libreoffice.service +++ b/build/packages-template/bbb-libreoffice-docker/libreoffice.service @@ -1,6 +1,7 @@ [Unit] Description=LibreOffice After=network.target +PartOf=bigbluebutton.target [Service] Type=simple @@ -12,5 +13,4 @@ MemoryLimit=1G CPUQuota=20% [Install] -WantedBy=multi-user.target - +WantedBy=multi-user.target bigbluebutton.target diff --git a/build/packages-template/bbb-lti/bbb-lti.service b/build/packages-template/bbb-lti/bbb-lti.service index 2a3a38a94c..d951f8b100 100644 --- a/build/packages-template/bbb-lti/bbb-lti.service +++ b/build/packages-template/bbb-lti/bbb-lti.service @@ -1,6 +1,7 @@ [Unit] Description=BigBlueButton LTI Application Requires=network.target +PartOf=bigbluebutton.target [Service] Type=simple @@ -19,5 +20,5 @@ PermissionsStartOnly=true LimitNOFILE=1024 [Install] -WantedBy=multi-user.target +WantedBy=multi-user.target bigbluebutton.target diff --git a/build/packages-template/bbb-web/bbb-web.service b/build/packages-template/bbb-web/bbb-web.service index 6caf03f082..bb68dbcaa5 100644 --- a/build/packages-template/bbb-web/bbb-web.service +++ b/build/packages-template/bbb-web/bbb-web.service @@ -2,6 +2,7 @@ Description=BigBlueButton Web Application Requires=network.target After=redis.service +PartOf=bigbluebutton.target [Service] Type=simple @@ -20,5 +21,5 @@ PermissionsStartOnly=true LimitNOFILE=1024 [Install] -WantedBy=multi-user.target +WantedBy=multi-user.target bigbluebutton.target diff --git a/build/packages-template/bbb-webhooks/bbb-webhooks.service b/build/packages-template/bbb-webhooks/bbb-webhooks.service index daf6c0d73e..fda742c862 100644 --- a/build/packages-template/bbb-webhooks/bbb-webhooks.service +++ b/build/packages-template/bbb-webhooks/bbb-webhooks.service @@ -2,6 +2,7 @@ Description=BigBlueButton Webhooks Wants=redis-server.service After=syslog.target network.target +PartOf=bigbluebutton.target [Service] WorkingDirectory=/usr/local/bigbluebutton/bbb-webhooks @@ -13,4 +14,4 @@ Group=bigbluebutton Environment=NODE_ENV=production [Install] -WantedBy=multi-user.target +WantedBy=multi-user.target bigbluebutton.target diff --git a/build/packages-template/bbb-webrtc-sfu/bbb-webrtc-sfu.service b/build/packages-template/bbb-webrtc-sfu/bbb-webrtc-sfu.service index 0a44ee1ae5..b3147dda89 100644 --- a/build/packages-template/bbb-webrtc-sfu/bbb-webrtc-sfu.service +++ b/build/packages-template/bbb-webrtc-sfu/bbb-webrtc-sfu.service @@ -2,6 +2,7 @@ Description=BigBlueButton WebRTC SFU Wants=redis-server.service After=syslog.target network.target freeswitch.service kurento-media-server.service +PartOf=bigbluebutton.target [Service] WorkingDirectory=/usr/local/bigbluebutton/bbb-webrtc-sfu @@ -14,4 +15,4 @@ Environment=NODE_ENV=production Environment=NODE_CONFIG_DIR=/etc/bigbluebutton/bbb-webrtc-sfu/:/usr/local/bigbluebutton/bbb-webrtc-sfu/config/ [Install] -WantedBy=multi-user.target +WantedBy=multi-user.target bigbluebutton.target diff --git a/build/packages-template/bbb-webrtc-sfu/kurento-media-server.service b/build/packages-template/bbb-webrtc-sfu/kurento-media-server.service index 1c8d33f8ab..c50e5d8b73 100644 --- a/build/packages-template/bbb-webrtc-sfu/kurento-media-server.service +++ b/build/packages-template/bbb-webrtc-sfu/kurento-media-server.service @@ -1,6 +1,7 @@ [Unit] Description=Kurento Media Server daemon After=network.target +PartOf=bigbluebutton.target [Service] UMask=0002 @@ -15,5 +16,5 @@ PIDFile=/var/run/kurento-media-server.pid Restart=always [Install] -WantedBy=default.target +WantedBy=multi-user.target bigbluebutton.target From 568756bf7ef3ef0be71ccea7ece2c8a7b7a32f8b Mon Sep 17 00:00:00 2001 From: Daniel Molkentin Date: Mon, 14 Feb 2022 18:49:13 +0100 Subject: [PATCH 040/268] move systemd files to /lib/systemd/system --- build/packages-template/bbb-html5/after-install.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/packages-template/bbb-html5/after-install.sh b/build/packages-template/bbb-html5/after-install.sh index d119456c19..073fcc81b5 100644 --- a/build/packages-template/bbb-html5/after-install.sh +++ b/build/packages-template/bbb-html5/after-install.sh @@ -77,9 +77,9 @@ if [ -f /opt/freeswitch/etc/freeswitch/sip_profiles/external.xml ]; then sed -i 's///g' /opt/freeswitch/etc/freeswitch/sip_profiles/external.xml fi -chown root:root /usr/lib/systemd/system -chown root:root /usr/lib/systemd/system/bbb-html5.service -chown root:root /usr/lib/systemd/system/disable-transparent-huge-pages.service +chown root:root /lib/systemd/system +chown root:root /lib/systemd/system/bbb-html5.service +chown root:root /lib/systemd/system/disable-transparent-huge-pages.service # Ensure settings is readable chmod go+r /usr/share/meteor/bundle/programs/server/assets/app/config/settings.yml From dbae417b0c1c361082a53573a687be7548cba699 Mon Sep 17 00:00:00 2001 From: Daniel Molkentin Date: Mon, 14 Feb 2022 18:49:14 +0100 Subject: [PATCH 041/268] akka apps should not have override files they are already provided by sbt --- build/packages-template/bbb-config/build.sh | 24 --------------------- 1 file changed, 24 deletions(-) diff --git a/build/packages-template/bbb-config/build.sh b/build/packages-template/bbb-config/build.sh index 7a17f706e4..3f0b0b2a1d 100755 --- a/build/packages-template/bbb-config/build.sh +++ b/build/packages-template/bbb-config/build.sh @@ -45,30 +45,6 @@ cp cron.daily/* staging/etc/cron.daily mkdir -p staging/etc/cron.hourly cp cron.hourly/bbb-resync-freeswitch staging/etc/cron.hourly -# Overrides - -mkdir -p staging/etc/systemd/system/bbb-apps-akka.service.d -cat > staging/etc/systemd/system/bbb-apps-akka.service.d/override.conf < staging/etc/systemd/system/bbb-fsesl-akka.service.d/override.conf < staging/etc/systemd/system/bbb-transcode-akka.service.d/override.conf < Date: Thu, 17 Mar 2022 14:23:28 +0100 Subject: [PATCH 042/268] make bbb-pads.service part of bigbluebutton.target --- build/packages-template/bbb-pads/bbb-pads.service | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build/packages-template/bbb-pads/bbb-pads.service b/build/packages-template/bbb-pads/bbb-pads.service index 200b7c03f6..6c130ecf1c 100644 --- a/build/packages-template/bbb-pads/bbb-pads.service +++ b/build/packages-template/bbb-pads/bbb-pads.service @@ -2,6 +2,7 @@ Description=BigBlueButton Pads Wants=redis.service etherpad.service After=syslog.target network.target +PartOf=bigbluebutton.target [Service] WorkingDirectory=/usr/local/bigbluebutton/bbb-pads @@ -13,4 +14,4 @@ Group=bigbluebutton Environment=NODE_ENV=production [Install] -WantedBy=multi-user.target +WantedBy=multi-user.target bigbluebutton.target From 658b89308d6eefeef398e90f191340ab4d49a60b Mon Sep 17 00:00:00 2001 From: Daniel Molkentin Date: Thu, 17 Mar 2022 14:27:05 +0100 Subject: [PATCH 043/268] chore: remove obsolete gstream upgrade routine --- bigbluebutton-config/bin/bbb-conf | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/bigbluebutton-config/bin/bbb-conf b/bigbluebutton-config/bin/bbb-conf index ed319dfb9b..7bd5918ca8 100755 --- a/bigbluebutton-config/bin/bbb-conf +++ b/bigbluebutton-config/bin/bbb-conf @@ -861,26 +861,6 @@ check_configuration() { } -update_gstreamer() { - # due to a change in the kurento packages naming convention, gstreamer packages don't naturally upgrade - # this snippet checks if the installed gstreamer packages are the same as the one available in the repository - # if they are not, it will update (possibly downgrade) - # TODO remove it on 2.3 or above - if [ "$DISTRIB_CODENAME" == "xenial" ]; then - DOWNGRADE_LIST="" - for PACKAGE in $(dpkg -l | grep gst | tr -s ' ' | cut -d' ' -f2 | cut -d':' -f1); do - RIGHT_VERSION=$(apt-cache policy $PACKAGE | grep -B1 ubuntu.bigbluebutton.org | head -n1 | tr -s ' ' | cut -d' ' -f2) - if [[ $RIGHT_VERSION != "***" ]] && [[ $RIGHT_VERSION != "" ]]; then - echo "Force $PACKAGE to version $RIGHT_VERSION" - DOWNGRADE_LIST="$PACKAGE=$RIGHT_VERSION $DOWNGRADE_LIST" - fi - done - if [[ $DOWNGRADE_LIST != "" ]]; then - apt-get -y --allow-downgrades install $DOWNGRADE_LIST > /dev/null - fi - fi -} - check_state() { echo print_header @@ -1737,8 +1717,6 @@ String BigBlueButtonURL = \"$BBB_WEB_URL/bigbluebutton/\"; echo "Restarting the BigBlueButton $BIGBLUEBUTTON_RELEASE ..." stop_bigbluebutton - update_gstreamer - echo start_bigbluebutton exit 0 @@ -1752,7 +1730,6 @@ if [ $RESTART ]; then echo "Restarting BigBlueButton $BIGBLUEBUTTON_RELEASE ..." stop_bigbluebutton - update_gstreamer start_bigbluebutton check_state fi @@ -1764,7 +1741,6 @@ if [ $CLEAN ]; then echo "Restarting BigBlueButton $BIGBLUEBUTTON_RELEASE (and cleaning out all log files) ..." stop_bigbluebutton - update_gstreamer # # Clean log files From 9a71e8e0b39ec99a057b5d5738c1d9084895b829 Mon Sep 17 00:00:00 2001 From: Daniel Molkentin Date: Thu, 17 Mar 2022 15:04:16 +0100 Subject: [PATCH 044/268] make record-and-playback-scripts part of bbb.target --- record-and-playback/core/systemd/bbb-rap-caption-inbox.service | 3 ++- record-and-playback/core/systemd/bbb-rap-resque-worker.service | 3 ++- record-and-playback/core/systemd/bbb-rap-starter.service | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/record-and-playback/core/systemd/bbb-rap-caption-inbox.service b/record-and-playback/core/systemd/bbb-rap-caption-inbox.service index 528954381e..09dff10622 100644 --- a/record-and-playback/core/systemd/bbb-rap-caption-inbox.service +++ b/record-and-playback/core/systemd/bbb-rap-caption-inbox.service @@ -1,5 +1,6 @@ [Unit] Description=BigBlueButton recording caption upload handler +PartOf=bigbluebutton.target [Service] Type=simple @@ -9,4 +10,4 @@ Slice=bbb_record_core.slice Restart=on-failure [Install] -WantedBy=multi-user.target +WantedBy=multi-user.target bigbluebutton.target diff --git a/record-and-playback/core/systemd/bbb-rap-resque-worker.service b/record-and-playback/core/systemd/bbb-rap-resque-worker.service index 4fec548cf3..f0b7d2c2dc 100644 --- a/record-and-playback/core/systemd/bbb-rap-resque-worker.service +++ b/record-and-playback/core/systemd/bbb-rap-resque-worker.service @@ -2,6 +2,7 @@ Description=BigBlueButton resque worker for recordings Wants=redis.service After=redis.service +PartOf=bigbluebutton.target [Service] Type=simple @@ -15,4 +16,4 @@ Restart=always RestartSec=3 [Install] -WantedBy=multi-user.target +WantedBy=multi-user.target bigbluebutton.target diff --git a/record-and-playback/core/systemd/bbb-rap-starter.service b/record-and-playback/core/systemd/bbb-rap-starter.service index de7e5eb8f9..9aba301aca 100644 --- a/record-and-playback/core/systemd/bbb-rap-starter.service +++ b/record-and-playback/core/systemd/bbb-rap-starter.service @@ -1,5 +1,6 @@ [Unit] Description=BigBlueButton recording processing starter +PartOf=bigbluebutton.target [Service] Type=simple @@ -10,4 +11,4 @@ Slice=bbb_record_core.slice Restart=on-failure [Install] -WantedBy=multi-user.target +WantedBy=multi-user.target bigbluebutton.target From 61ce7ab9b658cea6e069faf91c96e6cb1d90f349 Mon Sep 17 00:00:00 2001 From: Daniel Molkentin Date: Mon, 14 Feb 2022 18:49:14 +0100 Subject: [PATCH 045/268] inject dependency to bigbluebutton.target into externally provided services --- build/packages-template/bbb-config/bigbluebutton.conf | 5 +++++ build/packages-template/bbb-config/build.sh | 6 ++++++ .../bbb-freeswitch-core/freeswitch.service.focal | 3 ++- 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 build/packages-template/bbb-config/bigbluebutton.conf diff --git a/build/packages-template/bbb-config/bigbluebutton.conf b/build/packages-template/bbb-config/bigbluebutton.conf new file mode 100644 index 0000000000..f49dc0bf44 --- /dev/null +++ b/build/packages-template/bbb-config/bigbluebutton.conf @@ -0,0 +1,5 @@ +[Unit] +PartOf=bigbluebutton.target + +[Install] +WantedBy=bigbluebutton.target diff --git a/build/packages-template/bbb-config/build.sh b/build/packages-template/bbb-config/build.sh index 3f0b0b2a1d..cbc2b7d435 100755 --- a/build/packages-template/bbb-config/build.sh +++ b/build/packages-template/bbb-config/build.sh @@ -2,6 +2,12 @@ TARGET=`basename $(pwd)` +# inject dependency to bigbluebutton.target +for unit in freeswitch nginx redis-server; do + mkdir -p "staging/lib/systemd/system/${unit}.service.d" + cp bigbluebutton.conf "staging/lib/systemd/system/${unit}.service.d/" +done + PACKAGE=$(echo $TARGET | cut -d'_' -f1) VERSION=$(echo $TARGET | cut -d'_' -f2) diff --git a/build/packages-template/bbb-freeswitch-core/freeswitch.service.focal b/build/packages-template/bbb-freeswitch-core/freeswitch.service.focal index 4d5b714c21..5111cd1fc9 100644 --- a/build/packages-template/bbb-freeswitch-core/freeswitch.service.focal +++ b/build/packages-template/bbb-freeswitch-core/freeswitch.service.focal @@ -3,6 +3,7 @@ [Unit] Description=freeswitch After=syslog.target network.target local-fs.target +PartOf=bigbluebutton.target [Service] ; service @@ -54,5 +55,5 @@ CPUSchedulingPriority=89 ; execute "systemctl daemon-reload" after editing the unit files. [Install] -WantedBy=multi-user.target +WantedBy=multi-user.target bigbluebutton.target From cd80d891e7d736db6be7e7b9b59784421afeee16 Mon Sep 17 00:00:00 2001 From: Daniel Molkentin Date: Thu, 17 Mar 2022 15:29:40 +0100 Subject: [PATCH 046/268] make bbb-conf use bigbluebutton.target --- bigbluebutton-config/bin/bbb-conf | 57 ++----------------------------- 1 file changed, 2 insertions(+), 55 deletions(-) diff --git a/bigbluebutton-config/bin/bbb-conf b/bigbluebutton-config/bin/bbb-conf index 7bd5918ca8..2cd520b407 100755 --- a/bigbluebutton-config/bin/bbb-conf +++ b/bigbluebutton-config/bin/bbb-conf @@ -338,35 +338,11 @@ uncomment () { stop_bigbluebutton () { echo "Stopping BigBlueButton" - if [ -f /usr/lib/systemd/system/bbb-html5.service ]; then - HTML5="mongod bbb-html5 bbb-webrtc-sfu kurento-media-server" - fi - - if [ -f /usr/lib/systemd/system/bbb-webhooks.service ]; then - WEBHOOKS=bbb-webhooks - fi - - if [ -f /usr/lib/systemd/system/bbb-pads.service ]; then - PADS=bbb-pads - fi - - if [ -f /usr/share/etherpad-lite/settings.json ]; then - ETHERPAD=etherpad - fi - - if [ -f /lib/systemd/system/bbb-web.service ]; then - BBB_WEB=bbb-web - fi - - if [ -f /usr/share/bbb-lti/WEB-INF/classes/lti-config.properties ]; then - BBB_LTI=bbb-lti - fi - if systemctl list-units --full -all | grep -q $TOMCAT_USER.service; then TOMCAT_SERVICE=$TOMCAT_USER fi - systemctl stop $TOMCAT_SERVICE nginx freeswitch $REDIS_SERVICE bbb-apps-akka bbb-fsesl-akka bbb-rap-resque-worker.service bbb-rap-starter.service bbb-rap-caption-inbox.service $HTML5 $WEBHOOKS $PADS $ETHERPAD $BBB_WEB $BBB_LTI + systemctl stop $TOMCAT_SERVICE bigbluebutton.service } start_bigbluebutton () { @@ -389,41 +365,12 @@ start_bigbluebutton () { fi echo "Starting BigBlueButton" - if [ -f /usr/lib/systemd/system/bbb-html5.service ]; then - HTML5="mongod bbb-html5 bbb-webrtc-sfu kurento-media-server" - fi - - if [ -f /usr/lib/systemd/system/bbb-webhooks.service ]; then - WEBHOOKS=bbb-webhooks - fi - - if [ -f /usr/lib/systemd/system/bbb-pads.service ]; then - PADS=bbb-pads - fi - - if [ -f /usr/share/etherpad-lite/settings.json ]; then - ETHERPAD=etherpad - fi - - if [ -f /lib/systemd/system/bbb-web.service ]; then - BBB_WEB=bbb-web - fi - - if [ -f /usr/share/bbb-lti/WEB-INF/classes/lti-config.properties ]; then - BBB_LTI=bbb-lti - fi if systemctl list-units --full -all | grep -q $TOMCAT_USER.service; then TOMCAT_SERVICE=$TOMCAT_USER fi - systemctl start $TOMCAT_SERVICE nginx freeswitch $REDIS_SERVICE bbb-apps-akka bbb-fsesl-akka bbb-rap-resque-worker bbb-rap-starter.service bbb-rap-caption-inbox.service $HTML5 $WEBHOOKS $ETHERPAD $PADS $BBB_WEB $BBB_LTI - - if [ -f /usr/lib/systemd/system/bbb-html5.service ]; then - systemctl start mongod - sleep 3 - systemctl start bbb-html5 - fi + systemctl start $TOMCAT_SERVICE bigbluebutton.target } display_bigbluebutton_status () { From 91c882862e6879e397967f6bef802cb5b051ee3e Mon Sep 17 00:00:00 2001 From: Daniel Molkentin Date: Thu, 17 Mar 2022 16:06:42 +0100 Subject: [PATCH 047/268] fix paste error --- build/packages-template/bbb-etherpad/etherpad.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/packages-template/bbb-etherpad/etherpad.service b/build/packages-template/bbb-etherpad/etherpad.service index c1f01089ae..fa408bf374 100644 --- a/build/packages-template/bbb-etherpad/etherpad.service +++ b/build/packages-template/bbb-etherpad/etherpad.service @@ -2,7 +2,7 @@ Description=Etherpad Server Wants=redis-server.service After=syslog.target network.target -packages-template/bbb-config/build.sh +PartOf=bigbluebutton.target [Service] Type=simple From 3c83c8f1e91b0911da29b2cf54f19ccf4f70effe Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Tue, 5 Apr 2022 13:01:26 +0000 Subject: [PATCH 048/268] Refactor for SonarCloud --- export-annotations/master.js | 11 ++- export-annotations/workers/collector.js | 8 +- export-annotations/workers/process.js | 98 ++++++++++--------------- 3 files changed, 51 insertions(+), 66 deletions(-) diff --git a/export-annotations/master.js b/export-annotations/master.js index 240d7419cb..fb8d60a5ed 100644 --- a/export-annotations/master.js +++ b/export-annotations/master.js @@ -2,7 +2,7 @@ const Logger = require('./lib/utils/logger'); const config = require('./config'); const fs = require('fs'); const redis = require('redis'); -const { Worker } = require('worker_threads') +const { Worker } = require('worker_threads'); const logger = new Logger('presAnn Master'); logger.info("Running export-annotations"); @@ -23,6 +23,11 @@ function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } +async function redisAlive(client) { + let ping = await client.ping(); + return (ping === "PONG"); +} + (async () => { const client = redis.createClient({ host: config.redis.host, @@ -31,14 +36,12 @@ function sleep(ms) { }); client.on('error', (err) => logger.info('Redis Client Error', err)); - await client.connect(); - while (true) { + while (redisAlive(client)) { await sleep(config.redis.interval); let job = await client.LPOP(config.redis.channels.queue) - const exportJob = JSON.parse(job); if(job != null) { diff --git a/export-annotations/workers/collector.js b/export-annotations/workers/collector.js index d32b89f4c5..f9871a3adc 100644 --- a/export-annotations/workers/collector.js +++ b/export-annotations/workers/collector.js @@ -51,10 +51,10 @@ let exportJob = JSON.parse(job); }); // Collect the Presentation Page files from the presentation directory - for (let i = 0; i < pages.length; i++) { - let pageNumber = pages[i].page - let slide = `${exportJob.presLocation}/svgs/slide${pageNumber}.svg` - let file = `${dropbox}/slide${pageNumber}.svg` + for (let p of pages) { + let pageNumber = p.page; + let slide = `${exportJob.presLocation}/svgs/slide${pageNumber}.svg`; + let file = `${dropbox}/slide${pageNumber}.svg`; fs.copyFile(slide, file, (err) => { if (err) throw err; } ); } diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js index c22bb67251..dc3e834868 100644 --- a/export-annotations/workers/process.js +++ b/export-annotations/workers/process.js @@ -34,7 +34,7 @@ function scale_shape(dimension, coord) { } function render_HTMLTextBox(htmlFilePath, id, width, height) { - commands = [ + let commands = [ 'wkhtmltoimage', '--format', 'png', '--encoding', `${config.process.whiteboardTextEncoding}`, @@ -54,7 +54,6 @@ function render_HTMLTextBox(htmlFilePath, id, width, height) { if (stderr) { logger.error(`stderr when rendering text box for string "${string}" with wkhtmltoimage: ${stderr}`); - return; } }) } @@ -82,12 +81,12 @@ function overlay_ellipse(svg, annotation, w, h) { [y1, y2] = [y2, y1] } - path = `M${x1} ${hy} - A${width_r} ${height_r} 0 0 1 ${hx} ${y1} - A${width_r} ${height_r} 0 0 1 ${x2} ${hy} - A${width_r} ${height_r} 0 0 1 ${hx} ${y2} - A${width_r} ${height_r} 0 0 1 ${x1} ${hy} - Z` + let path = `M${x1} ${hy} + A${width_r} ${height_r} 0 0 1 ${hx} ${y1} + A${width_r} ${height_r} 0 0 1 ${x2} ${hy} + A${width_r} ${height_r} 0 0 1 ${hx} ${y2} + A${width_r} ${height_r} 0 0 1 ${x1} ${hy} + Z` svg.ele('g', { style: `stroke:#${shapeColor};stroke-width:${scale_shape(w, annotation.thickness)}; @@ -115,7 +114,6 @@ function overlay_pencil(svg, annotation, w, h) { if (annotation.points.length < 2) { logger.info("Pencil doesn't have enough points") - return; } else if (annotation.points.length == 2) { @@ -129,30 +127,31 @@ function overlay_pencil(svg, annotation, w, h) { } else { + let x; + let y; let path = "" let dataPoints = annotation.points - for(let i = 0; i < annotation.commands.length; i++) { - switch(annotation.commands[i]){ + for(let command of annotation.commands) { + switch(command){ case 1: // MOVE TO - var x = scale_shape(w, dataPoints.shift()) - var y = scale_shape(h, dataPoints.shift()) + x = scale_shape(w, dataPoints.shift()) + y = scale_shape(h, dataPoints.shift()) path = `${path} M${x} ${y}` break; case 2: // LINE TO - var x = scale_shape(w, dataPoints.shift()) - var y = scale_shape(h, dataPoints.shift()) + x = scale_shape(w, dataPoints.shift()) + y = scale_shape(h, dataPoints.shift()) path = `${path} L${x} ${y}` break; case 4: // C_CURVE_TO - var cx1 = scale_shape(w, dataPoints.shift()) - var cy1 = scale_shape(h, dataPoints.shift()) - var cx2 = scale_shape(w, dataPoints.shift()) - var cy2 = scale_shape(h, dataPoints.shift()) - var x = scale_shape(w, dataPoints.shift()) - var y = scale_shape(h, dataPoints.shift()) + let cx1 = scale_shape(w, dataPoints.shift()) + let cy1 = scale_shape(h, dataPoints.shift()) + let cx2 = scale_shape(w, dataPoints.shift()) + let cy2 = scale_shape(h, dataPoints.shift()) + x = scale_shape(w, dataPoints.shift()) + y = scale_shape(h, dataPoints.shift()) path = `${path} C${cx1} ${cy1},${cx2} ${cy2},${x} ${y}` - break; default: logger.error(`Unknown pencil command: ${annotation.commands[i]}`) @@ -204,9 +203,9 @@ function overlay_poll(svg, annotation, w, h) { logger.error(`Poll generation failed with error: ${error.message}`); return; } + if (stderr) { logger.error(`Poll generation failed with stderr: ${stderr}`); - return; } }); @@ -301,45 +300,32 @@ function overlay_text(svg, annotation, w, h) { }).up(); } -function overlay_line(svg, annotation, w, h) { - let shapeColor = color_to_hex(annotation.color); - - svg.ele('g', { - style: `stroke:#${shapeColor};stroke-width:${scale_shape(w, annotation.thickness)};stroke-linecap:butt` - }).ele('line', { - x1: scale_shape(w, annotation.points[0]), - y1: scale_shape(h, annotation.points[1]), - x2: scale_shape(w, annotation.points[2]), - y2: scale_shape(h, annotation.points[3]), - }).up() -} - -function overlay_annotations(svg, annotations, w, h) { - for(let i = 0; i < annotations.length; i++) { - switch (annotations[i].annotationType) { +function overlay_annotations(svg, currentSlideAnnotations, w, h) { + for(let annotation of currentSlideAnnotations) { + switch (annotation.annotationType) { case 'ellipse': - overlay_ellipse(svg, annotations[i].annotationInfo, w, h); + overlay_ellipse(svg, annotation.annotationInfo, w, h); break; case 'line': - overlay_line(svg, annotations[i].annotationInfo, w, h); + overlay_line(svg, annotation.annotationInfo, w, h); break; case 'poll_result': - overlay_poll(svg, annotations[i].annotationInfo, w, h); + overlay_poll(svg, annotation.annotationInfo, w, h); break; case 'pencil': - overlay_pencil(svg, annotations[i].annotationInfo, w, h); + overlay_pencil(svg, annotation.annotationInfo, w, h); break; case 'rectangle': - overlay_rectangle(svg, annotations[i].annotationInfo, w, h); + overlay_rectangle(svg, annotation.annotationInfo, w, h); break; case 'text': - overlay_text(svg, annotations[i].annotationInfo, w, h); + overlay_text(svg, annotation.annotationInfo, w, h); break; case 'triangle': - overlay_triangle(svg, annotations[i].annotationInfo, w, h); + overlay_triangle(svg, annotation.annotationInfo, w, h); break; default: - logger.error(`Unknown annotation type ${annotations[i].annotationType}.`); + logger.error(`Unknown annotation type ${annotation.annotationType}.`); } } } @@ -358,11 +344,8 @@ let pages = JSON.parse(whiteboard.pages); let rsvgConvertInput = "" // 3. Convert annotations to SVG -for (let i = 0; i < pages.length; i++) { - - // Get the current slide (without annotations) - let currentSlide = pages[i] - var backgroundSlide = fs.readFileSync(`${dropbox}/slide${pages[i].page}.svg`).toString(); +for (let currentSlide of pages) { + var backgroundSlide = fs.readFileSync(`${dropbox}/slide${currentSlide.page}.svg`).toString(); // Read background slide in as JSON to determine dimensions // TODO: find a better way to get width and height of slide (e.g. as part of message) backgroundSlide = JSON.parse(convert.xml2json(backgroundSlide)); @@ -370,8 +353,8 @@ for (let i = 0; i < pages.length; i++) { // There's a bug with older versions of rsvg which defaults SVG output to pixels. // So we ignore the units here as well. // See: https://gitlab.gnome.org/GNOME/librsvg/-/issues/766 - var slideWidth = Number(backgroundSlide.elements[0].attributes.width.replace(/\D/g, "")) - var slideHeight = Number(backgroundSlide.elements[0].attributes.height.replace(/\D/g, "")) + var slideWidth = Number(backgroundSlide.elements[0].attributes.width.replace(/[^\d.]/g, '')) + var slideHeight = Number(backgroundSlide.elements[0].attributes.height.replace(/[^\d.]/g, '')) var panzoom_x = -currentSlide.xOffset * MAGIC_MYSTERY_NUMBER / 100.0 * slideWidth var panzoom_y = -currentSlide.yOffset * MAGIC_MYSTERY_NUMBER / 100.0 * slideHeight @@ -392,7 +375,7 @@ for (let i = 0; i < pages.length; i++) { sysID: 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' }) .ele('image', { - 'xlink:href': `file://${dropbox}/slide${pages[i].page}.svg`, + 'xlink:href': `file://${dropbox}/slide${currentSlide.page}.svg`, width: slideWidth, height: slideHeight, }) @@ -403,11 +386,11 @@ for (let i = 0; i < pages.length; i++) { // 4. Overlay annotations onto slides // Based on /record-and-playback/presentation/scripts/publish/presentation.rb - overlay_annotations(svg, pages[i].annotations, slideWidth, slideHeight) + overlay_annotations(svg, currentSlide.annotations, slideWidth, slideHeight) svg = svg.end({ prettyPrint: true }); // Write annotated SVG file - let file = `${dropbox}/annotated-slide${pages[i].page}.svg` + let file = `${dropbox}/annotated-slide${currentSlide.page}.svg` fs.writeFileSync(file, svg, function(err) { if(err) { return logger.error(err); } }); @@ -433,7 +416,6 @@ execSync(render, (error, stderr) => { } if (stderr) { logger.error(`SVG to PDF export failed with stderr: ${stderr}`); - return; } }); From aa956919c2cffa153be5615ba9bd0d99102734e0 Mon Sep 17 00:00:00 2001 From: Paul Trudel Date: Fri, 8 Apr 2022 18:46:54 +0000 Subject: [PATCH 049/268] Updated recording changes for 2.6 --- bbb-common-web/.env | 5 + bbb-common-web/build.sbt | 12 + bbb-common-web/docker-clean.sh | 2 + bbb-common-web/docker-compose.yml | 14 + bbb-common-web/hibernate-cfg.sh | 31 + bbb-common-web/psql.sh | 12 + .../java/db/migration/V1__Initial_create.sql | 75 ++ .../org/bigbluebutton/api/MeetingService.java | 24 +- .../bigbluebutton/api/RecordingService.java | 713 +---------------- .../api/model/entity/CallbackData.java | 117 +++ .../api/model/entity/Metadata.java | 90 +++ .../api/model/entity/PlaybackFormat.java | 140 ++++ .../api/model/entity/Recording.java | 268 +++++++ .../api/model/entity/Thumbnail.java | 132 ++++ .../bigbluebutton/api/service/XmlService.java | 19 + .../service/impl/RecordingServiceDbImpl.java | 247 ++++++ .../impl/RecordingServiceFileImpl.java | 718 ++++++++++++++++++ .../api/service/impl/XmlServiceImpl.java | 583 ++++++++++++++ .../org/bigbluebutton/api/util/DataStore.java | 234 ++++++ bbb-recording-imex/README.md | 33 + bbb-recording-imex/get-recordings.sh | 50 ++ bbb-recording-imex/pom.xml | 111 +++ bbb-recording-imex/run.sh | 2 + .../java/org/bigbluebutton/RecordingApp.java | 105 +++ .../bigbluebutton/RecordingExportHandler.java | 120 +++ .../bigbluebutton/RecordingImportHandler.java | 93 +++ .../src/main/resources/logback.xml | 47 ++ .../RecordingImportHandlerTest.java | 43 ++ .../org/bigbluebutton/RecordingStoreTest.java | 80 ++ bigbluebutton-web/build.gradle | 6 +- .../grails-app/conf/spring/resources.xml | 2 +- .../controllers/RecordingController.groovy | 16 +- 32 files changed, 3452 insertions(+), 692 deletions(-) create mode 100644 bbb-common-web/.env create mode 100644 bbb-common-web/docker-clean.sh create mode 100644 bbb-common-web/docker-compose.yml create mode 100644 bbb-common-web/hibernate-cfg.sh create mode 100644 bbb-common-web/psql.sh create mode 100755 bbb-common-web/src/main/java/db/migration/V1__Initial_create.sql create mode 100755 bbb-common-web/src/main/java/org/bigbluebutton/api/model/entity/CallbackData.java create mode 100755 bbb-common-web/src/main/java/org/bigbluebutton/api/model/entity/Metadata.java create mode 100755 bbb-common-web/src/main/java/org/bigbluebutton/api/model/entity/PlaybackFormat.java create mode 100755 bbb-common-web/src/main/java/org/bigbluebutton/api/model/entity/Recording.java create mode 100755 bbb-common-web/src/main/java/org/bigbluebutton/api/model/entity/Thumbnail.java create mode 100755 bbb-common-web/src/main/java/org/bigbluebutton/api/service/XmlService.java create mode 100755 bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/RecordingServiceDbImpl.java create mode 100755 bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/RecordingServiceFileImpl.java create mode 100755 bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/XmlServiceImpl.java create mode 100755 bbb-common-web/src/main/java/org/bigbluebutton/api/util/DataStore.java create mode 100644 bbb-recording-imex/README.md create mode 100755 bbb-recording-imex/get-recordings.sh create mode 100755 bbb-recording-imex/pom.xml create mode 100755 bbb-recording-imex/run.sh create mode 100755 bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingApp.java create mode 100755 bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingExportHandler.java create mode 100755 bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingImportHandler.java create mode 100755 bbb-recording-imex/src/main/resources/logback.xml create mode 100755 bbb-recording-imex/src/test/java/org/bigbluebutton/RecordingImportHandlerTest.java create mode 100755 bbb-recording-imex/src/test/java/org/bigbluebutton/RecordingStoreTest.java diff --git a/bbb-common-web/.env b/bbb-common-web/.env new file mode 100644 index 0000000000..b84c94e3b5 --- /dev/null +++ b/bbb-common-web/.env @@ -0,0 +1,5 @@ +POSTGRES_VERSION=14.1 +POSTGRES_USER= +POSTGRES_PASSWORD= +HOST_PORT=5432 +CONTAINER_PORT=5432 diff --git a/bbb-common-web/build.sbt b/bbb-common-web/build.sbt index 293895cd7a..0c50dfe933 100755 --- a/bbb-common-web/build.sbt +++ b/bbb-common-web/build.sbt @@ -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" +) diff --git a/bbb-common-web/docker-clean.sh b/bbb-common-web/docker-clean.sh new file mode 100644 index 0000000000..864d9c5b3d --- /dev/null +++ b/bbb-common-web/docker-clean.sh @@ -0,0 +1,2 @@ +#!/bin/bash +docker rm -f $(docker ps -aq) diff --git a/bbb-common-web/docker-compose.yml b/bbb-common-web/docker-compose.yml new file mode 100644 index 0000000000..5fc7cfe6c0 --- /dev/null +++ b/bbb-common-web/docker-compose.yml @@ -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" diff --git a/bbb-common-web/hibernate-cfg.sh b/bbb-common-web/hibernate-cfg.sh new file mode 100644 index 0000000000..41ab1220f5 --- /dev/null +++ b/bbb-common-web/hibernate-cfg.sh @@ -0,0 +1,31 @@ +#!/bin/bash +. .env +mkdir -p ./src/main/resources +echo ' + + + + org.postgresql.Driver + jdbc:postgresql://localhost:'"$HOST_PORT"'/bbb + '"$POSTGRES_USER"' + '"$POSTGRES_PASSWORD"' + + com.zaxxer.hikari.hibernate.HikariConnectionProvider + 5 + 10 + 30000 + + org.hibernate.dialect.PostgreSQL10Dialect + + true + + thread + false + + false + + false + + ' > ./src/main/resources/hibernate.cfg.xml diff --git a/bbb-common-web/psql.sh b/bbb-common-web/psql.sh new file mode 100644 index 0000000000..f69143c3de --- /dev/null +++ b/bbb-common-web/psql.sh @@ -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 diff --git a/bbb-common-web/src/main/java/db/migration/V1__Initial_create.sql b/bbb-common-web/src/main/java/db/migration/V1__Initial_create.sql new file mode 100755 index 0000000000..6acb1cb34b --- /dev/null +++ b/bbb-common-web/src/main/java/db/migration/V1__Initial_create.sql @@ -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; \ No newline at end of file diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java index a1c3f5c141..d69129b28f 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java @@ -78,6 +78,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); @@ -572,8 +574,26 @@ public class MeetingService implements MessageListener { return recordingService.isRecordingExist(recordId); } - public String getRecordings2x(List idList, List states, Map metadataFilters) { - return recordingService.getRecordings2x(idList, states, metadataFilters); + public String getRecordings2x(List idList, List states, Map 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 idList) { diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/RecordingService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/RecordingService.java index 04345dc132..b08358368b 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/RecordingService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/RecordingService.java @@ -19,696 +19,39 @@ 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 idList, List states, Map metadataFilters, Pageable pageable); + boolean existAnyRecording(List idList); + boolean changeState(String recordingId, String state); + void updateMetaParams(List recordIDs, Map 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 Page recordingListToPage(List recordings, Pageable pageable) { + int start = (int) pageable.getOffset(); + int end = (int) (Math.min((start + pageable.getPageSize()), recordings.size())); + + Page recordingsPage = new PageImpl<>(recordings.subList(start, end), pageable, recordings.size()); + return recordingsPage; } - - 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 getRecordingsMetadata(List recordIDs, List states) { - List recs = new ArrayList<>(); - - Map> allDirectories = getAllDirectories(states); - if (recordIDs.isEmpty()) { - for (Map.Entry> entry : allDirectories.entrySet()) { - recordIDs.addAll(getAllRecordingIds(entry.getValue())); - } - } - - for (String recordID : recordIDs) { - for (Map.Entry> entry : allDirectories.entrySet()) { - List _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 idList, List states, Map metadataFilters) { - List recsList = getRecordingsMetadata(idList, states); - ArrayList 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 metadataFilters) { - boolean matchesMetadata = true; - Map recMeta = recording.getMeta(); - for (Map.Entry 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 filterRecordingsByMetadata(List recordings, Map metadataFilters) { - ArrayList resultRecordings = new ArrayList<>(); - for (RecordingMetadata entry : recordings) { - if (recordingMatchesMetadata(entry, metadataFilters)) - resultRecordings.add(entry); - } - return resultRecordings; - } - - private ArrayList getAllRecordingsFor(String recordId) { - String[] format = getPlaybackFormats(publishedDir); - ArrayList ids = new ArrayList(); - - for (int i = 0; i < format.length; i++) { - List 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 publishList = getAllRecordingIds(publishedDir); - List unpublishList = getAllRecordingIds(unpublishedDir); - if (publishList.contains(recordId) || unpublishList.contains(recordId)) { - return true; - } - - return false; - } - - public boolean existAnyRecording(List idList) { - List publishList = getAllRecordingIds(publishedDir); - List unpublishList = getAllRecordingIds(unpublishedDir); - - for (String id : idList) { - if (publishList.contains(id) || unpublishList.contains(id)) { - return true; - } - } - return false; - } - - private List getAllRecordingIds(String path) { - String[] format = getPlaybackFormats(path); - - return getAllRecordingIds(path, format); - } - - private List getAllRecordingIds(String path, String[] format) { - List ids = new ArrayList<>(); - - for (String aFormat : format) { - List recordings = getDirectories(path + File.separatorChar + aFormat); - for (File recording : recordings) { - if (!ids.contains(recording.getName())) { - ids.add(recording.getName()); - } - } - } - - return ids; - } - - private Set getAllRecordingIds(List recs) { - Set ids = new HashSet<>(); - - Iterator iterator = recs.iterator(); - while (iterator.hasNext()) { - ids.add(iterator.next().getName()); - } - - return ids; - } - - private List getRecordingsForPath(String id, List recordings) { - List recs = new ArrayList<>(); - - Iterator 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 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 getDirectories(String path) { - List files = new ArrayList<>(); - try { - DirectoryStream stream = Files.newDirectoryStream(FileSystems.getDefault().getPath(path)); - Iterator 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 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 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 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 getAllDirectories(String state) { - List 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> getAllDirectories(List states) { - Map> allDirectories = new HashMap<>(); - - if ( shouldIncludeState(states, Recording.STATE_PUBLISHED) ) { - List listedDirectories = getAllDirectories(Recording.STATE_PUBLISHED); - allDirectories.put(Recording.STATE_PUBLISHED, listedDirectories); - } - - if ( shouldIncludeState(states, Recording.STATE_UNPUBLISHED) ) { - List listedDirectories = getAllDirectories(Recording.STATE_UNPUBLISHED); - allDirectories.put(Recording.STATE_UNPUBLISHED, listedDirectories); - } - - if ( shouldIncludeState(states, Recording.STATE_DELETED) ) { - List listedDirectories = getAllDirectories(Recording.STATE_DELETED); - allDirectories.put(Recording.STATE_DELETED, listedDirectories); - } - - if ( shouldIncludeState(states, Recording.STATE_PROCESSING) ) { - List listedDirectories = getAllDirectories(Recording.STATE_PROCESSING); - allDirectories.put(Recording.STATE_PROCESSING, listedDirectories); - } - - if ( shouldIncludeState(states, Recording.STATE_PROCESSED) ) { - List listedDirectories = getAllDirectories(Recording.STATE_PROCESSED); - allDirectories.put(Recording.STATE_PROCESSED, listedDirectories); - } - - return allDirectories; - } - - public void updateMetaParams(List recordIDs, Map metaParams) { - // Define the directories used to lookup the recording - List 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> allDirectories = getAllDirectories(states); - - // Retrieve the actual recording from the directories gathered for the lookup - for (String recordID : recordIDs) { - for (Map.Entry> entry : allDirectories.entrySet()) { - List 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 metaParams, File destMetadataXml) { - RecordingMetadata rec = recordingServiceHelper.getRecordingMetadata(srxMetadataXml); - - Map recMeta = rec.getMeta(); - - if (rec != null && !recMeta.isEmpty()) { - for (Map.Entry 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 indexRecordings(List recs) { - Map indexedRecs = new HashMap<>(); - - Iterator 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/"; - } - -} +} \ No newline at end of file diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/model/entity/CallbackData.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/model/entity/CallbackData.java new file mode 100755 index 0000000000..68fecd121d --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/model/entity/CallbackData.java @@ -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 "); + } +} \ No newline at end of file diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/model/entity/Metadata.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/model/entity/Metadata.java new file mode 100755 index 0000000000..439e2c9376 --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/model/entity/Metadata.java @@ -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 "); + } +} \ No newline at end of file diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/model/entity/PlaybackFormat.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/model/entity/PlaybackFormat.java new file mode 100755 index 0000000000..bd056184cf --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/model/entity/PlaybackFormat.java @@ -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 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 getThumbnails() { return thumbnails; } + + public void setThumbnails(Set 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 "); + } +} \ No newline at end of file diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/model/entity/Recording.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/model/entity/Recording.java new file mode 100755 index 0000000000..c188e48767 --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/model/entity/Recording.java @@ -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; + + @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 getMetadata() { return metadata; } + + public void setMetadata(Set 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 "); + } +} \ No newline at end of file diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/model/entity/Thumbnail.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/model/entity/Thumbnail.java new file mode 100755 index 0000000000..7fbfb8a7ce --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/model/entity/Thumbnail.java @@ -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 { + + @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 "); + } +} \ No newline at end of file diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/service/XmlService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/XmlService.java new file mode 100755 index 0000000000..c179e6942f --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/XmlService.java @@ -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 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); +} diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/RecordingServiceDbImpl.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/RecordingServiceDbImpl.java new file mode 100755 index 0000000000..dcc6bb51d4 --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/RecordingServiceDbImpl.java @@ -0,0 +1,247 @@ +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(); + xmlService = new XmlServiceImpl(); + } + + @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 idList, List states, Map metadataFilters, Pageable pageable) { + logger.info("Retrieving all recordings"); + Set recordings = new HashSet<>(); + recordings.addAll(dataStore.findAll(Recording.class)); + + Set recordingsById = new HashSet<>(); + for(String id: idList) { + List 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 recordingsByState = new HashSet<>(); + for(String state: states) { + List 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 = new ArrayList<>(); + for(Map.Entry metadataFilter: metadataFilters.entrySet()) { + List m = dataStore.findMetadataByFilter(metadataFilter.getKey(), metadataFilter.getValue()); + if(m != null) metadata.addAll(m); + } + + Set 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 recordingsPage = recordingListToPage(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 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 recordIDs, Map metaParams) { + Set recordings = new HashSet<>(); + for(String id: recordIDs) { + Recording recording = dataStore.findRecordingByRecordId(id); + if(recording != null) recordings.add(recording); + } + + for(Recording recording: recordings) { + Set metadata = recording.getMetadata(); + + for(Map.Entry 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; + } +} \ No newline at end of file diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/RecordingServiceFileImpl.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/RecordingServiceFileImpl.java new file mode 100755 index 0000000000..74e91c0e29 --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/RecordingServiceFileImpl.java @@ -0,0 +1,718 @@ +/** + * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ + * + * Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below). + * + * This program is free software; you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation; either version 3.0 of the License, or (at your option) any later + * version. + * + * BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with BigBlueButton; if not, see . + * + */ + +package org.bigbluebutton.api.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.RecordingService; +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 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 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 getRecordingsMetadata(List recordIDs, List states) { + List recs = new ArrayList<>(); + + Map> allDirectories = getAllDirectories(states); + if (recordIDs.isEmpty()) { + for (Map.Entry> entry : allDirectories.entrySet()) { + recordIDs.addAll(getAllRecordingIds(entry.getValue())); + } + } + + for (String recordID : recordIDs) { + for (Map.Entry> entry : allDirectories.entrySet()) { + List _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 idList, List states, Map metadataFilters, Pageable pageable) { + List recsList = getRecordingsMetadata(idList, states); + ArrayList 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 metadataFilters) { + boolean matchesMetadata = true; + Map recMeta = recording.getMeta(); + for (Map.Entry 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 filterRecordingsByMetadata(List recordings, Map metadataFilters) { + ArrayList resultRecordings = new ArrayList<>(); + for (RecordingMetadata entry : recordings) { + if (recordingMatchesMetadata(entry, metadataFilters)) + resultRecordings.add(entry); + } + return resultRecordings; + } + + private ArrayList getAllRecordingsFor(String recordId) { + String[] format = getPlaybackFormats(publishedDir); + ArrayList ids = new ArrayList(); + + for (int i = 0; i < format.length; i++) { + List 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 publishList = getAllRecordingIds(publishedDir); + List unpublishList = getAllRecordingIds(unpublishedDir); + if (publishList.contains(recordId) || unpublishList.contains(recordId)) { + return true; + } + + return false; + } + + public boolean existAnyRecording(List idList) { + List publishList = getAllRecordingIds(publishedDir); + List unpublishList = getAllRecordingIds(unpublishedDir); + + for (String id : idList) { + if (publishList.contains(id) || unpublishList.contains(id)) { + return true; + } + } + return false; + } + + private List getAllRecordingIds(String path) { + String[] format = getPlaybackFormats(path); + + return getAllRecordingIds(path, format); + } + + private List getAllRecordingIds(String path, String[] format) { + List ids = new ArrayList<>(); + + for (String aFormat : format) { + List recordings = getDirectories(path + File.separatorChar + aFormat); + for (File recording : recordings) { + if (!ids.contains(recording.getName())) { + ids.add(recording.getName()); + } + } + } + + return ids; + } + + private Set getAllRecordingIds(List recs) { + Set ids = new HashSet<>(); + + Iterator iterator = recs.iterator(); + while (iterator.hasNext()) { + ids.add(iterator.next().getName()); + } + + return ids; + } + + private List getRecordingsForPath(String id, List recordings) { + List recs = new ArrayList<>(); + + Iterator 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 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 getDirectories(String path) { + List files = new ArrayList<>(); + try { + DirectoryStream stream = Files.newDirectoryStream(FileSystems.getDefault().getPath(path)); + Iterator 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 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 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 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 getAllDirectories(String state) { + List 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> getAllDirectories(List states) { + Map> allDirectories = new HashMap<>(); + + if ( shouldIncludeState(states, Recording.STATE_PUBLISHED) ) { + List listedDirectories = getAllDirectories(Recording.STATE_PUBLISHED); + allDirectories.put(Recording.STATE_PUBLISHED, listedDirectories); + } + + if ( shouldIncludeState(states, Recording.STATE_UNPUBLISHED) ) { + List listedDirectories = getAllDirectories(Recording.STATE_UNPUBLISHED); + allDirectories.put(Recording.STATE_UNPUBLISHED, listedDirectories); + } + + if ( shouldIncludeState(states, Recording.STATE_DELETED) ) { + List listedDirectories = getAllDirectories(Recording.STATE_DELETED); + allDirectories.put(Recording.STATE_DELETED, listedDirectories); + } + + if ( shouldIncludeState(states, Recording.STATE_PROCESSING) ) { + List listedDirectories = getAllDirectories(Recording.STATE_PROCESSING); + allDirectories.put(Recording.STATE_PROCESSING, listedDirectories); + } + + if ( shouldIncludeState(states, Recording.STATE_PROCESSED) ) { + List listedDirectories = getAllDirectories(Recording.STATE_PROCESSED); + allDirectories.put(Recording.STATE_PROCESSED, listedDirectories); + } + + return allDirectories; + } + + public void updateMetaParams(List recordIDs, Map metaParams) { + // Define the directories used to lookup the recording + List 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> allDirectories = getAllDirectories(states); + + // Retrieve the actual recording from the directories gathered for the lookup + for (String recordID : recordIDs) { + for (Map.Entry> entry : allDirectories.entrySet()) { + List 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 metaParams, File destMetadataXml) { + RecordingMetadata rec = recordingServiceHelper.getRecordingMetadata(srxMetadataXml); + + Map recMeta = rec.getMeta(); + + if (rec != null && !recMeta.isEmpty()) { + for (Map.Entry 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 indexRecordings(List recs) { + Map indexedRecs = new HashMap<>(); + + Iterator 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/"; + } + +} + diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/XmlServiceImpl.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/XmlServiceImpl.java new file mode 100755 index 0000000000..cc93003c66 --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/XmlServiceImpl.java @@ -0,0 +1,583 @@ +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.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 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 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.getNumberOfElements())); + pagination.appendChild(totalElements); + + Element last = createElement(document, "last", String.valueOf(page.isLast())); + pagination.appendChild(last); + + Element totalPages = createElement(document, "totalPages", + String.valueOf(Math.ceil(page.getNumberOfElements() / page.getSize()))); + 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); + + Sort sort = pageable.getSort(); + Element sortElement = createElement(document, "sort", null); + logger.info("Sort {}", sort); + + 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 + } +} \ No newline at end of file diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/util/DataStore.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/util/DataStore.java new file mode 100755 index 0000000000..965db5261d --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/util/DataStore.java @@ -0,0 +1,234 @@ +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 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 find(String id, Class 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 List findAll(Class entityClass) { + logger.info("Attempting to fetch all {}", entityClass.getSimpleName()); + Session session = sessionFactory.openSession(); + Transaction transaction = null; + List result = null; + + try { + transaction = session.beginTransaction(); + CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder(); + CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(entityClass); + Root root = criteriaQuery.from(entityClass); + CriteriaQuery 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 criteriaQuery = criteriaBuilder.createQuery(Recording.class); + Root 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 findRecordingsByState(String state) { + logger.info("Attempting to find recordings with state {}", state); + Session session = sessionFactory.openSession(); + Transaction transaction = null; + List result = null; + + try { + transaction = session.beginTransaction(); + CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder(); + CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Recording.class); + Root 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 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 result = null; + + try { + transaction = session.beginTransaction(); + CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder(); + CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Metadata.class); + Root 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 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 recordings = findAll(Recording.class); + + if(recordings != null) { + for(Recording recording: recordings) { + delete(recording); + } + } + } +} \ No newline at end of file diff --git a/bbb-recording-imex/README.md b/bbb-recording-imex/README.md new file mode 100644 index 0000000000..e23b020762 --- /dev/null +++ b/bbb-recording-imex/README.md @@ -0,0 +1,33 @@ +# BBB-Recording-Importer + +Imports and parses recordings 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 the generates 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-importer + - 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" + - To use the main program compile it with "mvn package" which will generate two jars in the target directory + - Run the program with the run.sh script + + +## 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 diff --git a/bbb-recording-imex/get-recordings.sh b/bbb-recording-imex/get-recordings.sh new file mode 100755 index 0000000000..73383573f2 --- /dev/null +++ b/bbb-recording-imex/get-recordings.sh @@ -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="https://bbb-dev-01.test/" +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" diff --git a/bbb-recording-imex/pom.xml b/bbb-recording-imex/pom.xml new file mode 100755 index 0000000000..66f9818417 --- /dev/null +++ b/bbb-recording-imex/pom.xml @@ -0,0 +1,111 @@ + + + 4.0.0 + + org.bigbluebutton + bbb-recording-imex + 1.0-SNAPSHOT + + + 11 + 11 + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + + shade + + + true + + + org.bigbluebutton.recording.RecordingApp + + + + + + + + maven-surefire-plugin + 2.22.2 + + + maven-failsafe-plugin + 2.22.2 + + + net.revelc.code.formatter + formatter-maven-plugin + 2.16.0 + + + + format + + + + + 1.8 + 1.8 + 1.8 + LF + UTF-8 + + + + + + + + + org.bigbluebutton + bbb-common-web + 0.0.3-SNAPSHOT + + + + ch.qos.logback + logback-core + 1.2.3 + + + org.slf4j + slf4j-api + 1.7.30 + + + ch.qos.logback + logback-classic + 1.2.3 + + + + org.junit.jupiter + junit-jupiter-api + 5.7.2 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.7.2 + test + + + org.junit.platform + junit-platform-console-standalone + 1.8.2 + + + + + \ No newline at end of file diff --git a/bbb-recording-imex/run.sh b/bbb-recording-imex/run.sh new file mode 100755 index 0000000000..48ed1fef2b --- /dev/null +++ b/bbb-recording-imex/run.sh @@ -0,0 +1,2 @@ +#!/bin/bash +java -jar target/bbb-recording-imex-1.0-SNAPSHOT-shaded.jar diff --git a/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingApp.java b/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingApp.java new file mode 100755 index 0000000000..9e557ba70a --- /dev/null +++ b/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingApp.java @@ -0,0 +1,105 @@ +package org.bigbluebutton; + +import java.io.Console; +import java.util.stream.IntStream; + +public class RecordingApp { + + public static void main(String[] args) { + 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); + } +} \ No newline at end of file diff --git a/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingExportHandler.java b/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingExportHandler.java new file mode 100755 index 0000000000..9e6c9ceadd --- /dev/null +++ b/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingExportHandler.java @@ -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 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(); + } + } +} diff --git a/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingImportHandler.java b/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingImportHandler.java new file mode 100755 index 0000000000..a59120ac6f --- /dev/null +++ b/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingImportHandler.java @@ -0,0 +1,93 @@ +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; + } +} diff --git a/bbb-recording-imex/src/main/resources/logback.xml b/bbb-recording-imex/src/main/resources/logback.xml new file mode 100755 index 0000000000..aa20308c50 --- /dev/null +++ b/bbb-recording-imex/src/main/resources/logback.xml @@ -0,0 +1,47 @@ + + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + + + ${HOME_LOG} + + + logs/archived/app.%d{yyyy-MM-dd}.%i.log.gz + + 10MB + + 20GB + + 60 + + + + %d %p %c{1.} [%t] %m%n + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bbb-recording-imex/src/test/java/org/bigbluebutton/RecordingImportHandlerTest.java b/bbb-recording-imex/src/test/java/org/bigbluebutton/RecordingImportHandlerTest.java new file mode 100755 index 0000000000..7e7e2a0518 --- /dev/null +++ b/bbb-recording-imex/src/test/java/org/bigbluebutton/RecordingImportHandlerTest.java @@ -0,0 +1,43 @@ +package org.bigbluebutton; + +import org.bigbluebutton.api.model.entity.Recording; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RecordingImportHandlerTest { + + private static final Logger logger = LoggerFactory.getLogger(RecordingImportHandlerTest.class); + + private final RecordingImportHandler handler = RecordingImportHandler.getInstance(); + + @Test + @DisplayName("RecordIDs should be properly parsed") + public void testParseRecordId() { + String metadataDirectory = "metadata"; + + String[] entries = new File(metadataDirectory).list(); + Set ids = new HashSet<>(); + + if (entries == null || entries.length == 0) { + logger.info("No recordings were found in {}", new File(metadataDirectory).getAbsolutePath()); + return; + } + + for (String entry : entries) { + String path = metadataDirectory + "/" + entry + "/metadata.xml"; + Recording recording = handler.importRecording(path, entry, false); + ids.add(recording.getRecordId()); + assertEquals(entry, recording.getRecordId()); + } + + assertEquals(entries.length, ids.size()); + } +} diff --git a/bbb-recording-imex/src/test/java/org/bigbluebutton/RecordingStoreTest.java b/bbb-recording-imex/src/test/java/org/bigbluebutton/RecordingStoreTest.java new file mode 100755 index 0000000000..1cc3d2652c --- /dev/null +++ b/bbb-recording-imex/src/test/java/org/bigbluebutton/RecordingStoreTest.java @@ -0,0 +1,80 @@ +package org.bigbluebutton; + +import org.bigbluebutton.api.model.entity.Recording; +import org.bigbluebutton.api.util.DataStore; +import org.junit.jupiter.api.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class RecordingStoreTest { + + private static final Logger logger = LoggerFactory.getLogger(RecordingStoreTest.class); + + private String metadataDirectory = "metadata"; + private final RecordingImportHandler importHandler = RecordingImportHandler.getInstance(); + private final RecordingExportHandler exportHandler = RecordingExportHandler.getInstance(); + private DataStore dataStore; + + @BeforeAll + public static void setup() { + DataStore.getInstance().truncateTables(); + } + + @Test + @DisplayName("Recordings should be properly persisted") + @Order(1) + public void testPersist() { + dataStore = DataStore.getInstance(); + importHandler.importRecordings(metadataDirectory, true); + List recordings = dataStore.findAll(Recording.class); + String[] entries = new File(metadataDirectory).list(); + + if (entries == null || entries.length == 0) { + logger.info("No recordings were found in {}", new File(metadataDirectory).getAbsolutePath()); + return; + } + + assertTrue(recordings != null); + assertEquals(entries.length, recordings.size()); + } + + @Test + @DisplayName("Recording should be properly retrieved") + @Order(2) + public void testFind() { + dataStore = DataStore.getInstance(); + String[] entries = new File(metadataDirectory).list(); + + if (entries == null || entries.length == 0) { + logger.info("No recordings were found in {}", new File(metadataDirectory).getAbsolutePath()); + return; + } + + for (String entry : entries) { + Recording recording = dataStore.findRecordingByRecordId(entry); + assertTrue(recording != null); + } + } + + @Test + @DisplayName("Records should be properly exported") + @Order(3) + public void testExportRecording() { + dataStore = DataStore.getInstance(); + String metadataDirectory = "metadata-export"; + + exportHandler.exportRecordings(metadataDirectory); + + String[] entries = new File(metadataDirectory).list(); + List recordings = dataStore.findAll(Recording.class); + + assertEquals(entries.length, recordings.size()); + } +} diff --git a/bigbluebutton-web/build.gradle b/bigbluebutton-web/build.gradle index 9c2989b1db..c7a86ae68d 100755 --- a/bigbluebutton-web/build.gradle +++ b/bigbluebutton-web/build.gradle @@ -86,9 +86,11 @@ dependencies { // https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload implementation group: 'commons-fileupload', name: 'commons-fileupload', version: '1.4' - implementation "javax.validation:validation-api:2.0.1.Final" -// compile "org.hibernate:hibernate-validator:7.0.1.Final" + implementation 'javax.validation:validation-api:2.0.1.Final' implementation 'org.springframework.boot:spring-boot-starter-validation:2.6.1' + implementation 'org.postgresql:postgresql:42.2.16' + implementation 'org.hibernate:hibernate-core:5.6.1.Final' + implementation 'com.h2database:h2:1.4.199' //--- BigBlueButton Dependencies End console "org.grails:grails-console" diff --git a/bigbluebutton-web/grails-app/conf/spring/resources.xml b/bigbluebutton-web/grails-app/conf/spring/resources.xml index 1f8502cebf..ff07e2a9d6 100755 --- a/bigbluebutton-web/grails-app/conf/spring/resources.xml +++ b/bigbluebutton-web/grails-app/conf/spring/resources.xml @@ -95,7 +95,7 @@ with BigBlueButton; if not, see . - + diff --git a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/RecordingController.groovy b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/RecordingController.groovy index ebf1dd4ce4..f793551e1a 100755 --- a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/RecordingController.groovy +++ b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/RecordingController.groovy @@ -102,9 +102,21 @@ class RecordingController { log.debug intRecId } - Map metadataFilters = ParamsProcessorUtil.processMetaParam(params); + Map metadataFilters = ParamsProcessorUtil.processMetaParam(params) - def getRecordingsResult = meetingService.getRecordings2x(internalRecordIds, states, metadataFilters) + String page + if(!StringUtils.isEmpty(params.page)) { + page = params.page + log.info("Requested page [${page}]") + } + + String size + if(!StringUtils.isEmpty(params.size)) { + size = params.size + log.info("Requested page size [${size}]") + } + + def getRecordingsResult = meetingService.getRecordings2x(internalRecordIds, states, metadataFilters, page, size) withFormat { xml { From 9cfd25b2281fa27c8db569c93dd5a2ed0e393950 Mon Sep 17 00:00:00 2001 From: Paul Trudel Date: Fri, 8 Apr 2022 19:01:26 +0000 Subject: [PATCH 050/268] Updated README --- bbb-recording-imex/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bbb-recording-imex/README.md b/bbb-recording-imex/README.md index e23b020762..48bc6dc9d7 100644 --- a/bbb-recording-imex/README.md +++ b/bbb-recording-imex/README.md @@ -1,13 +1,13 @@ # BBB-Recording-Importer -Imports and parses recordings metadata.xml files and stores the data in a Postgresql database +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 the generates the hibernate config file + - 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-importer From 7ec0e9b186ab6615ed7b4ee666cc151e48f09ce6 Mon Sep 17 00:00:00 2001 From: Paul Trudel Date: Fri, 8 Apr 2022 19:48:39 +0000 Subject: [PATCH 051/268] Fixed import errors --- .../impl/RecordingServiceFileImpl.java | 1 + .../api/service/impl/XmlServiceImpl.java | 1 + .../org/bigbluebutton/api/util/DataStore.java | 28 +++++++++++++++++++ bbb-recording-imex/get-recordings.sh | 2 +- 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/RecordingServiceFileImpl.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/RecordingServiceFileImpl.java index 74e91c0e29..7774c3cf72 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/RecordingServiceFileImpl.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/RecordingServiceFileImpl.java @@ -36,6 +36,7 @@ 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; diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/XmlServiceImpl.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/XmlServiceImpl.java index cc93003c66..4dbed49eb5 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/XmlServiceImpl.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/XmlServiceImpl.java @@ -20,6 +20,7 @@ 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; diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/util/DataStore.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/util/DataStore.java index 965db5261d..c86b25e78d 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/util/DataStore.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/util/DataStore.java @@ -143,6 +143,34 @@ public class DataStore { return result; } + public List findRecordingsByMeetingId(String meetingId) { + logger.info("Attempting to find recordings with meetingID {}", meetingId); + Session session = sessionFactory.openSession(); + Transaction transaction = null; + List result = null; + + try { + transaction = session.beginTransaction(); + CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder(); + CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Recording.class); + Root 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 findRecordingsByState(String state) { logger.info("Attempting to find recordings with state {}", state); Session session = sessionFactory.openSession(); diff --git a/bbb-recording-imex/get-recordings.sh b/bbb-recording-imex/get-recordings.sh index 73383573f2..2a0bdb1995 100755 --- a/bbb-recording-imex/get-recordings.sh +++ b/bbb-recording-imex/get-recordings.sh @@ -9,7 +9,7 @@ do esac done -BASE_URL="https://bbb-dev-01.test/" +BASE_URL="" SUBDIRECTORY="bigbluebutton/api/" ENDPOINT="getRecordings" QUERY="" From 50662e3e657369d18c92188f02fa402c5c1dd029 Mon Sep 17 00:00:00 2001 From: Paul Trudel Date: Mon, 11 Apr 2022 19:50:33 +0000 Subject: [PATCH 052/268] Fixed issue with pagination response --- .../bigbluebutton/api/RecordingService.java | 8 +- .../service/impl/RecordingServiceDbImpl.java | 5 +- .../impl/RecordingServiceFileImpl.java | 8 +- .../api/service/impl/XmlServiceImpl.java | 72 +++--- .../java/org/bigbluebutton/RecordingApp.java | 208 +++++++-------- .../bigbluebutton/RecordingExportHandler.java | 240 +++++++++--------- .../bigbluebutton/RecordingImportHandler.java | 187 +++++++------- .../RecordingImportHandlerTest.java | 86 +++---- .../org/bigbluebutton/RecordingStoreTest.java | 160 ++++++------ .../grails-app/conf/spring/resources.xml | 5 +- 10 files changed, 499 insertions(+), 480 deletions(-) diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/RecordingService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/RecordingService.java index b08358368b..ae38304c9a 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/RecordingService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/RecordingService.java @@ -47,11 +47,9 @@ public interface RecordingService { void processMakePresentationDownloadableMsg(MakePresentationDownloadableMsg msg); File getDownloadablePresentationFile(String meetingId, String presId, String presFilename); - default Page recordingListToPage(List recordings, Pageable pageable) { + default Page listToPage(List list, Pageable pageable) { int start = (int) pageable.getOffset(); - int end = (int) (Math.min((start + pageable.getPageSize()), recordings.size())); - - Page recordingsPage = new PageImpl<>(recordings.subList(start, end), pageable, recordings.size()); - return recordingsPage; + int end = (int) (Math.min((start + pageable.getPageSize()), list.size())); + return new PageImpl<>(list.subList(start, end), pageable, list.size()); } } \ No newline at end of file diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/RecordingServiceDbImpl.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/RecordingServiceDbImpl.java index dcc6bb51d4..10752a7368 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/RecordingServiceDbImpl.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/RecordingServiceDbImpl.java @@ -39,7 +39,6 @@ public class RecordingServiceDbImpl implements RecordingService { public RecordingServiceDbImpl() { dataStore = DataStore.getInstance(); - xmlService = new XmlServiceImpl(); } @Override @@ -128,7 +127,7 @@ public class RecordingServiceDbImpl implements RecordingService { } logger.info("{} recordings remaining", recordings.size()); - Page recordingsPage = recordingListToPage(new ArrayList<>(recordings), pageable); + Page recordingsPage = listToPage(new ArrayList<>(recordings), pageable); String recordingsXml = xmlService.recordingsToXml(recordingsPage.getContent()); String response = xmlService.constructResponseFromRecordingsXml(recordingsXml); return xmlService.constructPaginatedResponse(recordingsPage, response); @@ -244,4 +243,6 @@ public class RecordingServiceDbImpl implements RecordingService { public void setRecordingServiceHelper(RecordingMetadataReaderHelper r) { recordingServiceHelper = r; } + + public void setXmlService(XmlService xmlService) { this.xmlService = xmlService; } } \ No newline at end of file diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/RecordingServiceFileImpl.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/RecordingServiceFileImpl.java index 7774c3cf72..ed62eb229e 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/RecordingServiceFileImpl.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/RecordingServiceFileImpl.java @@ -41,6 +41,7 @@ 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; @@ -58,6 +59,7 @@ public class RecordingServiceFileImpl implements RecordingService { 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; @@ -202,7 +204,9 @@ public class RecordingServiceFileImpl implements RecordingService { public String getRecordings2x(List idList, List states, Map metadataFilters, Pageable pageable) { List recsList = getRecordingsMetadata(idList, states); ArrayList recs = filterRecordingsByMetadata(recsList, metadataFilters); - return recordingServiceHelper.getRecordings2x(recs); + Page recordingsPage = listToPage(recs, pageable); + String response = recordingServiceHelper.getRecordings2x(recs); + return xmlService.constructPaginatedResponse(recordingsPage, response); } private RecordingMetadata getRecordingMetadata(File dir) { @@ -427,6 +431,8 @@ public class RecordingServiceFileImpl implements RecordingService { recordingServiceHelper = r; } + public void setXmlService(XmlService xmlService) { this.xmlService = xmlService; } + private boolean shouldIncludeState(List states, String type) { boolean r = false; diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/XmlServiceImpl.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/XmlServiceImpl.java index 4dbed49eb5..497d403b61 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/XmlServiceImpl.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/impl/XmlServiceImpl.java @@ -59,9 +59,9 @@ public class XmlServiceImpl implements XmlService { } String result = documentToString(document); - logger.info("========== Result =========="); - logger.info("{}", result); - logger.info("============================"); +// logger.info("========== Result =========="); +// logger.info("{}", result); +// logger.info("============================"); return result; } catch(Exception e) { e.printStackTrace(); @@ -112,9 +112,9 @@ public class XmlServiceImpl implements XmlService { } String result = documentToString(document); - logger.info("========== Result =========="); - logger.info("{}", result); - logger.info("============================"); +// logger.info("========== Result =========="); +// logger.info("{}", result); +// logger.info("============================"); return result; } catch(Exception e) { e.printStackTrace(); @@ -135,9 +135,9 @@ public class XmlServiceImpl implements XmlService { document.appendChild(rootElement); String result = documentToString(document); - logger.info("========== Result =========="); - logger.info("{}", result); - logger.info("============================"); +// logger.info("========== Result =========="); +// logger.info("{}", result); +// logger.info("============================"); return result; } catch(Exception e) { e.printStackTrace(); @@ -174,9 +174,9 @@ public class XmlServiceImpl implements XmlService { } String result = documentToString(document); - logger.info("========== Result =========="); - logger.info("{}", result); - logger.info("============================"); +// logger.info("========== Result =========="); +// logger.info("{}", result); +// logger.info("============================"); return result; } catch(Exception e) { e.printStackTrace(); @@ -198,9 +198,9 @@ public class XmlServiceImpl implements XmlService { appendFields(document, rootElement, thumbnail, new String[] {"id", "url", "playbackFormat"}, Type.ATTRIBUTE); String result = documentToString(document); - logger.info("========== Result =========="); - logger.info("{}", result); - logger.info("============================"); +// logger.info("========== Result =========="); +// logger.info("{}", result); +// logger.info("============================"); return result; } catch(Exception e) { e.printStackTrace(); @@ -222,9 +222,9 @@ public class XmlServiceImpl implements XmlService { appendFields(document, rootElement, callbackData, new String[] {"id", "recording"}, Type.CHILD); String result = documentToString(document); - logger.info("========== Result =========="); - logger.info("{}", result); - logger.info("============================"); +// logger.info("========== Result =========="); +// logger.info("{}", result); +// logger.info("============================"); return result; } catch(Exception e) { e.printStackTrace(); @@ -252,9 +252,9 @@ public class XmlServiceImpl implements XmlService { rootElement.appendChild(recordingsNode); String result = documentToString(document); - logger.info("========== Result =========="); - logger.info("{}", result); - logger.info("============================"); +// logger.info("========== Result =========="); +// logger.info("{}", result); +// logger.info("============================"); return result; } catch (Exception e) { e.printStackTrace(); @@ -288,14 +288,13 @@ public class XmlServiceImpl implements XmlService { node = document.importNode(secondDoc.getDocumentElement(), true); pagination.appendChild(node); - Element totalElements = createElement(document, "totalElements", String.valueOf(page.getNumberOfElements())); + 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(Math.ceil(page.getNumberOfElements() / page.getSize()))); + Element totalPages = createElement(document, "totalPages", String.valueOf(page.getTotalPages())); pagination.appendChild(totalPages); Element first = createElement(document, "first", String.valueOf(page.isFirst())); @@ -307,9 +306,9 @@ public class XmlServiceImpl implements XmlService { rootElement.appendChild(pagination); String result = documentToString(document); - logger.info("========== Result =========="); - logger.info("{}", result); - logger.info("============================"); +// logger.info("========== Result =========="); +// logger.info("{}", result); +// logger.info("============================"); return result; } catch (Exception e) { e.printStackTrace(); @@ -326,10 +325,21 @@ public class XmlServiceImpl implements XmlService { Document document = builder.newDocument(); Element rootElement = createElement(document, "pageable", null); + document.appendChild(rootElement); Sort sort = pageable.getSort(); Element sortElement = createElement(document, "sort", null); - logger.info("Sort {}", sort); + + 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); @@ -347,9 +357,9 @@ public class XmlServiceImpl implements XmlService { rootElement.appendChild(unpaged); String result = documentToString(document); - logger.info("========== Result =========="); - logger.info("{}", result); - logger.info("============================"); +// logger.info("========== Result =========="); +// logger.info("{}", result); +// logger.info("============================"); return result; } catch(Exception e) { e.printStackTrace(); diff --git a/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingApp.java b/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingApp.java index 9e557ba70a..ef34eabb90 100755 --- a/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingApp.java +++ b/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingApp.java @@ -1,105 +1,105 @@ -package org.bigbluebutton; - -import java.io.Console; -import java.util.stream.IntStream; - -public class RecordingApp { - - public static void main(String[] args) { - 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); - } +package org.bigbluebutton; + +import java.io.Console; +import java.util.stream.IntStream; + +public class RecordingApp { + + public static void main(String[] args) { + 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); + } } \ No newline at end of file diff --git a/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingExportHandler.java b/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingExportHandler.java index 9e6c9ceadd..36c5f07035 100755 --- a/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingExportHandler.java +++ b/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingExportHandler.java @@ -1,120 +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 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(); - } - } -} +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 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(); + } + } +} diff --git a/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingImportHandler.java b/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingImportHandler.java index a59120ac6f..f2a32faba1 100755 --- a/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingImportHandler.java +++ b/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingImportHandler.java @@ -1,93 +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; - } -} +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; + } +} diff --git a/bbb-recording-imex/src/test/java/org/bigbluebutton/RecordingImportHandlerTest.java b/bbb-recording-imex/src/test/java/org/bigbluebutton/RecordingImportHandlerTest.java index 7e7e2a0518..49bd62e365 100755 --- a/bbb-recording-imex/src/test/java/org/bigbluebutton/RecordingImportHandlerTest.java +++ b/bbb-recording-imex/src/test/java/org/bigbluebutton/RecordingImportHandlerTest.java @@ -1,43 +1,43 @@ -package org.bigbluebutton; - -import org.bigbluebutton.api.model.entity.Recording; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.util.HashSet; -import java.util.Set; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class RecordingImportHandlerTest { - - private static final Logger logger = LoggerFactory.getLogger(RecordingImportHandlerTest.class); - - private final RecordingImportHandler handler = RecordingImportHandler.getInstance(); - - @Test - @DisplayName("RecordIDs should be properly parsed") - public void testParseRecordId() { - String metadataDirectory = "metadata"; - - String[] entries = new File(metadataDirectory).list(); - Set ids = new HashSet<>(); - - if (entries == null || entries.length == 0) { - logger.info("No recordings were found in {}", new File(metadataDirectory).getAbsolutePath()); - return; - } - - for (String entry : entries) { - String path = metadataDirectory + "/" + entry + "/metadata.xml"; - Recording recording = handler.importRecording(path, entry, false); - ids.add(recording.getRecordId()); - assertEquals(entry, recording.getRecordId()); - } - - assertEquals(entries.length, ids.size()); - } -} +package org.bigbluebutton; + +import org.bigbluebutton.api.model.entity.Recording; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RecordingImportHandlerTest { + + private static final Logger logger = LoggerFactory.getLogger(RecordingImportHandlerTest.class); + + private final RecordingImportHandler handler = RecordingImportHandler.getInstance(); + + @Test + @DisplayName("RecordIDs should be properly parsed") + public void testParseRecordId() { + String metadataDirectory = "metadata"; + + String[] entries = new File(metadataDirectory).list(); + Set ids = new HashSet<>(); + + if (entries == null || entries.length == 0) { + logger.info("No recordings were found in {}", new File(metadataDirectory).getAbsolutePath()); + return; + } + + for (String entry : entries) { + String path = metadataDirectory + "/" + entry + "/metadata.xml"; + Recording recording = handler.importRecording(path, entry, false); + ids.add(recording.getRecordId()); + assertEquals(entry, recording.getRecordId()); + } + + assertEquals(entries.length, ids.size()); + } +} diff --git a/bbb-recording-imex/src/test/java/org/bigbluebutton/RecordingStoreTest.java b/bbb-recording-imex/src/test/java/org/bigbluebutton/RecordingStoreTest.java index 1cc3d2652c..4d47b23aff 100755 --- a/bbb-recording-imex/src/test/java/org/bigbluebutton/RecordingStoreTest.java +++ b/bbb-recording-imex/src/test/java/org/bigbluebutton/RecordingStoreTest.java @@ -1,80 +1,80 @@ -package org.bigbluebutton; - -import org.bigbluebutton.api.model.entity.Recording; -import org.bigbluebutton.api.util.DataStore; -import org.junit.jupiter.api.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) -public class RecordingStoreTest { - - private static final Logger logger = LoggerFactory.getLogger(RecordingStoreTest.class); - - private String metadataDirectory = "metadata"; - private final RecordingImportHandler importHandler = RecordingImportHandler.getInstance(); - private final RecordingExportHandler exportHandler = RecordingExportHandler.getInstance(); - private DataStore dataStore; - - @BeforeAll - public static void setup() { - DataStore.getInstance().truncateTables(); - } - - @Test - @DisplayName("Recordings should be properly persisted") - @Order(1) - public void testPersist() { - dataStore = DataStore.getInstance(); - importHandler.importRecordings(metadataDirectory, true); - List recordings = dataStore.findAll(Recording.class); - String[] entries = new File(metadataDirectory).list(); - - if (entries == null || entries.length == 0) { - logger.info("No recordings were found in {}", new File(metadataDirectory).getAbsolutePath()); - return; - } - - assertTrue(recordings != null); - assertEquals(entries.length, recordings.size()); - } - - @Test - @DisplayName("Recording should be properly retrieved") - @Order(2) - public void testFind() { - dataStore = DataStore.getInstance(); - String[] entries = new File(metadataDirectory).list(); - - if (entries == null || entries.length == 0) { - logger.info("No recordings were found in {}", new File(metadataDirectory).getAbsolutePath()); - return; - } - - for (String entry : entries) { - Recording recording = dataStore.findRecordingByRecordId(entry); - assertTrue(recording != null); - } - } - - @Test - @DisplayName("Records should be properly exported") - @Order(3) - public void testExportRecording() { - dataStore = DataStore.getInstance(); - String metadataDirectory = "metadata-export"; - - exportHandler.exportRecordings(metadataDirectory); - - String[] entries = new File(metadataDirectory).list(); - List recordings = dataStore.findAll(Recording.class); - - assertEquals(entries.length, recordings.size()); - } -} +package org.bigbluebutton; + +import org.bigbluebutton.api.model.entity.Recording; +import org.bigbluebutton.api.util.DataStore; +import org.junit.jupiter.api.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class RecordingStoreTest { + + private static final Logger logger = LoggerFactory.getLogger(RecordingStoreTest.class); + + private String metadataDirectory = "metadata"; + private final RecordingImportHandler importHandler = RecordingImportHandler.getInstance(); + private final RecordingExportHandler exportHandler = RecordingExportHandler.getInstance(); + private DataStore dataStore; + + @BeforeAll + public static void setup() { + DataStore.getInstance().truncateTables(); + } + + @Test + @DisplayName("Recordings should be properly persisted") + @Order(1) + public void testPersist() { + dataStore = DataStore.getInstance(); + importHandler.importRecordings(metadataDirectory, true); + List recordings = dataStore.findAll(Recording.class); + String[] entries = new File(metadataDirectory).list(); + + if (entries == null || entries.length == 0) { + logger.info("No recordings were found in {}", new File(metadataDirectory).getAbsolutePath()); + return; + } + + assertTrue(recordings != null); + assertEquals(entries.length, recordings.size()); + } + + @Test + @DisplayName("Recording should be properly retrieved") + @Order(2) + public void testFind() { + dataStore = DataStore.getInstance(); + String[] entries = new File(metadataDirectory).list(); + + if (entries == null || entries.length == 0) { + logger.info("No recordings were found in {}", new File(metadataDirectory).getAbsolutePath()); + return; + } + + for (String entry : entries) { + Recording recording = dataStore.findRecordingByRecordId(entry); + assertTrue(recording != null); + } + } + + @Test + @DisplayName("Records should be properly exported") + @Order(3) + public void testExportRecording() { + dataStore = DataStore.getInstance(); + String metadataDirectory = "metadata-export"; + + exportHandler.exportRecordings(metadataDirectory); + + String[] entries = new File(metadataDirectory).list(); + List recordings = dataStore.findAll(Recording.class); + + assertEquals(entries.length, recordings.size()); + } +} diff --git a/bigbluebutton-web/grails-app/conf/spring/resources.xml b/bigbluebutton-web/grails-app/conf/spring/resources.xml index ff07e2a9d6..2d38a88d47 100755 --- a/bigbluebutton-web/grails-app/conf/spring/resources.xml +++ b/bigbluebutton-web/grails-app/conf/spring/resources.xml @@ -95,12 +95,15 @@ with BigBlueButton; if not, see . - + + + + From 2b4fd2b79fe4cf6aa37b9c4435e3ca32b9c52632 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Tue, 12 Apr 2022 15:40:17 +0000 Subject: [PATCH 053/268] Disconnect Redis when done --- export-annotations/master.js | 17 +++++++++-------- export-annotations/workers/collector.js | 2 ++ export-annotations/workers/notifier.js | 5 +++-- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/export-annotations/master.js b/export-annotations/master.js index fb8d60a5ed..1f68c341c2 100644 --- a/export-annotations/master.js +++ b/export-annotations/master.js @@ -23,22 +23,21 @@ function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } -async function redisAlive(client) { - let ping = await client.ping(); - return (ping === "PONG"); -} - (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 ping = await client.ping(); + let redisAlive = (ping === "PONG"); - while (redisAlive(client)) { + client.on('error', (err) => { logger.info('Redis Client Error', err); redisAlive = false; } ); + + while (redisAlive) { await sleep(config.redis.interval); let job = await client.LPOP(config.redis.channels.queue) @@ -59,4 +58,6 @@ async function redisAlive(client) { kickOffCollectorWorker(exportJob.jobId) } } + + client.disconnect(); })(); diff --git a/export-annotations/workers/collector.js b/export-annotations/workers/collector.js index f9871a3adc..8279ac31f2 100644 --- a/export-annotations/workers/collector.js +++ b/export-annotations/workers/collector.js @@ -60,6 +60,8 @@ let exportJob = JSON.parse(job); } kickOffProcessWorker(exportJob.jobId) + + client.disconnect(); })() parentPort.postMessage({ message: workerData }) diff --git a/export-annotations/workers/notifier.js b/export-annotations/workers/notifier.js index b338f0ee3a..3c7d8aca46 100644 --- a/export-annotations/workers/notifier.js +++ b/export-annotations/workers/notifier.js @@ -15,7 +15,7 @@ const dropbox = `${config.shared.presAnnDropboxDir}/${jobId}` let job = fs.readFileSync(`${dropbox}/job`); let exportJob = JSON.parse(job); -async function connectToRedis() { +async function notifyMeetingActor() { const client = redis.createClient({ host: config.redis.host, port: config.redis.port, @@ -49,6 +49,7 @@ async function connectToRedis() { logger.info(`Annotated PDF available at ${link}`); await client.publish(config.redis.channels.publish, JSON.stringify(notification)); + client.disconnect(); } async function upload(exportJob) { @@ -68,7 +69,7 @@ async function upload(exportJob) { } if (jobType == 'PresentationWithAnnotationDownloadJob') { - connectToRedis(); + notifyMeetingActor(); } else if (jobType == 'PresentationWithAnnotationExportJob') { upload(exportJob); From 0c2e3d1ec6113660af7493dad00d2e0fa195506b Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Tue, 19 Apr 2022 16:35:03 +0200 Subject: [PATCH 054/268] Use CairoSVG and GhostScript instead of Librsvg --- export-annotations/package-lock.json | 382 ++++++++++++++++++++++-- export-annotations/package.json | 2 +- export-annotations/workers/collector.js | 27 +- export-annotations/workers/process.js | 62 ++-- 4 files changed, 428 insertions(+), 45 deletions(-) diff --git a/export-annotations/package-lock.json b/export-annotations/package-lock.json index 9e5edb1e1e..c43e427511 100644 --- a/export-annotations/package-lock.json +++ b/export-annotations/package-lock.json @@ -1,13 +1,345 @@ { "name": "export-annotations", "version": "0.0.1", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "export-annotations", + "version": "0.0.1", + "dependencies": { + "axios": "^0.26.0", + "form-data": "^4.0.0", + "image-size": "^1.0.1", + "redis": "^4.0.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/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/image-size": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.1.tgz", + "integrity": "sha512-VAwkvNSNGClRw9mDHhc5Efax8PLlsOGcUTh0T/LIriC8vPA3U5PdqXWqkz406MoYHMKW8Uf9gWr05T/rYB44kQ==", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "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/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "dependencies": { + "inherits": "~2.0.3" + } + }, + "node_modules/redis": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.0.3.tgz", + "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/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "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==" + "integrity": "sha512-mXEBvEIgF4tUzdIN89LiYsbi6//EdpFA7L8M+DHCvePXg+bfHWi+ct5VI6nHUFQE5+ohm/9wmgihCH3HSkeKsw==", + "requires": {} }, "@node-redis/client": { "version": "1.0.3", @@ -23,22 +355,26 @@ "@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==" + "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==" + "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==" + "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==" + "integrity": "sha512-+nTn6EewVj3GlUXPuD3dgheWqo219jTxlo6R+pg24OeVvFHx9aFGGiyOgj3vBPhWUdRZ0xMcujXV5ki4fbLyMw==", + "requires": {} }, "@oozcitak/dom": { "version": "1.15.10", @@ -133,6 +469,19 @@ "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.8.2.tgz", "integrity": "sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg==" }, + "image-size": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.1.tgz", + "integrity": "sha512-VAwkvNSNGClRw9mDHhc5Efax8PLlsOGcUTh0T/LIriC8vPA3U5PdqXWqkz406MoYHMKW8Uf9gWr05T/rYB44kQ==", + "requires": { + "queue": "6.0.2" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, "mime-db": { "version": "1.51.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", @@ -146,6 +495,14 @@ "mime-db": "1.51.0" } }, + "queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "requires": { + "inherits": "~2.0.3" + } + }, "redis": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/redis/-/redis-4.0.3.tgz", @@ -172,24 +529,11 @@ "redis-errors": "^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=" }, - "xml-js": { - "version": "1.6.11", - "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", - "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", - "requires": { - "sax": "^1.2.4" - } - }, "xmlbuilder2": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-3.0.2.tgz", diff --git a/export-annotations/package.json b/export-annotations/package.json index 2f06ffde89..927748598f 100644 --- a/export-annotations/package.json +++ b/export-annotations/package.json @@ -8,8 +8,8 @@ "dependencies": { "axios": "^0.26.0", "form-data": "^4.0.0", + "image-size": "^1.0.1", "redis": "^4.0.3", - "xml-js": "^1.6.11", "xmlbuilder2": "^3.0.2" } } diff --git a/export-annotations/workers/collector.js b/export-annotations/workers/collector.js index 8279ac31f2..049e4a32a2 100644 --- a/export-annotations/workers/collector.js +++ b/export-annotations/workers/collector.js @@ -2,6 +2,7 @@ const Logger = require('../lib/utils/logger'); const config = require('../config'); const fs = require('fs'); const redis = require('redis'); +const { execSync } = require("child_process"); const { Worker, workerData, parentPort } = require('worker_threads') @@ -53,10 +54,30 @@ let exportJob = JSON.parse(job); // Collect the Presentation Page files from the presentation directory for (let p of pages) { let pageNumber = p.page; - let slide = `${exportJob.presLocation}/svgs/slide${pageNumber}.svg`; - let file = `${dropbox}/slide${pageNumber}.svg`; + let pdf = `${exportJob.presLocation}/${exportJob.presId}.pdf`; + let file = `${dropbox}/slide${pageNumber}`; - fs.copyFile(slide, file, (err) => { if (err) throw err; } ); + let extactSlideAsPDFCommands = [ + 'pdftocairo', + '-png', + '-f', pageNumber, + '-l', pageNumber, + '-singlefile', + pdf, + file + ].join(' '); + + execSync(extactSlideAsPDFCommands, (error, stderr) => { + if (error) { + logger.error(`PDFtoCairo failed with error: ${error.message}`); + return; + } + + if (stderr) { + logger.error(`PDFtoCairo failed with stderr: ${stderr}`); + return; + } + }) } kickOffProcessWorker(exportJob.jobId) diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js index dc3e834868..b60bac843d 100644 --- a/export-annotations/workers/process.js +++ b/export-annotations/workers/process.js @@ -1,7 +1,7 @@ const Logger = require('../lib/utils/logger'); const config = require('../config'); const fs = require('fs'); -const convert = require('xml-js'); +const sizeOf = require('image-size'); const { create } = require('xmlbuilder2', { encoding: 'utf-8' }); const { execSync } = require("child_process"); const { Worker, workerData, parentPort } = require('worker_threads'); @@ -49,7 +49,6 @@ function render_HTMLTextBox(htmlFilePath, id, width, height) { execSync(commands.join(' '), (error, stderr) => { if (error) { logger.error(`Error when rendering text box for string "${string}" with wkhtmltoimage: ${error.message}`); - return; } if (stderr) { @@ -206,6 +205,7 @@ function overlay_poll(svg, annotation, w, h) { if (stderr) { logger.error(`Poll generation failed with stderr: ${stderr}`); + return; } }); @@ -341,20 +341,13 @@ let exportJob = JSON.parse(job); let annotations = fs.readFileSync(`${dropbox}/whiteboard`); let whiteboard = JSON.parse(annotations); let pages = JSON.parse(whiteboard.pages); -let rsvgConvertInput = "" +let ghostScriptInput = "" // 3. Convert annotations to SVG for (let currentSlide of pages) { - var backgroundSlide = fs.readFileSync(`${dropbox}/slide${currentSlide.page}.svg`).toString(); - // Read background slide in as JSON to determine dimensions - // TODO: find a better way to get width and height of slide (e.g. as part of message) - backgroundSlide = JSON.parse(convert.xml2json(backgroundSlide)); - - // There's a bug with older versions of rsvg which defaults SVG output to pixels. - // So we ignore the units here as well. - // See: https://gitlab.gnome.org/GNOME/librsvg/-/issues/766 - var slideWidth = Number(backgroundSlide.elements[0].attributes.width.replace(/[^\d.]/g, '')) - var slideHeight = Number(backgroundSlide.elements[0].attributes.height.replace(/[^\d.]/g, '')) + var dimensions = sizeOf(`${dropbox}/slide${currentSlide.page}.png`); + var slideWidth = dimensions.width; + var slideHeight = dimensions.height; var panzoom_x = -currentSlide.xOffset * MAGIC_MYSTERY_NUMBER / 100.0 * slideWidth var panzoom_y = -currentSlide.yOffset * MAGIC_MYSTERY_NUMBER / 100.0 * slideHeight @@ -375,7 +368,7 @@ for (let currentSlide of pages) { sysID: 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' }) .ele('image', { - 'xlink:href': `file://${dropbox}/slide${currentSlide.page}.svg`, + 'xlink:href': `file://${dropbox}/slide${currentSlide.page}.png`, width: slideWidth, height: slideHeight, }) @@ -390,32 +383,57 @@ for (let currentSlide of pages) { svg = svg.end({ prettyPrint: true }); // Write annotated SVG file - let file = `${dropbox}/annotated-slide${currentSlide.page}.svg` - fs.writeFileSync(file, svg, function(err) { + let SVGfile = `${dropbox}/annotated-slide${currentSlide.page}.svg` + let PDFfile = `${dropbox}/annotated-slide${currentSlide.page}.pdf` + + fs.writeFileSync(SVGfile, svg, function(err) { if(err) { return logger.error(err); } }); - rsvgConvertInput += `${file} ` + let convertAnnotatedSlide = [ + 'cairosvg', + SVGfile, + '-o', PDFfile + ].join(' '); + + execSync(convertAnnotatedSlide, (error, stderr) => { + if (error) { + logger.error(`SVG to PDF export failed with error: ${error.message}`); + return; + } + + if (stderr) { + logger.error(`SVG to PDF export failed with stderr: ${stderr}`); + return; + } + }); + + ghostScriptInput += `${PDFfile} ` } // Create PDF output directory if it doesn't exist let output_dir = `${exportJob.presLocation}/pdfs`; if (!fs.existsSync(output_dir)) { fs.mkdirSync(output_dir); } -let render = [ - 'rsvg-convert', rsvgConvertInput, - '-f', 'pdf', - '-o', `${output_dir}/annotated_slides_${jobId}.pdf` +let mergePDFs = [ + 'gs', + '-dNOPAUSE', + '-sDEVICE=pdfwrite', + `-sOUTPUTFILE=${output_dir}/annotated_slides_${jobId}.pdf`, + `-dBATCH`, + ghostScriptInput, ].join(' '); // Resulting PDF file is stored in the presentation dir -execSync(render, (error, stderr) => { +execSync(mergePDFs, (error, stderr) => { if (error) { logger.error(`SVG to PDF export failed with error: ${error.message}`); return; } + if (stderr) { logger.error(`SVG to PDF export failed with stderr: ${stderr}`); + return; } }); From 2cb7f514b0440379f14bb62f8f68181b04518b4a Mon Sep 17 00:00:00 2001 From: Paul Trudel Date: Tue, 19 Apr 2022 19:35:40 +0000 Subject: [PATCH 055/268] Updated README --- bbb-recording-imex/README.md | 2 +- bbb-recording-imex/pom.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bbb-recording-imex/README.md b/bbb-recording-imex/README.md index 48bc6dc9d7..49c624078b 100644 --- a/bbb-recording-imex/README.md +++ b/bbb-recording-imex/README.md @@ -10,7 +10,7 @@ Imports and parses recording metadata.xml files and stores the data in a Postgre - 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-importer +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" diff --git a/bbb-recording-imex/pom.xml b/bbb-recording-imex/pom.xml index 66f9818417..2013def185 100755 --- a/bbb-recording-imex/pom.xml +++ b/bbb-recording-imex/pom.xml @@ -27,7 +27,7 @@ true - org.bigbluebutton.recording.RecordingApp + org.bigbluebutton.RecordingApp @@ -108,4 +108,4 @@ - \ No newline at end of file + From 333a01aab4e70100536a2f47b8ab1b368e463a0d Mon Sep 17 00:00:00 2001 From: Paul Trudel Date: Tue, 19 Apr 2022 19:38:21 +0000 Subject: [PATCH 056/268] Changed RecordingService implementation back to file --- bigbluebutton-web/grails-app/conf/spring/resources.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-web/grails-app/conf/spring/resources.xml b/bigbluebutton-web/grails-app/conf/spring/resources.xml index 2d38a88d47..5684a7c38b 100755 --- a/bigbluebutton-web/grails-app/conf/spring/resources.xml +++ b/bigbluebutton-web/grails-app/conf/spring/resources.xml @@ -97,7 +97,7 @@ with BigBlueButton; if not, see . - + From 54600f6212856313df05f8fa99946ca98d1652a5 Mon Sep 17 00:00:00 2001 From: Paul Trudel Date: Thu, 21 Apr 2022 15:56:01 +0000 Subject: [PATCH 057/268] Changed deployment directory --- bbb-recording-imex/README.md | 4 ++-- bbb-recording-imex/deploy.sh | 11 +++++++++++ bbb-recording-imex/run.sh | 2 -- 3 files changed, 13 insertions(+), 4 deletions(-) create mode 100755 bbb-recording-imex/deploy.sh delete mode 100755 bbb-recording-imex/run.sh diff --git a/bbb-recording-imex/README.md b/bbb-recording-imex/README.md index 49c624078b..eb6fce2942 100644 --- a/bbb-recording-imex/README.md +++ b/bbb-recording-imex/README.md @@ -14,8 +14,8 @@ Imports and parses recording metadata.xml files and stores the data in a Postgre - 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" - - To use the main program compile it with "mvn package" which will generate two jars in the target directory - - Run the program with the run.sh script + - Use the deploy.sh script to compile the program + - Run the program with the recording-imex.sh script found in ~/usr/local/bin ## Testing the new recording service diff --git a/bbb-recording-imex/deploy.sh b/bbb-recording-imex/deploy.sh new file mode 100755 index 0000000000..d2183c3b65 --- /dev/null +++ b/bbb-recording-imex/deploy.sh @@ -0,0 +1,11 @@ +#!/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 diff --git a/bbb-recording-imex/run.sh b/bbb-recording-imex/run.sh deleted file mode 100755 index 48ed1fef2b..0000000000 --- a/bbb-recording-imex/run.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -java -jar target/bbb-recording-imex-1.0-SNAPSHOT-shaded.jar From cc5b24e81fcb57207f28bc77382bd1cb3e849614 Mon Sep 17 00:00:00 2001 From: Paul Trudel Date: Mon, 25 Apr 2022 20:44:42 +0000 Subject: [PATCH 058/268] Added support for command line arguments to RecordingApp --- .../java/org/bigbluebutton/RecordingApp.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingApp.java b/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingApp.java index ef34eabb90..c6faa688b9 100755 --- a/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingApp.java +++ b/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingApp.java @@ -6,6 +6,74 @@ 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++]; + + 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) persist = Boolean.parseBoolean(args[i++]); + 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) { + System.out.println("Error: Required arguments not specified"); + System.out.println("Usage: {-e|-i } [-s ] PATH"); + return; + } else path = args[i]; + + executeCommands(export, persist, id, 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 { From c9606257aad443366319b8f5dd69f6ecfd4cdd50 Mon Sep 17 00:00:00 2001 From: Brent Baccala Date: Tue, 26 Apr 2022 10:51:29 -0400 Subject: [PATCH 059/268] package build scripts - calculate GIT_REV and COMMIT_DATE outside the docker container and pass them in as environment variables, since we can't compute them inside the docker container if the source directory is a git worktree --- build/setup-inside-docker.sh | 7 ------- build/setup.sh | 12 ++++++++++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/build/setup-inside-docker.sh b/build/setup-inside-docker.sh index ee54366b89..3ae629fa62 100755 --- a/build/setup-inside-docker.sh +++ b/build/setup-inside-docker.sh @@ -26,16 +26,9 @@ for dir in .gradle .grails .ivy2 .m2; do ln -s "${SOURCE}/cache/${dir}" "/root/${dir}" done -if [ "$LOCAL_BUILD" != 1 ] ; then - GIT_REV="${CI_COMMIT_SHA:0:10}" -else - GIT_REV=$(git rev-parse HEAD) - GIT_REV="local-build-${GIT_REV:0:10}" -fi VERSION_NUMBER="$(cat "$SOURCE/bigbluebutton-config/bigbluebutton-release" | cut -d '=' -f2 | cut -d "-" -f1)" # this contains stuff like alpha4 etc VERSION_ADDON="$(cat "$SOURCE/bigbluebutton-config/bigbluebutton-release" | cut -d '=' -f2 | cut -d "-" -f2)" -COMMIT_DATE="$(git log -n1 --pretty='format:%cd' --date=format:'%Y%m%dT%H%M%S')" BUILD_NUMBER=${BUILD_NUMBER:=1} EPOCH=${EPOCH:=2} diff --git a/build/setup.sh b/build/setup.sh index f8ee735905..1b33da724e 100755 --- a/build/setup.sh +++ b/build/setup.sh @@ -15,9 +15,17 @@ mkdir -p artifacts DOCKER_IMAGE=$(python3 -c 'import yaml; print(yaml.load(open("./.gitlab-ci.yml"), Loader=yaml.SafeLoader)["default"]["image"])') +if [ "$LOCAL_BUILD" != 1 ] ; then + GIT_REV="${CI_COMMIT_SHA:0:10}" +else + GIT_REV=$(git rev-parse HEAD) + GIT_REV="local-build-${GIT_REV:0:10}" +fi +COMMIT_DATE="$(git log -n1 --pretty='format:%cd' --date=format:'%Y%m%dT%H%M%S')" + # -v "$CACHE_DIR/dev":/root/dev sudo docker run --rm \ - -e "LOCAL_BUILD=1" \ + --env GIT_REV=$GIT_REV --env COMMIT_DATE=$COMMIT_DATE --env "LOCAL_BUILD=1" \ --mount type=bind,src="$PWD",dst=/mnt \ --mount type=bind,src="${PWD}/artifacts,dst=/artifacts" \ -t "$DOCKER_IMAGE" /mnt/build/setup-inside-docker.sh "$PACKAGE_TO_BUILD" @@ -28,4 +36,4 @@ sudo docker run --rm \ # -v "$CACHE_DIR/$DISTRO/.m2:/root/.m2" \ # -v "$TMP/$TARGET:$TMP/$TARGET" \ -find artifacts \ No newline at end of file +find artifacts From 0148deacd66c61d30d535ea0b1a4928b3f0377e7 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Wed, 27 Apr 2022 13:12:16 +0200 Subject: [PATCH 060/268] Redis: remove stale keys and ping-pong behavior --- export-annotations/config/settings.json | 1 - export-annotations/master.js | 54 ++++++++++++------------- export-annotations/workers/collector.js | 2 + 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/export-annotations/config/settings.json b/export-annotations/config/settings.json index 8cc6f1c00f..5962900ba1 100644 --- a/export-annotations/config/settings.json +++ b/export-annotations/config/settings.json @@ -27,7 +27,6 @@ "host": "127.0.0.1", "port": 6379, "password": null, - "interval": 1000, "channels": { "queue": "exportJobs", "publish": "to-akka-apps-redis-channel" diff --git a/export-annotations/master.js b/export-annotations/master.js index 1f68c341c2..1f1548c683 100644 --- a/export-annotations/master.js +++ b/export-annotations/master.js @@ -2,6 +2,7 @@ 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 logger = new Logger('presAnn Master'); @@ -19,10 +20,6 @@ const kickOffCollectorWorker = (jobId) => { }) } -function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - (async () => { const client = redis.createClient({ host: config.redis.host, @@ -32,32 +29,33 @@ function sleep(ms) { await client.connect(); - let ping = await client.ping(); - let redisAlive = (ping === "PONG"); + client.on('error', (err) => logger.info('Redis Client Error', err)); - client.on('error', (err) => { logger.info('Redis Client Error', err); redisAlive = false; } ); + async function waitForJobs () { + const queue = client.blPop( + commandOptions({ isolated: true }), + config.redis.channels.queue, + 0 + ); - while (redisAlive) { - await sleep(config.redis.interval); + let job = await queue; - let job = await client.LPOP(config.redis.channels.queue) - const exportJob = JSON.parse(job); - - if(job != null) { - logger.info('Received job', job) - - // Create folder in dropbox - let dropbox = `${config.shared.presAnnDropboxDir}/${exportJob.jobId}` - fs.mkdirSync(dropbox, { recursive: true }) - - // Drop job into dropbox as JSON - fs.writeFile(`${dropbox}/job`, job, function(err) { - if(err) { return logger.error(err); } - }); - - kickOffCollectorWorker(exportJob.jobId) - } - } + logger.info('Received job', job.element); + const exportJob = JSON.parse(job.element); + + // Create folder in dropbox + let dropbox = `${config.shared.presAnnDropboxDir}/${exportJob.jobId}` + fs.mkdirSync(dropbox, { recursive: true }) - client.disconnect(); + // Drop job into dropbox as JSON + fs.writeFile(`${dropbox}/job`, job.element, function(err) { + if(err) { return logger.error(err); } + }); + + kickOffCollectorWorker(exportJob.jobId) + + waitForJobs(); + } + + waitForJobs(); })(); diff --git a/export-annotations/workers/collector.js b/export-annotations/workers/collector.js index 049e4a32a2..37b8c9be5a 100644 --- a/export-annotations/workers/collector.js +++ b/export-annotations/workers/collector.js @@ -82,6 +82,8 @@ let exportJob = JSON.parse(job); kickOffProcessWorker(exportJob.jobId) + // Remove annotations from Redis + await client.DEL(jobId); client.disconnect(); })() From 717c6924687d42e4eabd535763033fd7aeb7e98e Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Wed, 27 Apr 2022 17:35:39 +0200 Subject: [PATCH 061/268] Support PNG and JPEG images --- export-annotations/workers/collector.js | 81 +++++++++++++++++-------- export-annotations/workers/process.js | 18 ++---- 2 files changed, 63 insertions(+), 36 deletions(-) diff --git a/export-annotations/workers/collector.js b/export-annotations/workers/collector.js index 37b8c9be5a..838f89d45c 100644 --- a/export-annotations/workers/collector.js +++ b/export-annotations/workers/collector.js @@ -42,6 +42,10 @@ let exportJob = JSON.parse(job); await client.connect(); let presAnn = await client.hGetAll(exportJob.jobId); + + // Remove annotations from Redis + await client.DEL(jobId); + let annotations = JSON.stringify(presAnn); let whiteboard = JSON.parse(annotations); @@ -52,38 +56,67 @@ let exportJob = JSON.parse(job); }); // Collect the Presentation Page files from the presentation directory + let path = `${exportJob.presLocation}/${exportJob.presId}`; + let pdfFileExists = fs.existsSync(`${path}.pdf`); + for (let p of pages) { let pageNumber = p.page; - let pdf = `${exportJob.presLocation}/${exportJob.presId}.pdf`; let file = `${dropbox}/slide${pageNumber}`; - let extactSlideAsPDFCommands = [ - 'pdftocairo', - '-png', - '-f', pageNumber, - '-l', pageNumber, - '-singlefile', - pdf, - file - ].join(' '); + if(pdfFileExists) { + let extactSlideAsPDFCommands = [ + 'pdftocairo', + '-png', + '-f', pageNumber, + '-l', pageNumber, + '-singlefile', + `${path}.pdf`, + file + ].join(' '); + + execSync(extactSlideAsPDFCommands, (error, stderr) => { + if (error) { + return logger.error(`PDFtoCairo failed with error: ${error.message}`); + } + + if (stderr) { + return logger.error(`PDFtoCairo failed with stderr: ${stderr}`); + } + }) + } - execSync(extactSlideAsPDFCommands, (error, stderr) => { - if (error) { - logger.error(`PDFtoCairo failed with error: ${error.message}`); - return; - } + else if (fs.existsSync(`${path}.png`)) { + fs.copyFileSync(`${path}.png`, `${file}.png`); + } + + else if (fs.existsSync(`${path}.jpeg`)) { + let convertImageToPngCommands = [ + 'convert', + `${path}.jpeg`, + '-background', 'white', + '-resize', '1600x1600', + '-auto-orient', + '-flatten', + `${file}.png` + ].join(' '); - if (stderr) { - logger.error(`PDFtoCairo failed with stderr: ${stderr}`); - return; - } - }) + execSync(convertImageToPngCommands, (error, stderr) => { + if (error) { + return logger.error(`Image conversion to PNG failed with error: ${error.message}`); + } + + if (stderr) { + return logger.error(`Image conversion to PNG failed with stderr: ${stderr}`); + } + }) + } + + else { + return logger.error(`Could not find whiteboard presentation file for job ${exportJob.jobId}`); + } } - kickOffProcessWorker(exportJob.jobId) - - // Remove annotations from Redis - await client.DEL(jobId); + kickOffProcessWorker(exportJob.jobId); client.disconnect(); })() diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js index b60bac843d..0389fe00cc 100644 --- a/export-annotations/workers/process.js +++ b/export-annotations/workers/process.js @@ -199,13 +199,11 @@ function overlay_poll(svg, annotation, w, h) { // Render the poll SVG using gen_poll_svg script execSync(`${config.genPollSVG.path} -i ${pollJSON} -w ${poll_width} -h ${poll_height} -n ${annotation.numResponders} -o ${pollSVG}`, (error, stderr) => { if (error) { - logger.error(`Poll generation failed with error: ${error.message}`); - return; + return logger.error(`Poll generation failed with error: ${error.message}`); } if (stderr) { - logger.error(`Poll generation failed with stderr: ${stderr}`); - return; + return logger.error(`Poll generation failed with stderr: ${stderr}`); } }); @@ -398,13 +396,11 @@ for (let currentSlide of pages) { execSync(convertAnnotatedSlide, (error, stderr) => { if (error) { - logger.error(`SVG to PDF export failed with error: ${error.message}`); - return; + return logger.error(`SVG to PDF export failed with error: ${error.message}`); } if (stderr) { - logger.error(`SVG to PDF export failed with stderr: ${stderr}`); - return; + return logger.error(`SVG to PDF export failed with stderr: ${stderr}`); } }); @@ -427,13 +423,11 @@ let mergePDFs = [ // Resulting PDF file is stored in the presentation dir execSync(mergePDFs, (error, stderr) => { if (error) { - logger.error(`SVG to PDF export failed with error: ${error.message}`); - return; + return logger.error(`SVG to PDF export failed with error: ${error.message}`); } if (stderr) { - logger.error(`SVG to PDF export failed with stderr: ${stderr}`); - return; + return logger.error(`SVG to PDF export failed with stderr: ${stderr}`); } }); From f4b4e40c379ca294e1c2f04a8a3524a39773767d Mon Sep 17 00:00:00 2001 From: Paul Trudel Date: Thu, 28 Apr 2022 14:28:43 +0000 Subject: [PATCH 062/268] Improved usage description and added new --help option --- .../java/org/bigbluebutton/RecordingApp.java | 86 +++++++++++++------ 1 file changed, 58 insertions(+), 28 deletions(-) diff --git a/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingApp.java b/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingApp.java index c6faa688b9..6cb0a6a84f 100755 --- a/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingApp.java +++ b/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingApp.java @@ -6,7 +6,7 @@ import java.util.stream.IntStream; public class RecordingApp { public static void main(String[] args) { - if(args.length > 0) { + if (args.length > 0) { commandMode(args); } else { interactiveMode(); @@ -22,54 +22,84 @@ public class RecordingApp { String id = null; String path; - while(i < args.length && args[i].startsWith("-")) { + while (i < args.length && args[i].startsWith("-")) { arg = args[i++]; - for(j = 1; j < arg.length(); j++) { + 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) persist = Boolean.parseBoolean(args[i++]); + 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: Imports require an argument specifying if they should be persisted"); + System.out.println("Error: Could not parse persist argument"); 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); + } 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) { + if (i == args.length) { System.out.println("Error: Required arguments not specified"); - System.out.println("Usage: {-e|-i } [-s ] PATH"); + printUsage(); return; - } else path = args[i]; + } else + path = args[i]; executeCommands(export, persist, id, path); } + private static void printUsage() { + System.out.println("Usage: {-e|-i } [-s ] PATH"); + System.out.println("Import/export recording(s) to/from PATH"); + System.out.println("-e export recording(s)"); + System.out.println( + "-i import recording(s) and indicate if they should be persisted [true|false]"); + System.out.println("-s ID of single recording to be imported/exported"); + } + private static void executeCommands(boolean export, boolean persist, String id, String path) { - if(!export) { + if (!export) { RecordingImportHandler handler = RecordingImportHandler.getInstance(); - if(id == null || id.isEmpty()) handler.importRecordings(path,persist); - else handler.importRecording(path, id, persist); + 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); + if (id == null || id.isEmpty()) + handler.exportRecordings(path); + else + handler.exportRecording(id, path); } } From 7851d54484fc10acf7d14fc3bff8333df0f1f4ab Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Fri, 29 Apr 2022 13:50:42 +0200 Subject: [PATCH 063/268] PDF file: include meeting and room name --- .../StoreExportJobInRedisPresAnnEvent.scala | 10 +++++ .../core/running/MeetingActor.scala | 44 ++++++++++++++----- .../redis/ExportAnnotationsActor.scala | 2 + .../endpoint/redis/RedisRecorderActor.scala | 2 +- .../common2/msgs/WhiteboardMsgs.scala | 2 + .../nginx-confs/presentation-slides.nginx | 4 +- export-annotations/master.js | 7 +-- export-annotations/package-lock.json | 43 ++++++++++++++++++ export-annotations/package.json | 1 + export-annotations/workers/collector.js | 28 ++++++------ export-annotations/workers/notifier.js | 12 +++-- export-annotations/workers/process.js | 38 +++++++++------- 12 files changed, 138 insertions(+), 55 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StoreExportJobInRedisPresAnnEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StoreExportJobInRedisPresAnnEvent.scala index 79332f71d8..8d01bcc9bc 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StoreExportJobInRedisPresAnnEvent.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StoreExportJobInRedisPresAnnEvent.scala @@ -35,6 +35,14 @@ class StoreExportJobInRedisPresAnnEvent extends AbstractPresentationWithAnnotati eventMap.put(JOB_TYPE, jobType) } + def setMeetingName(meetingName: String) { + eventMap.put(MEETING_NAME, meetingName) + } + + def setPresName(presName: String) { + eventMap.put(PRES_NAME, presName) + } + def setPresId(presId: String) { eventMap.put(PRES_ID, presId) } @@ -63,6 +71,8 @@ class StoreExportJobInRedisPresAnnEvent extends AbstractPresentationWithAnnotati object StoreExportJobInRedisPresAnnEvent { protected final val JOB_ID = "jobId" protected final val JOB_TYPE = "jobType" + protected final val MEETING_NAME = "meetingName" + protected final val PRES_NAME = "presName" protected final val PRES_ID = "presId" protected final val PRES_LOCATION = "presLocation" protected final val ALL_PAGES = "allPages" diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index dd5ec22658..f69b0d09dc 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -753,6 +753,7 @@ class MeetingActor( def handleMakePresentationWithAnnotationDownloadReqMsg(m: MakePresentationWithAnnotationDownloadReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = { val meetingId = liveMeeting.props.meetingProp.intId + val meetingName: String = liveMeeting.props.meetingProp.name // Whiteboard ID val presId: String = m.body.presId match { @@ -765,18 +766,31 @@ class MeetingActor( // Determine page amount val presentationPods: Vector[PresentationPod] = state.presentationPodManager.getAllPresentationPodsInMeeting() - val currentPres = presentationPods.flatMap(_.getCurrentPresentation()).head - val pageCount = currentPres.pages.size + val currentPres = presentationPods.flatMap(_.getCurrentPresentation()).headOption + + currentPres match { + case None => + log.error(s"No presentation set in meeting ${meetingId}") + return + case _ => () + } + + val pageCount = currentPres.get.pages.size val pagesRange: List[Int] = if (allPages) (1 to pageCount).toList else pages var storeAnnotationPages = new Array[PresentationPageForExport](pagesRange.size) var resultingPage = 0 for (pageNumber <- pagesRange) { + if (pageNumber < 1 || pageNumber > pageCount) { + println(pagesRange.length) + log.error(s"Page ${pageNumber} requested for export out of range, aborting") + return + } var whiteboardId = s"${presId}/${pageNumber.toString}" - val presentationPage: PresentationPage = currentPres.pages(whiteboardId) + val presentationPage: PresentationPage = currentPres.get.pages(whiteboardId) val xOffset: Double = presentationPage.xOffset val yOffset: Double = presentationPage.yOffset val widthRatio: Double = presentationPage.widthRatio @@ -796,7 +810,7 @@ class MeetingActor( // 2) Insert Export Job in Redis val jobType = "PresentationWithAnnotationDownloadJob" val presLocation = s"/var/bigbluebutton/${meetingId}/${meetingId}/${presId}" - val exportJob = new ExportJob(jobId, jobType, presId, presLocation, allPages, pagesRange, meetingId, "") + val exportJob = new ExportJob(jobId, jobType, meetingName, currentPres.get.name, presId, presLocation, allPages, pagesRange, meetingId, "") var job = buildStoreExportJobInRedisSysMsg(exportJob) outGW.send(job) } @@ -804,25 +818,33 @@ class MeetingActor( def handleExportPresentationWithAnnotationReqMsg(m: ExportPresentationWithAnnotationReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = { val meetingId = liveMeeting.props.meetingProp.intId + val meetingName: String = liveMeeting.props.meetingProp.name val userId = m.header.userId val presId: String = getMeetingInfoPresentationDetails.id val parentMeetingId: String = m.body.parentMeetingId val allPages: Boolean = m.body.allPages val presentationPods: Vector[PresentationPod] = state.presentationPodManager.getAllPresentationPodsInMeeting() - val currentPres = presentationPods.flatMap(_.getCurrentPresentation()).head - val currentPage: PresentationPage = PresentationInPod.getCurrentPage(currentPres).get + val currentPres = presentationPods.flatMap(_.getCurrentPresentation()).headOption - val pageCount = currentPres.pages.size + currentPres match { + case None => + log.error(s"No presentation set in meeting ${meetingId}") + return + case _ => () + } + + val currentPage: PresentationPage = PresentationInPod.getCurrentPage(currentPres.get).get + + val pageCount = currentPres.get.pages.size val pagesRange: List[Int] = if (allPages) (1 to pageCount).toList else List(currentPage.num) var storeAnnotationPages = new Array[PresentationPageForExport](pagesRange.size) var resultingPage = 0 for (pageNumber <- pagesRange) { - var whiteboardId = s"${presId}/${pageNumber.toString}" - val presentationPage: PresentationPage = currentPres.pages(whiteboardId) + val presentationPage: PresentationPage = currentPres.get.pages(whiteboardId) val xOffset: Double = presentationPage.xOffset val yOffset: Double = presentationPage.yOffset val widthRatio: Double = presentationPage.widthRatio @@ -837,7 +859,7 @@ class MeetingActor( val jobId = RandomStringGenerator.randomAlphanumericString(16) // 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 - outGW.send(buildPresentationUploadTokenSysPubMsg(parentMeetingId, userId, presentationUploadToken, currentPres.name)) + outGW.send(buildPresentationUploadTokenSysPubMsg(parentMeetingId, userId, presentationUploadToken, currentPres.get.name)) // 1) Send Annotations to Redis var annotations = new StoredAnnotations(jobId, presId, storeAnnotationPages) @@ -846,7 +868,7 @@ class MeetingActor( // 2) Insert Export Job in Redis val jobType: String = "PresentationWithAnnotationExportJob" val presLocation = s"/var/bigbluebutton/${meetingId}/${meetingId}/${presId}" - val exportJob = new ExportJob(jobId, jobType, presId, presLocation, allPages, pagesRange, parentMeetingId, presentationUploadToken) + val exportJob = new ExportJob(jobId, jobType, meetingName, currentPres.get.name, presId, presLocation, allPages, pagesRange, parentMeetingId, presentationUploadToken) var job = buildStoreExportJobInRedisSysMsg(exportJob) outGW.send(job) } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/ExportAnnotationsActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/ExportAnnotationsActor.scala index 30acdf6205..7b7cb40159 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/ExportAnnotationsActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/ExportAnnotationsActor.scala @@ -75,6 +75,8 @@ class ExportAnnotationsActor( ev.setJobId(msg.body.exportJob.jobId) ev.setJobType(msg.body.exportJob.jobType) + ev.setMeetingName(msg.body.exportJob.meetingName) + ev.setPresName(msg.body.exportJob.presName) ev.setPresId(msg.body.exportJob.presId) ev.setPresLocation(msg.body.exportJob.presLocation) ev.setAllPages(msg.body.exportJob.allPages.toString) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala index d6e5dd8f63..a106261155 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala @@ -142,7 +142,7 @@ class RedisRecorderActor( ev.setSenderId(msg.body.msg.sender.id) ev.setMessage(msg.body.msg.message) ev.setSenderRole(msg.body.msg.sender.role) - + val isModerator = msg.body.msg.sender.role == "MODERATOR" ev.setChatEmphasizedText(msg.body.msg.chatEmphasizedText && isModerator) diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala index 45eed23001..eb62113ab4 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala @@ -21,6 +21,8 @@ case class StoredAnnotations( case class ExportJob( jobId: String, jobType: String, + meetingName: String, + presName: String, presId: String, presLocation: String, allPages: Boolean, diff --git a/bigbluebutton-web/nginx-confs/presentation-slides.nginx b/bigbluebutton-web/nginx-confs/presentation-slides.nginx index a04ba59318..4f0713f0f1 100644 --- a/bigbluebutton-web/nginx-confs/presentation-slides.nginx +++ b/bigbluebutton-web/nginx-confs/presentation-slides.nginx @@ -27,9 +27,9 @@ } } - location ~^\/bigbluebutton\/presentation\/(?[A-Za-z0-9\-]+)\/(?[A-Za-z0-9\-]+)\/(?[A-Za-z0-9\-]+)\/pdf\/(?[A-Za-z0-9]+)$ { + location ~^\/bigbluebutton\/presentation\/(?[A-Za-z0-9\-]+)\/(?[A-Za-z0-9\-]+)\/(?[A-Za-z0-9\-]+)\/pdf\/(?[A-Za-z0-9]+)/(?.*)$ { default_type application/pdf; - alias /var/bigbluebutton/$meeting_id_2/$meeting_id_2/$pres_id/pdfs/annotated_slides_$job_id.pdf; + alias /var/bigbluebutton/$meeting_id_2/$meeting_id_2/$pres_id/pdfs/$job_id/annotated_$filename.pdf; if ($bbb_loadbalancer_node) { add_header 'Access-Control-Allow-Origin' $bbb_loadbalancer_node always; } diff --git a/export-annotations/master.js b/export-annotations/master.js index 1f1548c683..95ab75c510 100644 --- a/export-annotations/master.js +++ b/export-annotations/master.js @@ -4,13 +4,14 @@ 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 export-annotations"); const kickOffCollectorWorker = (jobId) => { return new Promise((resolve, reject) => { - const worker = new Worker('./workers/collector.js', { workerData: jobId }); + const worker = new Worker(path.join(__dirname, 'workers', 'collector.js'), { workerData: jobId }); worker.on('message', resolve); worker.on('error', reject); worker.on('exit', (code) => { @@ -44,11 +45,11 @@ const kickOffCollectorWorker = (jobId) => { const exportJob = JSON.parse(job.element); // Create folder in dropbox - let dropbox = `${config.shared.presAnnDropboxDir}/${exportJob.jobId}` + let dropbox = path.join(config.shared.presAnnDropboxDir, exportJob.jobId); fs.mkdirSync(dropbox, { recursive: true }) // Drop job into dropbox as JSON - fs.writeFile(`${dropbox}/job`, job.element, function(err) { + fs.writeFile(path.join(dropbox, 'job'), job.element, function(err) { if(err) { return logger.error(err); } }); diff --git a/export-annotations/package-lock.json b/export-annotations/package-lock.json index c43e427511..2d944eaef3 100644 --- a/export-annotations/package-lock.json +++ b/export-annotations/package-lock.json @@ -12,6 +12,7 @@ "form-data": "^4.0.0", "image-size": "^1.0.1", "redis": "^4.0.3", + "sanitize-filename": "^1.6.3", "xmlbuilder2": "^3.0.2" } }, @@ -288,11 +289,32 @@ "node": ">=4" } }, + "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/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, + "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", @@ -529,11 +551,32 @@ "redis-errors": "^1.0.0" } }, + "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" + } + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, + "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", diff --git a/export-annotations/package.json b/export-annotations/package.json index 927748598f..e965ff6df6 100644 --- a/export-annotations/package.json +++ b/export-annotations/package.json @@ -10,6 +10,7 @@ "form-data": "^4.0.0", "image-size": "^1.0.1", "redis": "^4.0.3", + "sanitize-filename": "^1.6.3", "xmlbuilder2": "^3.0.2" } } diff --git a/export-annotations/workers/collector.js b/export-annotations/workers/collector.js index 838f89d45c..9a760d0399 100644 --- a/export-annotations/workers/collector.js +++ b/export-annotations/workers/collector.js @@ -3,8 +3,8 @@ const config = require('../config'); const fs = require('fs'); const redis = require('redis'); const { execSync } = require("child_process"); - const { Worker, workerData, parentPort } = require('worker_threads') +const path = require('path'); const jobId = workerData; @@ -23,10 +23,10 @@ const kickOffProcessWorker = (jobId) => { }) } -let dropbox = `${config.shared.presAnnDropboxDir}/${jobId}` +let dropbox = path.join(config.shared.presAnnDropboxDir, jobId); // Takes the Job from the dropbox -let job = fs.readFileSync(`${dropbox}/job`); +let job = fs.readFileSync(path.join(dropbox, 'job')); let exportJob = JSON.parse(job); // Collect the annotations from Redis @@ -51,17 +51,17 @@ let exportJob = JSON.parse(job); let whiteboard = JSON.parse(annotations); let pages = JSON.parse(whiteboard.pages); - fs.writeFile(`${dropbox}/whiteboard`, annotations, function(err) { + fs.writeFile(path.join(dropbox, 'whiteboard'), annotations, function(err) { if(err) { return logger.error(err); } }); // Collect the Presentation Page files from the presentation directory - let path = `${exportJob.presLocation}/${exportJob.presId}`; - let pdfFileExists = fs.existsSync(`${path}.pdf`); + let presentationFile = path.join(exportJob.presLocation, exportJob.presId); + let pdfFileExists = fs.existsSync(`${presentationFile}.pdf`); for (let p of pages) { let pageNumber = p.page; - let file = `${dropbox}/slide${pageNumber}`; + let outputFile = path.join(dropbox, `slide${pageNumber}`); if(pdfFileExists) { let extactSlideAsPDFCommands = [ @@ -70,8 +70,8 @@ let exportJob = JSON.parse(job); '-f', pageNumber, '-l', pageNumber, '-singlefile', - `${path}.pdf`, - file + `${presentationFile}.pdf`, + outputFile ].join(' '); execSync(extactSlideAsPDFCommands, (error, stderr) => { @@ -85,19 +85,19 @@ let exportJob = JSON.parse(job); }) } - else if (fs.existsSync(`${path}.png`)) { - fs.copyFileSync(`${path}.png`, `${file}.png`); + else if (fs.existsSync(`${presentationFile}.png`)) { + fs.copyFileSync(`${presentationFile}.png`, `${outputFile}.png`); } - else if (fs.existsSync(`${path}.jpeg`)) { + else if (fs.existsSync(`${presentationFile}.jpeg`)) { let convertImageToPngCommands = [ 'convert', - `${path}.jpeg`, + `${presentationFile}.jpeg`, '-background', 'white', '-resize', '1600x1600', '-auto-orient', '-flatten', - `${file}.png` + `${outputFile}.png` ].join(' '); execSync(convertImageToPngCommands, (error, stderr) => { diff --git a/export-annotations/workers/notifier.js b/export-annotations/workers/notifier.js index 3c7d8aca46..31c7cc24f6 100644 --- a/export-annotations/workers/notifier.js +++ b/export-annotations/workers/notifier.js @@ -4,15 +4,16 @@ 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] = workerData; +const [jobType, jobId, filename] = workerData; const logger = new Logger('presAnn Notifier Worker'); const dropbox = `${config.shared.presAnnDropboxDir}/${jobId}` -let job = fs.readFileSync(`${dropbox}/job`); +let job = fs.readFileSync(path.join(dropbox, 'job')); let exportJob = JSON.parse(job); async function notifyMeetingActor() { @@ -25,7 +26,7 @@ async function notifyMeetingActor() { 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}`; + let link = `${config.notifier.protocol}://${config.notifier.host}/bigbluebutton/presentation/${exportJob.parentMeetingId}/${exportJob.parentMeetingId}/${exportJob.presId}/pdf/${jobId}/${filename}`; // Notify Meeting Actor of file availability by sending a message through Redis PubSub const notification = { envelope: { @@ -56,13 +57,10 @@ async function upload(exportJob) { let callbackUrl = `http://${config.bbbWeb.host}:${config.bbbWeb.port}/bigbluebutton/presentation/${exportJob.presentationUploadToken}/upload` let formData = new FormData(); - formData.append('presentation_name', 'annotated_slides.pdf'); - formData.append('Filename', 'annotated_slides'); formData.append('conference', exportJob.parentMeetingId); - formData.append('room', exportJob.parentMeetingId); formData.append('pod_id', config.notifier.pod_id); formData.append('is_downloadable', config.notifier.is_downloadable); - formData.append('fileUpload', fs.createReadStream(`${exportJob.presLocation}/pdfs/annotated_slides_${jobId}.pdf`)); + 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}`); diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js index 0389fe00cc..ab6027f3e0 100644 --- a/export-annotations/workers/process.js +++ b/export-annotations/workers/process.js @@ -5,6 +5,8 @@ const sizeOf = require('image-size'); 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 jobId = workerData; const MAGIC_MYSTERY_NUMBER = 2; @@ -12,9 +14,9 @@ const MAGIC_MYSTERY_NUMBER = 2; const logger = new Logger('presAnn Process Worker'); logger.info("Processing PDF for job " + jobId); -const kickOffNotifierWorker = (jobType) => { +const kickOffNotifierWorker = (jobType, sanitizedFilename) => { return new Promise((resolve, reject) => { - const worker = new Worker('./workers/notifier.js', { workerData: [jobType, jobId] }); + const worker = new Worker('./workers/notifier.js', { workerData: [jobType, jobId, sanitizedFilename] }); worker.on('message', resolve); worker.on('error', reject); worker.on('exit', (code) => { @@ -43,7 +45,7 @@ function render_HTMLTextBox(htmlFilePath, id, width, height) { '--crop-h', height, '--log-level', 'none', '--quality', '100', - htmlFilePath, `${dropbox}/text${id}.png` + htmlFilePath, path.join(dropbox, `text${id}.png`) ] execSync(commands.join(' '), (error, stderr) => { @@ -176,8 +178,8 @@ function overlay_poll(svg, annotation, w, h) { let poll_width = Math.round(scale_shape(w, annotation.points[2])); let poll_height = Math.round(scale_shape(h, annotation.points[3])); let pollId = annotation.id.replace(/\//g, ''); - let pollSVG = `${dropbox}/poll-${pollId}.svg` - let pollJSON = `${dropbox}/poll-${pollId}.json` + let pollSVG = path.join(dropbox, `poll-${pollId}.svg`); + let pollJSON = path.join(dropbox, `poll-${pollId}.json`); // Rename 'numVotes' key to 'num_votes' let pollJSONContent = annotation.result.map(result => { @@ -281,7 +283,7 @@ function overlay_text(svg, annotation, w, h) {

`; - var htmlFilePath = `${dropbox}/text${annotation.id}.html` + var htmlFilePath = path.join(dropbox, `text${annotation.id}.html`) fs.writeFileSync(htmlFilePath, html, function (err) { if (err) logger.error(err) @@ -331,19 +333,19 @@ function overlay_annotations(svg, currentSlideAnnotations, w, h) { // Process the presentation pages and annotations into a PDF file // 1. Get the job -const dropbox = `${config.shared.presAnnDropboxDir}/${jobId}` -let job = fs.readFileSync(`${dropbox}/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(`${dropbox}/whiteboard`); +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) { - var dimensions = sizeOf(`${dropbox}/slide${currentSlide.page}.png`); + var dimensions = sizeOf(path.join(dropbox, `slide${currentSlide.page}.png`)); var slideWidth = dimensions.width; var slideHeight = dimensions.height; @@ -381,8 +383,8 @@ for (let currentSlide of pages) { svg = svg.end({ prettyPrint: true }); // Write annotated SVG file - let SVGfile = `${dropbox}/annotated-slide${currentSlide.page}.svg` - let PDFfile = `${dropbox}/annotated-slide${currentSlide.page}.pdf` + 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); } @@ -408,14 +410,16 @@ for (let currentSlide of pages) { } // Create PDF output directory if it doesn't exist -let output_dir = `${exportJob.presLocation}/pdfs`; -if (!fs.existsSync(output_dir)) { fs.mkdirSync(output_dir); } +let output_dir = path.join(exportJob.presLocation, 'pdfs', jobId); +let filename = sanitize(`annotated_${exportJob.meetingName}_${path.parse(exportJob.presName).name}`).replace(/\s/g, '_'); + +if (!fs.existsSync(output_dir)) { fs.mkdirSync(output_dir, { recursive: true }); } let mergePDFs = [ 'gs', '-dNOPAUSE', '-sDEVICE=pdfwrite', - `-sOUTPUTFILE=${output_dir}/annotated_slides_${jobId}.pdf`, + `-sOUTPUTFILE="${path.join(output_dir, `${filename}.pdf`)}"`, `-dBATCH`, ghostScriptInput, ].join(' '); @@ -432,8 +436,8 @@ execSync(mergePDFs, (error, stderr) => { }); // Launch Notifier Worker depending on job type -logger.info(`Saved PDF at ${output_dir}/annotated_slides_${jobId}.pdf`); +logger.info(`Saved PDF at ${output_dir}/${jobId}/${filename}.pdf`); -kickOffNotifierWorker(exportJob.jobType); +kickOffNotifierWorker(exportJob.jobType, filename); parentPort.postMessage({ message: workerData }) From 19bbb4115dbb2789446f3b59ae2877f682a9980d Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Sat, 30 Apr 2022 23:28:11 +0200 Subject: [PATCH 064/268] Refactor handlers, role validation --- .../PresentationPodHdlrs.scala | 1 + .../PresentationWithAnnotationsMsgHdlr.scala | 173 ++++++++++++++++++ .../StoreExportJobInRedisPresAnnEvent.scala | 11 +- .../core/running/MeetingActor.scala | 157 +--------------- .../redis/ExportAnnotationsActor.scala | 3 +- .../common2/msgs/WhiteboardMsgs.scala | 3 +- .../nginx-confs/presentation-slides.nginx | 4 +- export-annotations/workers/notifier.js | 2 +- export-annotations/workers/process.js | 9 +- 9 files changed, 190 insertions(+), 173 deletions(-) create mode 100644 akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationWithAnnotationsMsgHdlr.scala diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPodHdlrs.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPodHdlrs.scala index cf079dc83c..10a63694bb 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPodHdlrs.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPodHdlrs.scala @@ -19,6 +19,7 @@ class PresentationPodHdlrs(implicit val context: ActorContext) with PresentationPageCountErrorPubMsgHdlr with PresentationUploadedFileTooLargeErrorPubMsgHdlr with PresentationUploadTokenReqMsgHdlr + with PresentationWithAnnotationsMsgHdlr with ResizeAndMovePagePubMsgHdlr with SyncGetPresentationPodsMsgHdlr with RemovePresentationPodPubMsgHdlr diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationWithAnnotationsMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationWithAnnotationsMsgHdlr.scala new file mode 100644 index 0000000000..6f87696a47 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationWithAnnotationsMsgHdlr.scala @@ -0,0 +1,173 @@ +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 } + +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 handleMakePresentationWithAnnotationDownloadReqMsg(m: MakePresentationWithAnnotationDownloadReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { + + val meetingId = liveMeeting.props.meetingProp.intId + + // Whiteboard ID + val presId: String = m.body.presId match { + case "" => PresentationPodsApp.getAllPresentationPodsInMeeting(state).flatMap(_.getCurrentPresentation.map(_.id)).mkString + case _ => m.body.presId + } + + val allPages: Boolean = m.body.allPages // Whether or not all pages of the presentation should be exported + val pages: List[Int] = m.body.pages // Desired presentation pages for export + + // Determine page amount + val presentationPods: Vector[PresentationPod] = state.presentationPodManager.getAllPresentationPodsInMeeting() + + val currentPres = presentationPods.flatMap(_.getCurrentPresentation()).headOption + + currentPres match { + case None => + log.error(s"No presentation set in meeting ${meetingId}") + return + case _ => () + } + + val pageCount = currentPres.get.pages.size + val pagesRange: List[Int] = if (allPages) (1 to pageCount).toList else pages + + var storeAnnotationPages = new Array[PresentationPageForExport](pagesRange.size) + var resultingPage = 0 + + for (pageNumber <- pagesRange) { + if (pageNumber < 1 || pageNumber > pageCount) { + println(pagesRange.length) + log.error(s"Page ${pageNumber} requested for export out of range, aborting") + return + } + + var whiteboardId = s"${presId}/${pageNumber.toString}" + val presentationPage: PresentationPage = currentPres.get.pages(whiteboardId) + val xOffset: Double = presentationPage.xOffset + val yOffset: Double = presentationPage.yOffset + val widthRatio: Double = presentationPage.widthRatio + val heightRatio: Double = presentationPage.heightRatio + val whiteboardHistory: Array[AnnotationVO] = liveMeeting.wbModel.getHistory(whiteboardId) + + storeAnnotationPages(resultingPage) = new PresentationPageForExport(pageNumber, xOffset, yOffset, widthRatio, heightRatio, whiteboardHistory) + resultingPage += 1 + } + + val jobId = RandomStringGenerator.randomAlphanumericString(16) + + // 1) Send Annotations to Redis + var annotations = new StoredAnnotations(jobId, presId, storeAnnotationPages) + bus.outGW.send(buildStoreAnnotationsInRedisSysMsg(annotations, liveMeeting)) + + // 2) Insert Export Job in Redis + val jobType = "PresentationWithAnnotationDownloadJob" + val presLocation = s"/var/bigbluebutton/${meetingId}/${meetingId}/${presId}" + val exportJob = new ExportJob(jobId, jobType, "annotated_slides", presId, presLocation, allPages, pagesRange, meetingId, "") + var job = buildStoreExportJobInRedisSysMsg(exportJob, liveMeeting) + bus.outGW.send(job) + } + + def handleExportPresentationWithAnnotationReqMsg(m: ExportPresentationWithAnnotationReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { + + val meetingId = liveMeeting.props.meetingProp.intId + val userId = m.header.userId + + 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) + return + } + + val parentMeetingId: String = m.body.parentMeetingId + + val presId: String = PresentationPodsApp.getAllPresentationPodsInMeeting(state).flatMap(_.getCurrentPresentation.map(_.id)).mkString + val allPages: Boolean = m.body.allPages + + val presentationPods: Vector[PresentationPod] = state.presentationPodManager.getAllPresentationPodsInMeeting() + val currentPres = presentationPods.flatMap(_.getCurrentPresentation()).headOption + + currentPres match { + case None => + log.error(s"No presentation set in meeting ${meetingId}") + return + case _ => () + } + + val currentPage: PresentationPage = PresentationInPod.getCurrentPage(currentPres.get).get + + val pageCount = currentPres.get.pages.size + val pagesRange: List[Int] = if (allPages) (1 to pageCount).toList else List(currentPage.num) + + var storeAnnotationPages = new Array[PresentationPageForExport](pagesRange.size) + var resultingPage = 0 + + for (pageNumber <- pagesRange) { + var whiteboardId = s"${presId}/${pageNumber.toString}" + val presentationPage: PresentationPage = currentPres.get.pages(whiteboardId) + val xOffset: Double = presentationPage.xOffset + val yOffset: Double = presentationPage.yOffset + val widthRatio: Double = presentationPage.widthRatio + val heightRatio: Double = presentationPage.heightRatio + val whiteboardHistory: Array[AnnotationVO] = liveMeeting.wbModel.getHistory(whiteboardId) + + storeAnnotationPages(resultingPage) = new PresentationPageForExport(pageNumber, xOffset, yOffset, widthRatio, heightRatio, whiteboardHistory) + resultingPage += 1 + } + + val presentationUploadToken: String = PresentationPodsApp.generateToken("DEFAULT_PRESENTATION_POD", userId) + val jobId = RandomStringGenerator.randomAlphanumericString(16) + val filename: String = s"${liveMeeting.props.meetingProp.name}-annotated" + + // 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)) + + // 1) Send Annotations to Redis + var annotations = new StoredAnnotations(jobId, presId, storeAnnotationPages) + bus.outGW.send(buildStoreAnnotationsInRedisSysMsg(annotations, liveMeeting)) + + // 2) Insert Export Job in Redis + val jobType: String = "PresentationWithAnnotationExportJob" + val presLocation = s"/var/bigbluebutton/${meetingId}/${meetingId}/${presId}" + val exportJob = new ExportJob(jobId, jobType, filename, presId, presLocation, allPages, pagesRange, parentMeetingId, presentationUploadToken) + var job = buildStoreExportJobInRedisSysMsg(exportJob, liveMeeting) + bus.outGW.send(job) + } + +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StoreExportJobInRedisPresAnnEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StoreExportJobInRedisPresAnnEvent.scala index 8d01bcc9bc..d6a97bbabd 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StoreExportJobInRedisPresAnnEvent.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StoreExportJobInRedisPresAnnEvent.scala @@ -35,12 +35,8 @@ class StoreExportJobInRedisPresAnnEvent extends AbstractPresentationWithAnnotati eventMap.put(JOB_TYPE, jobType) } - def setMeetingName(meetingName: String) { - eventMap.put(MEETING_NAME, meetingName) - } - - def setPresName(presName: String) { - eventMap.put(PRES_NAME, presName) + def setFilename(filename: String) { + eventMap.put(FILENAME, filename) } def setPresId(presId: String) { @@ -71,8 +67,7 @@ class StoreExportJobInRedisPresAnnEvent extends AbstractPresentationWithAnnotati object StoreExportJobInRedisPresAnnEvent { protected final val JOB_ID = "jobId" protected final val JOB_TYPE = "jobType" - protected final val MEETING_NAME = "meetingName" - protected final val PRES_NAME = "presName" + protected final val FILENAME = "filename" protected final val PRES_ID = "presId" protected final val PRES_LOCATION = "presLocation" protected final val ALL_PAGES = "allPages" diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index f69b0d09dc..9511c12c69 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -24,7 +24,6 @@ import org.bigbluebutton.core.apps.webcam.WebcamApp2x import org.bigbluebutton.core.apps.whiteboard.WhiteboardApp2x import org.bigbluebutton.core.bus._ import org.bigbluebutton.core.models.{ Users2x, VoiceUsers, _ } -import org.bigbluebutton.core.util.RandomStringGenerator import org.bigbluebutton.core2.{ MeetingStatus2x, Permissions } import org.bigbluebutton.core2.message.handlers._ import org.bigbluebutton.core2.message.handlers.meeting._ @@ -496,8 +495,8 @@ class MeetingActor( // Presentation case m: PreuploadedPresentationsSysPubMsg => presentationApp2x.handle(m, liveMeeting, msgBus) case m: AssignPresenterReqMsg => state = handlePresenterChange(m, state) - case m: MakePresentationWithAnnotationDownloadReqMsg => handleMakePresentationWithAnnotationDownloadReqMsg(m, state, liveMeeting) - case m: ExportPresentationWithAnnotationReqMsg => handleExportPresentationWithAnnotationReqMsg(m, state, liveMeeting) + case m: MakePresentationWithAnnotationDownloadReqMsg => presentationPodsApp.handleMakePresentationWithAnnotationDownloadReqMsg(m, state, liveMeeting, msgBus) + case m: ExportPresentationWithAnnotationReqMsg => presentationPodsApp.handleExportPresentationWithAnnotationReqMsg(m, state, liveMeeting, msgBus) case m: NewPresAnnFileAvailableMsg => log.info("***** New PDF with annotations available.") // Presentation Pods @@ -721,158 +720,6 @@ class MeetingActor( } - def buildStoreAnnotationsInRedisSysMsg(annotations: StoredAnnotations): 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): 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 handleMakePresentationWithAnnotationDownloadReqMsg(m: MakePresentationWithAnnotationDownloadReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = { - - val meetingId = liveMeeting.props.meetingProp.intId - val meetingName: String = liveMeeting.props.meetingProp.name - - // Whiteboard ID - val presId: String = m.body.presId match { - case "" => getMeetingInfoPresentationDetails().id - case _ => m.body.presId - } - - val allPages: Boolean = m.body.allPages // Whether or not all pages of the presentation should be exported - val pages: List[Int] = m.body.pages // Desired presentation pages for export - - // Determine page amount - val presentationPods: Vector[PresentationPod] = state.presentationPodManager.getAllPresentationPodsInMeeting() - - val currentPres = presentationPods.flatMap(_.getCurrentPresentation()).headOption - - currentPres match { - case None => - log.error(s"No presentation set in meeting ${meetingId}") - return - case _ => () - } - - val pageCount = currentPres.get.pages.size - val pagesRange: List[Int] = if (allPages) (1 to pageCount).toList else pages - - var storeAnnotationPages = new Array[PresentationPageForExport](pagesRange.size) - var resultingPage = 0 - - for (pageNumber <- pagesRange) { - if (pageNumber < 1 || pageNumber > pageCount) { - println(pagesRange.length) - log.error(s"Page ${pageNumber} requested for export out of range, aborting") - return - } - - var whiteboardId = s"${presId}/${pageNumber.toString}" - val presentationPage: PresentationPage = currentPres.get.pages(whiteboardId) - val xOffset: Double = presentationPage.xOffset - val yOffset: Double = presentationPage.yOffset - val widthRatio: Double = presentationPage.widthRatio - val heightRatio: Double = presentationPage.heightRatio - val whiteboardHistory: Array[AnnotationVO] = liveMeeting.wbModel.getHistory(whiteboardId) - - storeAnnotationPages(resultingPage) = new PresentationPageForExport(pageNumber, xOffset, yOffset, widthRatio, heightRatio, whiteboardHistory) - resultingPage += 1 - } - - val jobId = RandomStringGenerator.randomAlphanumericString(16) - - // 1) Send Annotations to Redis - var annotations = new StoredAnnotations(jobId, presId, storeAnnotationPages) - outGW.send(buildStoreAnnotationsInRedisSysMsg(annotations)) - - // 2) Insert Export Job in Redis - val jobType = "PresentationWithAnnotationDownloadJob" - val presLocation = s"/var/bigbluebutton/${meetingId}/${meetingId}/${presId}" - val exportJob = new ExportJob(jobId, jobType, meetingName, currentPres.get.name, presId, presLocation, allPages, pagesRange, meetingId, "") - var job = buildStoreExportJobInRedisSysMsg(exportJob) - outGW.send(job) - } - - def handleExportPresentationWithAnnotationReqMsg(m: ExportPresentationWithAnnotationReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = { - - val meetingId = liveMeeting.props.meetingProp.intId - val meetingName: String = liveMeeting.props.meetingProp.name - val userId = m.header.userId - val presId: String = getMeetingInfoPresentationDetails.id - val parentMeetingId: String = m.body.parentMeetingId - val allPages: Boolean = m.body.allPages - - val presentationPods: Vector[PresentationPod] = state.presentationPodManager.getAllPresentationPodsInMeeting() - val currentPres = presentationPods.flatMap(_.getCurrentPresentation()).headOption - - currentPres match { - case None => - log.error(s"No presentation set in meeting ${meetingId}") - return - case _ => () - } - - val currentPage: PresentationPage = PresentationInPod.getCurrentPage(currentPres.get).get - - val pageCount = currentPres.get.pages.size - val pagesRange: List[Int] = if (allPages) (1 to pageCount).toList else List(currentPage.num) - - var storeAnnotationPages = new Array[PresentationPageForExport](pagesRange.size) - var resultingPage = 0 - - for (pageNumber <- pagesRange) { - var whiteboardId = s"${presId}/${pageNumber.toString}" - val presentationPage: PresentationPage = currentPres.get.pages(whiteboardId) - val xOffset: Double = presentationPage.xOffset - val yOffset: Double = presentationPage.yOffset - val widthRatio: Double = presentationPage.widthRatio - val heightRatio: Double = presentationPage.heightRatio - val whiteboardHistory: Array[AnnotationVO] = liveMeeting.wbModel.getHistory(whiteboardId) - - storeAnnotationPages(resultingPage) = new PresentationPageForExport(pageNumber, xOffset, yOffset, widthRatio, heightRatio, whiteboardHistory) - resultingPage += 1 - } - - val presentationUploadToken: String = PresentationPodsApp.generateToken("DEFAULT_PRESENTATION_POD", userId) - val jobId = RandomStringGenerator.randomAlphanumericString(16) - - // 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 - outGW.send(buildPresentationUploadTokenSysPubMsg(parentMeetingId, userId, presentationUploadToken, currentPres.get.name)) - - // 1) Send Annotations to Redis - var annotations = new StoredAnnotations(jobId, presId, storeAnnotationPages) - outGW.send(buildStoreAnnotationsInRedisSysMsg(annotations)) - - // 2) Insert Export Job in Redis - val jobType: String = "PresentationWithAnnotationExportJob" - val presLocation = s"/var/bigbluebutton/${meetingId}/${meetingId}/${presId}" - val exportJob = new ExportJob(jobId, jobType, meetingName, currentPres.get.name, presId, presLocation, allPages, pagesRange, parentMeetingId, presentationUploadToken) - var job = buildStoreExportJobInRedisSysMsg(exportJob) - outGW.send(job) - } - def handleMonitorNumberOfUsers(msg: MonitorNumberOfUsersInternalMsg) { state = removeUsersWithExpiredUserLeftFlag(liveMeeting, state) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/ExportAnnotationsActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/ExportAnnotationsActor.scala index 7b7cb40159..ccac60a14a 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/ExportAnnotationsActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/ExportAnnotationsActor.scala @@ -75,8 +75,7 @@ class ExportAnnotationsActor( ev.setJobId(msg.body.exportJob.jobId) ev.setJobType(msg.body.exportJob.jobType) - ev.setMeetingName(msg.body.exportJob.meetingName) - ev.setPresName(msg.body.exportJob.presName) + 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) diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala index eb62113ab4..c31be07276 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala @@ -21,8 +21,7 @@ case class StoredAnnotations( case class ExportJob( jobId: String, jobType: String, - meetingName: String, - presName: String, + filename: String, presId: String, presLocation: String, allPages: Boolean, diff --git a/bigbluebutton-web/nginx-confs/presentation-slides.nginx b/bigbluebutton-web/nginx-confs/presentation-slides.nginx index 4f0713f0f1..f12930a6b6 100644 --- a/bigbluebutton-web/nginx-confs/presentation-slides.nginx +++ b/bigbluebutton-web/nginx-confs/presentation-slides.nginx @@ -27,9 +27,9 @@ } } - location ~^\/bigbluebutton\/presentation\/(?[A-Za-z0-9\-]+)\/(?[A-Za-z0-9\-]+)\/(?[A-Za-z0-9\-]+)\/pdf\/(?[A-Za-z0-9]+)/(?.*)$ { + location ~^\/bigbluebutton\/presentation\/(?[A-Za-z0-9\-]+)\/(?[A-Za-z0-9\-]+)\/(?[A-Za-z0-9\-]+)\/pdf\/(?[A-Za-z0-9]+)\/annotated_slides.pdf$ { default_type application/pdf; - alias /var/bigbluebutton/$meeting_id_2/$meeting_id_2/$pres_id/pdfs/$job_id/annotated_$filename.pdf; + alias /var/bigbluebutton/$meeting_id_2/$meeting_id_2/$pres_id/pdfs/$job_id/annotated_slides.pdf; if ($bbb_loadbalancer_node) { add_header 'Access-Control-Allow-Origin' $bbb_loadbalancer_node always; } diff --git a/export-annotations/workers/notifier.js b/export-annotations/workers/notifier.js index 31c7cc24f6..474516a560 100644 --- a/export-annotations/workers/notifier.js +++ b/export-annotations/workers/notifier.js @@ -26,7 +26,7 @@ async function notifyMeetingActor() { 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}`; + 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: { diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js index ab6027f3e0..a095df4cd6 100644 --- a/export-annotations/workers/process.js +++ b/export-annotations/workers/process.js @@ -14,9 +14,9 @@ const MAGIC_MYSTERY_NUMBER = 2; const logger = new Logger('presAnn Process Worker'); logger.info("Processing PDF for job " + jobId); -const kickOffNotifierWorker = (jobType, sanitizedFilename) => { +const kickOffNotifierWorker = (jobType, filename) => { return new Promise((resolve, reject) => { - const worker = new Worker('./workers/notifier.js', { workerData: [jobType, jobId, sanitizedFilename] }); + const worker = new Worker('./workers/notifier.js', { workerData: [jobType, jobId, filename] }); worker.on('message', resolve); worker.on('error', reject); worker.on('exit', (code) => { @@ -411,10 +411,13 @@ for (let currentSlide of pages) { // Create PDF output directory if it doesn't exist let output_dir = path.join(exportJob.presLocation, 'pdfs', jobId); -let filename = sanitize(`annotated_${exportJob.meetingName}_${path.parse(exportJob.presName).name}`).replace(/\s/g, '_'); if (!fs.existsSync(output_dir)) { fs.mkdirSync(output_dir, { recursive: true }); } +let filename = sanitize(exportJob.filename.replace(/\s/g, '_')); + +console.log(filename) + let mergePDFs = [ 'gs', '-dNOPAUSE', From da12af5141570895c37b422d9c8fe7ab187a49fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emre=20S=C3=BCl=C3=BCn?= Date: Mon, 2 May 2022 16:46:32 +0300 Subject: [PATCH 065/268] Fix typo in the issue template chooser --- .github/ISSUE_TEMPLATE/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 82ddfb55ba..27d6d37ae3 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -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 \ No newline at end of file + about: List of companies offering commercial BigBlueButton support From c2db1575a438315b508fe12c6f9aef74b94a9eba Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Tue, 3 May 2022 18:46:45 +0200 Subject: [PATCH 066/268] Render emojis properly --- export-annotations/package-lock.json | 117 ++++++++++++++++++++++++++ export-annotations/package.json | 1 + export-annotations/workers/process.js | 11 ++- 3 files changed, 127 insertions(+), 2 deletions(-) diff --git a/export-annotations/package-lock.json b/export-annotations/package-lock.json index 2d944eaef3..21fe7f96a1 100644 --- a/export-annotations/package-lock.json +++ b/export-annotations/package-lock.json @@ -13,6 +13,7 @@ "image-size": "^1.0.1", "redis": "^4.0.3", "sanitize-filename": "^1.6.3", + "twemoji": "^14.0.2", "xmlbuilder2": "^3.0.2" } }, @@ -203,6 +204,27 @@ "node": ">= 6" } }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-extra/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/generic-pool": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.8.2.tgz", @@ -211,6 +233,11 @@ "node": ">= 4" } }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + }, "node_modules/image-size": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.1.tgz", @@ -230,6 +257,17 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/jsonfile": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-5.0.0.tgz", + "integrity": "sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==", + "dependencies": { + "universalify": "^0.1.2" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/mime-db": { "version": "1.51.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", @@ -310,6 +348,30 @@ "utf8-byte-length": "^1.0.1" } }, + "node_modules/twemoji": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/twemoji/-/twemoji-14.0.2.tgz", + "integrity": "sha512-BzOoXIe1QVdmsUmZ54xbEH+8AgtOKUiG53zO5vVP2iUu6h5u9lN15NcuS6te4OY96qx0H7JK9vjjl9WQbkTRuA==", + "dependencies": { + "fs-extra": "^8.0.1", + "jsonfile": "^5.0.0", + "twemoji-parser": "14.0.0", + "universalify": "^0.1.2" + } + }, + "node_modules/twemoji-parser": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/twemoji-parser/-/twemoji-parser-14.0.0.tgz", + "integrity": "sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA==" + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/utf8-byte-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", @@ -486,11 +548,36 @@ "mime-types": "^2.1.12" } }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "dependencies": { + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + } + } + }, "generic-pool": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.8.2.tgz", "integrity": "sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg==" }, + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + }, "image-size": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.1.tgz", @@ -504,6 +591,15 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "jsonfile": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-5.0.0.tgz", + "integrity": "sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^0.1.2" + } + }, "mime-db": { "version": "1.51.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", @@ -572,6 +668,27 @@ "utf8-byte-length": "^1.0.1" } }, + "twemoji": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/twemoji/-/twemoji-14.0.2.tgz", + "integrity": "sha512-BzOoXIe1QVdmsUmZ54xbEH+8AgtOKUiG53zO5vVP2iUu6h5u9lN15NcuS6te4OY96qx0H7JK9vjjl9WQbkTRuA==", + "requires": { + "fs-extra": "^8.0.1", + "jsonfile": "^5.0.0", + "twemoji-parser": "14.0.0", + "universalify": "^0.1.2" + } + }, + "twemoji-parser": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/twemoji-parser/-/twemoji-parser-14.0.0.tgz", + "integrity": "sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA==" + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, "utf8-byte-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", diff --git a/export-annotations/package.json b/export-annotations/package.json index e965ff6df6..4d037c71cb 100644 --- a/export-annotations/package.json +++ b/export-annotations/package.json @@ -11,6 +11,7 @@ "image-size": "^1.0.1", "redis": "^4.0.3", "sanitize-filename": "^1.6.3", + "twemoji": "^14.0.2", "xmlbuilder2": "^3.0.2" } } diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js index a095df4cd6..98b57d5267 100644 --- a/export-annotations/workers/process.js +++ b/export-annotations/workers/process.js @@ -7,6 +7,7 @@ const { execSync } = require("child_process"); const { Worker, workerData, parentPort } = require('worker_threads'); const path = require('path'); const sanitize = require("sanitize-filename"); +const twemoji = require("twemoji") const jobId = workerData; const MAGIC_MYSTERY_NUMBER = 2; @@ -275,13 +276,19 @@ function overlay_text(svg, annotation, w, h) { `font-size:${fontSize}px` ] - var html = + var html = twemoji.parse( ` +

${annotation.text.split('\n').join('
')}

- `; + `); var htmlFilePath = path.join(dropbox, `text${annotation.id}.html`) From 48f05962a06203159c157d01b1a4fd0a15fd333c Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Wed, 4 May 2022 18:45:04 +0200 Subject: [PATCH 067/268] Add sequence number to exported PDFs --- .../PresentationWithAnnotationsMsgHdlr.scala | 9 ++++++++- .../org/bigbluebutton/core/models/PresentationPods.scala | 3 +++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationWithAnnotationsMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationWithAnnotationsMsgHdlr.scala index 6f87696a47..a2e09f4515 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationWithAnnotationsMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationWithAnnotationsMsgHdlr.scala @@ -153,7 +153,14 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait { val presentationUploadToken: String = PresentationPodsApp.generateToken("DEFAULT_PRESENTATION_POD", userId) val jobId = RandomStringGenerator.randomAlphanumericString(16) - val filename: String = s"${liveMeeting.props.meetingProp.name}-annotated" + + // Set filename, checking if it is already in use + var filename: String = liveMeeting.props.meetingProp.name + val duplicatedCount = presentationPods.flatMap(_.getPresentationsByFilename(filename)).size + filename = duplicatedCount match { + case 0 => filename + case _ => s"${filename}(${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)) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/PresentationPods.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/PresentationPods.scala index 76ba641965..136b40e708 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/PresentationPods.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/PresentationPods.scala @@ -83,6 +83,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 From 0c2de0f9c3b0090fafee6443f7d4e29925c8f32a Mon Sep 17 00:00:00 2001 From: Paul Trudel Date: Thu, 12 May 2022 15:40:57 +0000 Subject: [PATCH 068/268] Implemented some small suggested changes --- bbb-recording-imex/README.md | 15 ++++++++ bbb-recording-imex/deploy.sh | 3 +- .../java/org/bigbluebutton/RecordingApp.java | 35 +++++++++++++++---- 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/bbb-recording-imex/README.md b/bbb-recording-imex/README.md index eb6fce2942..f6238c1f12 100644 --- a/bbb-recording-imex/README.md +++ b/bbb-recording-imex/README.md @@ -16,6 +16,21 @@ Imports and parses recording metadata.xml files and stores the data in a Postgre - 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 } [-s ] [PATH] + Import/export recording(s) to/from PATH. The default PATH is + /var/bigbluebutton/published/presentation + -e export recording(s) + -i import recording(s) and indicate if they should be persisted [true|false] + -s 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 diff --git a/bbb-recording-imex/deploy.sh b/bbb-recording-imex/deploy.sh index d2183c3b65..36acff5c34 100755 --- a/bbb-recording-imex/deploy.sh +++ b/bbb-recording-imex/deploy.sh @@ -8,4 +8,5 @@ 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 +java -jar '${JAR_DIR}'/'${JAR_NAME} '"$@"'> ${RUN_DIR}/recording-imex.sh +chmod +x ${RUN_DIR}/recording-imex.sh diff --git a/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingApp.java b/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingApp.java index 6cb0a6a84f..5ae4ba6a5a 100755 --- a/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingApp.java +++ b/bbb-recording-imex/src/main/java/org/bigbluebutton/RecordingApp.java @@ -1,6 +1,9 @@ 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 { @@ -68,25 +71,43 @@ public class RecordingApp { } } - if (i == args.length) { - System.out.println("Error: Required arguments not specified"); - printUsage(); - return; - } else + 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 } [-s ] PATH"); - System.out.println("Import/export recording(s) to/from PATH"); + System.out.println("Usage: {-e|-i } [-s ] [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 import recording(s) and indicate if they should be persisted [true|false]"); System.out.println("-s 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(); From e070b586c401c24f8221831842f563d90086fcc2 Mon Sep 17 00:00:00 2001 From: Joao Victor Date: Fri, 13 May 2022 10:42:19 -0300 Subject: [PATCH 069/268] [2.5] fix: popover menus in RTL mode --- .../actions-bar/actions-dropdown/component.jsx | 7 ++++--- .../actions-bar/actions-dropdown/container.jsx | 4 +++- .../components/audio/audio-controls/component.jsx | 4 ++-- .../components/audio/audio-controls/container.jsx | 4 +++- .../input-stream-live-selector/component.jsx | 11 +++++++++++ .../breakout-room/breakout-dropdown/component.jsx | 5 +++-- .../ui/components/breakout-room/component.jsx | 2 ++ .../ui/components/breakout-room/container.jsx | 5 +++-- .../ui/components/chat/chat-dropdown/component.jsx | 5 +++-- .../ui/components/chat/chat-dropdown/container.jsx | 4 +++- .../components/common/button/button-emoji/styles.js | 10 ++++++++++ .../imports/ui/components/common/menu/component.jsx | 9 +++++---- .../imports/ui/components/layout/initState.js | 4 ++-- .../nav-bar/settings-dropdown/component.jsx | 13 ++++++++++++- .../nav-bar/settings-dropdown/container.jsx | 5 +++-- .../user-participants/user-options/component.jsx | 12 +++++++++++- .../user-participants/user-options/container.jsx | 4 ++++ .../video-list/video-list-item/component.jsx | 3 ++- .../video-list/video-list-item/container.jsx | 2 ++ .../video-list-item/user-actions/component.jsx | 7 ++++--- 20 files changed, 92 insertions(+), 28 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx index 7e9d16002a..b815510135 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx @@ -251,6 +251,7 @@ class ActionsDropdown extends PureComponent { isMeteorConnected, isDropdownOpen, isMobile, + isRTL, } = this.props; const availableActions = this.getAvailableActions(); @@ -263,7 +264,7 @@ class ActionsDropdown extends PureComponent { || !isMeteorConnected) { return null; } - const customStyles = { top: '-3rem' }; + const customStyles = { top: '-1rem' }; return ( ); diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/container.jsx index 985c0911f5..c29f13c4c2 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/container.jsx @@ -4,7 +4,7 @@ import Presentations from '/imports/api/presentations'; import PresentationUploaderService from '/imports/ui/components/presentation/presentation-uploader/service'; import PresentationPodService from '/imports/ui/components/presentation-pod/service'; import ActionsDropdown from './component'; -import { layoutSelectInput, layoutDispatch } from '../../layout/context'; +import { layoutSelectInput, layoutDispatch, layoutSelect } from '../../layout/context'; import getFromUserSettings from '/imports/ui/services/users-settings'; import { SMALL_VIEWPORT_BREAKPOINT } from '../../layout/enums'; @@ -14,6 +14,7 @@ const ActionsDropdownContainer = (props) => { const { width: browserWidth } = layoutSelectInput((i) => i.browser); const isMobile = browserWidth <= SMALL_VIEWPORT_BREAKPOINT; const layoutContextDispatch = layoutDispatch(); + const isRTL = layoutSelect((i) => i.isRTL); return ( { sidebarContent, sidebarNavigation, isMobile, + isRTL, ...props, }} /> diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx index f42e44b1d2..a5adaf4892 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx @@ -79,9 +79,9 @@ class AudioControls extends PureComponent { } static renderLeaveButtonWithLiveStreamSelector(props) { - const { handleLeaveAudio } = props; + const { handleLeaveAudio, isRTL } = props; return ( - + ); } diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx index a6bdc71692..d1f56b4805 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx @@ -14,6 +14,7 @@ import { setUserSelectedMicrophone, setUserSelectedListenOnly, } from '../audio-modal/service'; +import { layoutSelect } from '/imports/ui/components/layout/context'; import Service from '../service'; import AppService from '/imports/ui/components/app/service'; @@ -25,7 +26,8 @@ const AudioControlsContainer = (props) => { const { users, lockSettings, userLocks, children, ...newProps } = props; - return ; + const isRTL = layoutSelect((i) => i.isRTL); + return ; }; const handleLeaveAudio = () => { diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/input-stream-live-selector/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/input-stream-live-selector/component.jsx index 16be3f8bf1..8da5c5339d 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/input-stream-live-selector/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/input-stream-live-selector/component.jsx @@ -263,6 +263,7 @@ class InputStreamLiveSelector extends Component { currentInputDeviceId, currentOutputDeviceId, isListenOnly, + isRTL, } = this.props; const inputDeviceList = !isListenOnly @@ -313,6 +314,16 @@ class InputStreamLiveSelector extends Component { )} actions={dropdownListComplete} + opts={{ + id: 'default-dropdown-menu', + keepMounted: true, + transitionDuration: 0, + elevation: 3, + getContentAnchorEl: null, + fullwidth: 'true', + anchorOrigin: { vertical: 'top', horizontal: isRTL ? 'left' : 'right' }, + transformOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' }, + }} /> ); } diff --git a/bigbluebutton-html5/imports/ui/components/breakout-room/breakout-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/breakout-room/breakout-dropdown/component.jsx index d486604c09..06a02b49a5 100644 --- a/bigbluebutton-html5/imports/ui/components/breakout-room/breakout-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/breakout-room/breakout-dropdown/component.jsx @@ -65,6 +65,7 @@ class BreakoutDropdown extends PureComponent { render() { const { intl, + isRTL, } = this.props; return ( @@ -91,8 +92,8 @@ class BreakoutDropdown extends PureComponent { elevation: 3, getContentAnchorEl: null, fullwidth: "true", - anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, - transformorigin: { vertical: 'bottom', horizontal: 'left' }, + anchorOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' }, + transformOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' }, }} actions={this.getAvailableActions()} /> diff --git a/bigbluebutton-html5/imports/ui/components/breakout-room/component.jsx b/bigbluebutton-html5/imports/ui/components/breakout-room/component.jsx index cc15d5f3bd..28bcd852f6 100644 --- a/bigbluebutton-html5/imports/ui/components/breakout-room/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/breakout-room/component.jsx @@ -547,6 +547,7 @@ class BreakoutRoom extends PureComponent { intl, endAllBreakouts, amIModerator, + isRTL, } = this.props; return ( this.panel = n}> @@ -568,6 +569,7 @@ class BreakoutRoom extends PureComponent { }} isMeteorConnected={isMeteorConnected} amIModerator={amIModerator} + isRTL={isRTL} /> ) } diff --git a/bigbluebutton-html5/imports/ui/components/breakout-room/container.jsx b/bigbluebutton-html5/imports/ui/components/breakout-room/container.jsx index 55e63ec158..59734a682c 100644 --- a/bigbluebutton-html5/imports/ui/components/breakout-room/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/breakout-room/container.jsx @@ -4,7 +4,7 @@ import AudioService from '/imports/ui/components/audio/service'; import AudioManager from '/imports/ui/services/audio-manager'; import BreakoutComponent from './component'; import Service from './service'; -import { layoutDispatch } from '../layout/context'; +import { layoutDispatch, layoutSelect } from '../layout/context'; import Auth from '/imports/ui/services/auth'; import { UsersContext } from '/imports/ui/components/components-data/users-context/context'; import { @@ -18,10 +18,11 @@ const BreakoutContainer = (props) => { const usingUsersContext = useContext(UsersContext); const { users } = usingUsersContext; const amIPresenter = users[Auth.meetingID][Auth.userID].presenter; + const isRTL = layoutSelect((i) => i.isRTL); return ; }; diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx index b694f53751..70a57770ea 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx @@ -131,6 +131,7 @@ class ChatDropdown extends PureComponent { const { intl, amIModerator, + isRTL, } = this.props; if (!amIModerator && !ENABLE_SAVE_AND_COPY_PUBLIC_CHAT) return null; @@ -158,8 +159,8 @@ class ChatDropdown extends PureComponent { elevation: 3, getContentAnchorEl: null, fullwidth: "true", - anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, - transformorigin: { vertical: 'bottom', horizontal: 'left' }, + anchorOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' }, + transformOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' }, }} actions={this.getAvailableActions()} /> diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/container.jsx index c822a3a8ee..7888bb64c1 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/container.jsx @@ -4,12 +4,14 @@ import Auth from '/imports/ui/services/auth'; import Meetings from '/imports/api/meetings'; import { UsersContext } from '/imports/ui/components/components-data/users-context/context'; import ChatDropdown from './component'; +import { layoutSelect } from '../../layout/context'; const ChatDropdownContainer = ({ ...props }) => { const usingUsersContext = useContext(UsersContext); const { users } = usingUsersContext; + const isRTL = layoutSelect((i) => i.isRTL); - return ; + return ; }; export default withTracker(() => { diff --git a/bigbluebutton-html5/imports/ui/components/common/button/button-emoji/styles.js b/bigbluebutton-html5/imports/ui/components/common/button/button-emoji/styles.js index 682f8f9378..12d93e36ec 100644 --- a/bigbluebutton-html5/imports/ui/components/common/button/button-emoji/styles.js +++ b/bigbluebutton-html5/imports/ui/components/common/button/button-emoji/styles.js @@ -45,6 +45,11 @@ const EmojiButton = styled.button` z-index: 2; border: none; + [dir="rtl"] & { + right: initial; + left: -.2em; + } + &:hover { transform: scale(1.5); transition-duration: 150ms; @@ -70,6 +75,11 @@ const EmojiButtonSpace = styled.div` right: -.4em; bottom: -.2em; border-radius: 50%; + + [dir="rtl"] & { + right: initial; + left: -.4em; + } `; export default { diff --git a/bigbluebutton-html5/imports/ui/components/common/menu/component.jsx b/bigbluebutton-html5/imports/ui/components/common/menu/component.jsx index 93b1b51ded..c09695ac9c 100644 --- a/bigbluebutton-html5/imports/ui/components/common/menu/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/common/menu/component.jsx @@ -26,7 +26,7 @@ class BBBMenu extends React.Component { anchorEl: null, }; - this.opts = props.opts; + this.optsToMerge = {}; this.autoFocus = false; this.handleClick = this.handleClick.bind(this); @@ -112,7 +112,7 @@ class BBBMenu extends React.Component { render() { const { anchorEl } = this.state; - const { trigger, intl, customStyles, dataTest } = this.props; + const { trigger, intl, customStyles, dataTest, opts } = this.props; const actionsItems = this.makeMenuItems(); let menuStyles = { zIndex: 9999 }; @@ -129,7 +129,7 @@ class BBBMenu extends React.Component { const firefoxInputSource = !([1, 5].includes(e.nativeEvent.mozInputSource)); // 1 = mouse, 5 = touch (firefox only) const chromeInputSource = !(['mouse', 'touch'].includes(e.nativeEvent.pointerType)); - this.opts.autoFocus = firefoxInputSource && chromeInputSource; + this.optsToMerge.autoFocus = firefoxInputSource && chromeInputSource; this.handleClick(e); }} onKeyPress={(e) => { @@ -144,7 +144,8 @@ class BBBMenu extends React.Component { )} actions={this.renderMenuItems()} + opts={{ + id: "default-dropdown-menu", + keepMounted: true, + transitionDuration: 0, + elevation: 3, + getContentAnchorEl: null, + fullwidth: "true", + anchorOrigin: { vertical: 'bottom', horizontal: isRTL ? 'left' : 'right' }, + transformorigin: { vertical: 'top', horizontal: isRTL ? 'left' : 'right' }, + }} /> ); } diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/container.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/container.jsx index d476bf3003..7872351b8a 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/container.jsx @@ -5,7 +5,7 @@ import browserInfo from '/imports/utils/browserInfo'; import SettingsDropdown from './component'; import FullscreenService from '/imports/ui/components/common/fullscreen-button/service'; import { meetingIsBreakout } from '/imports/ui/components/app/service'; -import { layoutSelectInput } from '../../layout/context'; +import { layoutSelectInput, layoutSelect } from '../../layout/context'; import { SMALL_VIEWPORT_BREAKPOINT } from '../../layout/enums'; const { isIphone } = deviceInfo; @@ -16,9 +16,10 @@ const noIOSFullscreen = !!(((isSafari && !isValidSafariVersion) || isIphone)); const SettingsDropdownContainer = (props) => { const { width: browserWidth } = layoutSelectInput((i) => i.browser); const isMobile = browserWidth <= SMALL_VIEWPORT_BREAKPOINT; + const isRTL = layoutSelect((i) => i.isRTL); return ( - + ); }; diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx index 217b1e6b9a..e69d052191 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx @@ -343,7 +343,7 @@ class UserOptions extends PureComponent { } render() { - const { intl } = this.props; + const { intl, isRTL } = this.props; return ( )} actions={this.renderMenuItems()} + opts={{ + id: "default-dropdown-menu", + keepMounted: true, + transitionDuration: 0, + elevation: 3, + getContentAnchorEl: null, + fullwidth: "true", + anchorOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' }, + transformOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' }, + }} /> ); } diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/container.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/container.jsx index 8b719def02..ef90cedbd4 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/container.jsx @@ -10,6 +10,7 @@ import logger from '/imports/startup/client/logger'; import { defineMessages, injectIntl } from 'react-intl'; import { notify } from '/imports/ui/services/notification'; import UserOptions from './component'; +import { layoutSelect } from '/imports/ui/components/layout/context'; const propTypes = { users: PropTypes.arrayOf(Object).isRequired, @@ -60,6 +61,8 @@ const UserOptionsContainer = withTracker((props) => { return name; }; + const isRTL = layoutSelect((i) => i.isRTL); + return { toggleMuteAllUsers: () => { UserListService.muteAllUsers(Auth.userID); @@ -92,6 +95,7 @@ const UserOptionsContainer = withTracker((props) => { meetingName: getMeetingName(), openLearningDashboardUrl: LearningDashboardService.openLearningDashboardUrl, dynamicGuestPolicy, + isRTL, }; })(UserOptions); diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx index 1a123f04ac..3fdca05e02 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx @@ -17,7 +17,7 @@ import Styled from './styles'; const VideoListItem = (props) => { const { name, voiceUser, isFullscreenContext, layoutContextDispatch, user, onHandleVideoFocus, - cameraId, numOfStreams, focused, onVideoItemMount, onVideoItemUnmount, + cameraId, numOfStreams, focused, onVideoItemMount, onVideoItemUnmount, isRTL, } = props; const [videoIsReady, setVideoIsReady] = useState(false); @@ -124,6 +124,7 @@ const VideoListItem = (props) => { onHandleVideoFocus={onHandleVideoFocus} focused={focused} onHandleMirror={() => setIsMirrored((value) => !value)} + isRTL={isRTL} /> { const { element } = fullscreen; const isFullscreenContext = (element === cameraId); const layoutContextDispatch = layoutDispatch(); + const isRTL = layoutSelect((i) => i.isRTL); return ( { {...{ isFullscreenContext, layoutContextDispatch, + isRTL, }} /> ); diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/user-actions/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/user-actions/component.jsx index e96e597bb0..d763cd4361 100644 --- a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/user-actions/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/user-actions/component.jsx @@ -41,7 +41,8 @@ const intlMessages = defineMessages({ const UserActions = (props) => { const { - name, cameraId, numOfStreams, onHandleVideoFocus, user, focused, onHandleMirror, + name, cameraId, numOfStreams, onHandleVideoFocus, + user, focused, onHandleMirror, isRTL, } = props; const intl = useIntl(); @@ -103,8 +104,8 @@ const UserActions = (props) => { elevation: 3, getContentAnchorEl: null, fullwidth: 'true', - anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, - transformorigin: { vertical: 'bottom', horizontal: 'left' }, + anchorOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' }, + transformOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' }, }} /> ) From 5397498746554396f0820e22512a73f9bf8ea219 Mon Sep 17 00:00:00 2001 From: Ramon Souza Date: Fri, 13 May 2022 16:18:51 -0300 Subject: [PATCH 070/268] prevent user join from multiple tabs with same sessionToken --- .../server/handlers/validateAuthToken.js | 3 ++ .../modifiers/updateUserConnectionId.js | 31 +++++++++++++++++++ .../imports/startup/client/base.jsx | 8 +++++ .../ui/components/error-screen/component.jsx | 3 ++ bigbluebutton-html5/public/locales/en.json | 1 + 5 files changed, 46 insertions(+) create mode 100644 bigbluebutton-html5/imports/api/users/server/modifiers/updateUserConnectionId.js diff --git a/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js b/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js index b7d5be1005..f24ec1afeb 100644 --- a/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js +++ b/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js @@ -4,6 +4,7 @@ import Users from '/imports/api/users'; import userJoin from './userJoin'; import pendingAuthenticationsStore from '../store/pendingAuthentications'; import createDummyUser from '../modifiers/createDummyUser'; +import updateUserConnectionId from '../modifiers/updateUserConnectionId'; import ClientConnections from '/imports/startup/server/ClientConnections'; import upsertValidationState from '/imports/api/auth-token-validation/server/modifiers/upsertValidationState'; @@ -81,6 +82,8 @@ export default function handleValidateAuthToken({ body }, meetingId) { if (!User) { createDummyUser(meetingId, userId, authToken); + }else{ + updateUserConnectionId(meetingId, userId, methodInvocationObject.connection.id); } ClientConnections.add(sessionId, methodInvocationObject.connection); diff --git a/bigbluebutton-html5/imports/api/users/server/modifiers/updateUserConnectionId.js b/bigbluebutton-html5/imports/api/users/server/modifiers/updateUserConnectionId.js new file mode 100644 index 0000000000..8a151d2f74 --- /dev/null +++ b/bigbluebutton-html5/imports/api/users/server/modifiers/updateUserConnectionId.js @@ -0,0 +1,31 @@ +import { check } from 'meteor/check'; +import Logger from '/imports/startup/server/logger'; +import Users from '/imports/api/users'; + +export default function updateUserConnectionId(meetingId, userId, connectionId) { + check(meetingId, String); + check(userId, String); + check(connectionId, String); + + const selector = { meetingId, userId }; + + const modifier = { + $set: { + currentConnectionId: connectionId, + }, + }; + + const User = Users.findOne(selector); + + if (User) { + try { + const updated = Users.update(selector, modifier); + + if (updated) { + Logger.info(`Updated connection user=${userId} connectionid=${connectionId} meeting=${meetingId}`); + } + } catch (err) { + Logger.error(`Updating user connection: ${err}`); + } + } +} diff --git a/bigbluebutton-html5/imports/startup/client/base.jsx b/bigbluebutton-html5/imports/startup/client/base.jsx index ec9c7b15d3..4f08812103 100755 --- a/bigbluebutton-html5/imports/startup/client/base.jsx +++ b/bigbluebutton-html5/imports/startup/client/base.jsx @@ -429,6 +429,7 @@ export default withTracker(() => { userId: 1, inactivityCheck: 1, responseDelay: 1, + currentConnectionId: 1, }; const User = Users.findOne({ intId: credentials.requesterUserId }, { fields }); const meeting = Meetings.findOne({ meetingId }, { @@ -447,6 +448,13 @@ export default withTracker(() => { const ejected = User?.ejected; const ejectedReason = User?.ejectedReason; const meetingEndedReason = meeting?.meetingEndedReason; + const currentConnectionId = User?.currentConnectionId; + const { connectionID } = Auth; + + if (currentConnectionId && currentConnectionId !== connectionID) { + Session.set('codeError', 403); + Session.set('errorMessageDescription', 'joined_another_window_reason') + } let userSubscriptionHandler; diff --git a/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx b/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx index 2e6933baa3..619337f07d 100644 --- a/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx @@ -40,6 +40,9 @@ const intlMessages = defineMessages({ banned_user_rejoining_reason: { id: 'app.error.userBanned', }, + joined_another_window_reason: { + id: 'app.error.joinedAnotherWindow', + }, }); const propTypes = { diff --git a/bigbluebutton-html5/public/locales/en.json b/bigbluebutton-html5/public/locales/en.json index f0608a2a56..7fdc2a8e5b 100755 --- a/bigbluebutton-html5/public/locales/en.json +++ b/bigbluebutton-html5/public/locales/en.json @@ -603,6 +603,7 @@ "app.error.500": "Ops, something went wrong", "app.error.userLoggedOut": "User has an invalid sessionToken due to log out", "app.error.ejectedUser": "User has an invalid sessionToken due to ejection", + "app.error.joinedAnotherWindow": "This session seems to be opened in another browser window.", "app.error.userBanned": "User has been banned", "app.error.leaveLabel": "Log in again", "app.error.fallback.presentation.title": "An error occurred", From 380006538004c409763acabf4dcfcd58dda5c911 Mon Sep 17 00:00:00 2001 From: Ramon Souza Date: Fri, 1 Apr 2022 10:16:15 -0300 Subject: [PATCH 071/268] remove chat name --- .../apps/groupchats/CreateDefaultPublicGroupChat.scala | 2 +- .../apps/groupchats/CreateGroupChatReqMsgHdlr.scala | 10 +++++----- .../core/apps/groupchats/GetGroupChatsReqMsgHdlr.scala | 4 ++-- .../bigbluebutton/core/apps/groupchats/GroupChat.scala | 6 +++--- .../apps/groupchats/SyncGetGroupChatsInfoMsgHdlr.scala | 2 +- .../org/bigbluebutton/core/models/GroupChats.scala | 6 +++--- .../bigbluebutton/core/models/GroupsChatTests.scala | 6 ++---- .../org/bigbluebutton/common2/msgs/GroupChatMsg.scala | 8 ++++---- .../api/group-chat/server/methods/createGroupChat.js | 1 - .../api/group-chat/server/modifiers/addGroupChat.js | 4 +--- 10 files changed, 22 insertions(+), 27 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/CreateDefaultPublicGroupChat.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/CreateDefaultPublicGroupChat.scala index 31bac464d2..3d7620fb15 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/CreateDefaultPublicGroupChat.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/CreateDefaultPublicGroupChat.scala @@ -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) } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/CreateGroupChatReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/CreateGroupChatReqMsgHdlr.scala index fddebc0b0b..479f3b7894 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/CreateGroupChatReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/CreateGroupChatReqMsgHdlr.scala @@ -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) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/GetGroupChatsReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/GetGroupChatsReqMsgHdlr.scala index 9456e87bea..98ff166a75 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/GetGroupChatsReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/GetGroupChatsReqMsgHdlr.scala @@ -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 diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/GroupChat.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/GroupChat.scala index d686f10e7f..ee49cfba11 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/GroupChat.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/GroupChat.scala @@ -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,7 +46,7 @@ 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 = { diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/SyncGetGroupChatsInfoMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/SyncGetGroupChatsInfoMsgHdlr.scala index f8a63764cc..b7390ede78 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/SyncGetGroupChatsInfoMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/SyncGetGroupChatsInfoMsgHdlr.scala @@ -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 diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/GroupChats.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/GroupChats.scala index 232b436956..8c5d91b545 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/GroupChats.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/GroupChats.scala @@ -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) diff --git a/akka-bbb-apps/src/test/scala/org/bigbluebutton/core/models/GroupsChatTests.scala b/akka-bbb-apps/src/test/scala/org/bigbluebutton/core/models/GroupsChatTests.scala index dd8e75f0bb..5ca56de7ae 100755 --- a/akka-bbb-apps/src/test/scala/org/bigbluebutton/core/models/GroupsChatTests.scala +++ b/akka-bbb-apps/src/test/scala/org/bigbluebutton/core/models/GroupsChatTests.scala @@ -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!" diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/GroupChatMsg.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/GroupChatMsg.scala index 502f6cbc97..62f86140c6 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/GroupChatMsg.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/GroupChatMsg.scala @@ -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 diff --git a/bigbluebutton-html5/imports/api/group-chat/server/methods/createGroupChat.js b/bigbluebutton-html5/imports/api/group-chat/server/methods/createGroupChat.js index 665005e582..58b357f313 100644 --- a/bigbluebutton-html5/imports/api/group-chat/server/methods/createGroupChat.js +++ b/bigbluebutton-html5/imports/api/group-chat/server/methods/createGroupChat.js @@ -22,7 +22,6 @@ export default function createGroupChat(receiver) { msg: [], users: [receiver.userId], access: CHAT_ACCESS_PRIVATE, - name: receiver.name, }; RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); diff --git a/bigbluebutton-html5/imports/api/group-chat/server/modifiers/addGroupChat.js b/bigbluebutton-html5/imports/api/group-chat/server/modifiers/addGroupChat.js index 4b7f389d6d..eff45ec1d7 100644 --- a/bigbluebutton-html5/imports/api/group-chat/server/modifiers/addGroupChat.js +++ b/bigbluebutton-html5/imports/api/group-chat/server/modifiers/addGroupChat.js @@ -9,7 +9,6 @@ export default function addGroupChat(meetingId, chat) { id: Match.Maybe(String), chatId: Match.Maybe(String), correlationId: Match.Maybe(String), - name: String, access: String, createdBy: Object, users: Array, @@ -19,9 +18,8 @@ export default function addGroupChat(meetingId, chat) { const chatDocument = { meetingId, chatId: chat.chatId || chat.id, - name: chat.name, access: chat.access, - users: chat.users.map(u => u.id), + users: chat.users.map((u) => u.id), participants: chat.users, createdBy: chat.createdBy.id, }; From fe7a2d6c7f56d9155f12b0ca01c90e8f1c7fe843 Mon Sep 17 00:00:00 2001 From: Fred Dixon Date: Sun, 15 May 2022 13:52:28 -0500 Subject: [PATCH 072/268] Better wording on breakout room actions --- bigbluebutton-html5/public/locales/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bigbluebutton-html5/public/locales/en.json b/bigbluebutton-html5/public/locales/en.json index f0608a2a56..ddda2437bd 100755 --- a/bigbluebutton-html5/public/locales/en.json +++ b/bigbluebutton-html5/public/locales/en.json @@ -506,8 +506,8 @@ "app.breakoutJoinConfirmation.freeJoinMessage": "Choose a breakout room to join", "app.breakoutTimeRemainingMessage": "Breakout room time remaining: {0}", "app.breakoutWillCloseMessage": "Time ended. Breakout room will close soon", - "app.breakout.dropdown.manageDuration": "Manage duration", - "app.breakout.dropdown.destroyAll": "Destroy breakouts", + "app.breakout.dropdown.manageDuration": "Change duration", + "app.breakout.dropdown.destroyAll": "End breakout rooms", "app.breakout.dropdown.options": "Breakout Options", "app.calculatingBreakoutTimeRemaining": "Calculating remaining time ...", "app.audioModal.ariaTitle": "Join audio modal", From ef6eb21ccea0f2764e23eaa792a6c2e460108107 Mon Sep 17 00:00:00 2001 From: Fred Dixon Date: Sun, 15 May 2022 13:58:25 -0500 Subject: [PATCH 073/268] Minor refactoring of propagating new version --- build/packages-template/bbb-config/after-install.sh | 10 ++++------ build/packages-template/bbb-html5/after-install.sh | 13 ++++++------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/build/packages-template/bbb-config/after-install.sh b/build/packages-template/bbb-config/after-install.sh index ec450f49eb..9dfe0e12c7 100644 --- a/build/packages-template/bbb-config/after-install.sh +++ b/build/packages-template/bbb-config/after-install.sh @@ -128,12 +128,10 @@ fi # set full BBB version in settings.yml so it can be displayed in the client BBB_RELEASE_FILE=/etc/bigbluebutton/bigbluebutton-release BBB_HTML5_SETTINGS_FILE=/usr/share/meteor/bundle/programs/server/assets/app/config/settings.yml -if [[ -f $BBB_RELEASE_FILE ]] ; then - BBB_FULL_VERSION=$(cat $BBB_RELEASE_FILE | sed -n '/^BIGBLUEBUTTON_RELEASE/{s/.*=//;p}' ) - echo "setting BBB_FULL_VERSION=$BBB_FULL_VERSION in $BBB_HTML5_SETTINGS_FILE " - if [[ -f $BBB_HTML5_SETTINGS_FILE ]] ; then - yq w -i $BBB_HTML5_SETTINGS_FILE public.app.bbbServerVersion $BBB_FULL_VERSION - fi +if [ -f $BBB_RELEASE_FILE ] && [ -f $BBB_HTML5_SETTINGS_FILE ]; then + BBB_FULL_VERSION=$(cat $BBB_RELEASE_FILE | sed -n '/^BIGBLUEBUTTON_RELEASE/{s/.*=//;p}' | tail -n 1) + echo "setting public.app.bbbServerVersion: $BBB_FULL_VERSION in $BBB_HTML5_SETTINGS_FILE " + yq w -i $BBB_HTML5_SETTINGS_FILE public.app.bbbServerVersion $BBB_FULL_VERSION fi # Fix permissions for logging diff --git a/build/packages-template/bbb-html5/after-install.sh b/build/packages-template/bbb-html5/after-install.sh index e96d8df68f..733a48ce92 100644 --- a/build/packages-template/bbb-html5/after-install.sh +++ b/build/packages-template/bbb-html5/after-install.sh @@ -38,13 +38,12 @@ fi # set full BBB version in settings.yml so it can be displayed in the client BBB_RELEASE_FILE=/etc/bigbluebutton/bigbluebutton-release BBB_HTML5_SETTINGS_FILE=/usr/share/meteor/bundle/programs/server/assets/app/config/settings.yml -if [[ -f $BBB_RELEASE_FILE ]] ; then - BBB_FULL_VERSION=$(cat $BBB_RELEASE_FILE | sed -n '/^BIGBLUEBUTTON_RELEASE/{s/.*=//;p}' ) - echo "setting BBB_FULL_VERSION=$BBB_FULL_VERSION in $BBB_HTML5_SETTINGS_FILE " - if [[ -f $BBB_HTML5_SETTINGS_FILE ]] ; then - yq w -i $BBB_HTML5_SETTINGS_FILE public.app.bbbServerVersion $BBB_FULL_VERSION - fi -fi +if [ -f $BBB_RELEASE_FILE ] && [ -f $BBB_HTML5_SETTINGS_FILE ]; then + BBB_FULL_VERSION=$(cat $BBB_RELEASE_FILE | sed -n '/^BIGBLUEBUTTON_RELEASE/{s/.*=//;p}' | tail -n 1) + echo "setting public.app.bbbServerVersion: $BBB_FULL_VERSION in $BBB_HTML5_SETTINGS_FILE " + yq w -i $BBB_HTML5_SETTINGS_FILE public.app.bbbServerVersion $BBB_FULL_VERSION +fi + # Remove old overrides if [ -f /etc/systemd/system/mongod.service.d/override-mongo.conf ] \ From a52986b015e9e6359d37ef3f0115e25f437ca1b8 Mon Sep 17 00:00:00 2001 From: Fred Dixon Date: Sun, 15 May 2022 13:59:25 -0500 Subject: [PATCH 074/268] Bump release to rc3 --- bigbluebutton-config/bigbluebutton-release | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-config/bigbluebutton-release b/bigbluebutton-config/bigbluebutton-release index 16547f44f6..a22c4b5974 100644 --- a/bigbluebutton-config/bigbluebutton-release +++ b/bigbluebutton-config/bigbluebutton-release @@ -1 +1 @@ -BIGBLUEBUTTON_RELEASE=2.5.0-rc.2 +BIGBLUEBUTTON_RELEASE=2.5.0-rc.3 From 7aa4de009678a4bd090cc608439157d841feba9d Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Sun, 15 May 2022 22:24:06 +0200 Subject: [PATCH 075/268] Modify PresentationPageForExport parameters due to tldraw panzoom handling --- .../PresentationWithAnnotationsMsgHdlr.scala | 18 ++++++++---------- .../common2/msgs/WhiteboardMsgs.scala | 7 +++---- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationWithAnnotationsMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationWithAnnotationsMsgHdlr.scala index a2e09f4515..6744a43c95 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationWithAnnotationsMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationWithAnnotationsMsgHdlr.scala @@ -80,13 +80,12 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait { var whiteboardId = s"${presId}/${pageNumber.toString}" val presentationPage: PresentationPage = currentPres.get.pages(whiteboardId) - val xOffset: Double = presentationPage.xOffset - val yOffset: Double = presentationPage.yOffset - val widthRatio: Double = presentationPage.widthRatio - val heightRatio: Double = presentationPage.heightRatio + val xCamera: Double = presentationPage.xCamera + val yCamera: Double = presentationPage.yCamera + val zoom: Double = presentationPage.zoom val whiteboardHistory: Array[AnnotationVO] = liveMeeting.wbModel.getHistory(whiteboardId) - storeAnnotationPages(resultingPage) = new PresentationPageForExport(pageNumber, xOffset, yOffset, widthRatio, heightRatio, whiteboardHistory) + storeAnnotationPages(resultingPage) = new PresentationPageForExport(pageNumber, xCamera, yCamera, zoom, whiteboardHistory) resultingPage += 1 } @@ -141,13 +140,12 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait { for (pageNumber <- pagesRange) { var whiteboardId = s"${presId}/${pageNumber.toString}" val presentationPage: PresentationPage = currentPres.get.pages(whiteboardId) - val xOffset: Double = presentationPage.xOffset - val yOffset: Double = presentationPage.yOffset - val widthRatio: Double = presentationPage.widthRatio - val heightRatio: Double = presentationPage.heightRatio + val xCamera: Double = presentationPage.xCamera + val yCamera: Double = presentationPage.yCamera + val zoom: Double = presentationPage.zoom val whiteboardHistory: Array[AnnotationVO] = liveMeeting.wbModel.getHistory(whiteboardId) - storeAnnotationPages(resultingPage) = new PresentationPageForExport(pageNumber, xOffset, yOffset, widthRatio, heightRatio, whiteboardHistory) + storeAnnotationPages(resultingPage) = new PresentationPageForExport(pageNumber, xCamera, yCamera, zoom, whiteboardHistory) resultingPage += 1 } diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala index 3594610a5e..1cf8979dfd 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WhiteboardMsgs.scala @@ -5,10 +5,9 @@ case class AnnotationVO(id: String, annotationInfo: scala.collection.immutable.M case class PresentationPageForExport( page: Int, - xOffset: Double, - yOffset: Double, - widthRatio: Double, - heightRatio: Double, + xCamera: Double, + yCamera: Double, + zoom: Double, annotations: Array[AnnotationVO], ) From dfd93e79596fe5974d2c18b1f636965a10de4ae3 Mon Sep 17 00:00:00 2001 From: Daniel Schreiber Date: Sun, 15 May 2022 21:48:05 +0200 Subject: [PATCH 076/268] Fix etherpad session authentication to work in cluster setups Etherpad uses the sessionID cookie for authorization. In cluster setups the host part of the URI which serves the html5 frontend is different from the hostname part of the URI which serves etherpad. Therefore the bbb-html5 client can't set a cookie for etherpad which contains the etherpad sessionID. This patch uses the `ep_auth_session` etherpad plugin which takes the `sessionID` as query parameter, sets the cookie in the browser and redirects the iframe to the pad URI. --- .../imports/ui/components/pads/service.js | 12 ++++++++---- build/packages-template/bbb-etherpad/build.sh | 1 + build/packages-template/bbb-etherpad/notes.nginx | 10 ++++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/pads/service.js b/bigbluebutton-html5/imports/ui/components/pads/service.js index 56b0dc3679..f4cc3e81d4 100644 --- a/bigbluebutton-html5/imports/ui/components/pads/service.js +++ b/bigbluebutton-html5/imports/ui/components/pads/service.js @@ -1,5 +1,5 @@ import _ from 'lodash'; -import Pads, { PadsUpdates } from '/imports/api/pads'; +import Pads, { PadsSessions, PadsUpdates } from '/imports/api/pads'; import { makeCall } from '/imports/ui/services/api'; import Auth from '/imports/ui/services/auth'; import Settings from '/imports/ui/services/settings'; @@ -47,9 +47,13 @@ const throttledCreateSession = _.throttle(createSession, THROTTLE_TIMEOUT, { const buildPadURL = (padId) => { if (padId) { - const params = getParams(); - const url = Auth.authenticateURL(`${PADS_CONFIG.url}/p/${padId}?${params}`); - return url; + const padsSessions = PadsSessions.findOne({}); + if (padsSessions && padsSessions.sessions) { + const params = getParams(); + const sessionIds = padsSessions.sessions.map(session => Object.values(session)).join(','); + const url = Auth.authenticateURL(`${PADS_CONFIG.url}/auth_session?padName=${padId}&sessionID=${sessionIds}&${params}`); + return url; + } } return null; diff --git a/build/packages-template/bbb-etherpad/build.sh b/build/packages-template/bbb-etherpad/build.sh index 3d06f38224..3c5e491565 100755 --- a/build/packages-template/bbb-etherpad/build.sh +++ b/build/packages-template/bbb-etherpad/build.sh @@ -49,6 +49,7 @@ npm install ./ep_redis_publisher-*.tgz npm install ep_cursortrace npm install ep_disable_chat +npm install --no-save --legacy-peer-deps ep_auth_session mkdir -p staging/usr/share/etherpad-lite diff --git a/build/packages-template/bbb-etherpad/notes.nginx b/build/packages-template/bbb-etherpad/notes.nginx index de53f0b90d..130d1d88f6 100644 --- a/build/packages-template/bbb-etherpad/notes.nginx +++ b/build/packages-template/bbb-etherpad/notes.nginx @@ -22,6 +22,16 @@ location /pad/p/ { auth_request_set $auth_status $upstream_status; } +location /pad/auth_session { + rewrite /pad/auth_session(.*) /auth_session$1 break; + proxy_pass http://127.0.0.1:9001/; + proxy_pass_header Server; + proxy_set_header Host $host; + proxy_buffering off; + auth_request /bigbluebutton/connection/checkAuthorization; + auth_request_set $auth_status $upstream_status; +} + location /pad { rewrite /pad/(.*) /$1 break; rewrite ^/pad$ /pad/ permanent; From 7ff6f8750d85576c11ac63256a245d9c09521295 Mon Sep 17 00:00:00 2001 From: Joao Victor Date: Mon, 16 May 2022 10:20:16 -0300 Subject: [PATCH 077/268] [2.5] fix(whiteboard): border-radius of toolbar buttons #15009 --- .../whiteboard-toolbar/toolbar-menu-item/styles.js | 8 -------- .../whiteboard-toolbar/toolbar-submenu-item/styles.js | 7 +++++++ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-menu-item/styles.js b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-menu-item/styles.js index d060de3d78..37f53135ef 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-menu-item/styles.js +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-menu-item/styles.js @@ -81,14 +81,6 @@ const ToolbarButton = styled(Button)` & > i { color: ${toolbarListColor}; } - - border-top-left-radius: 0; - border-top-right-radius: ${toolbarButtonBorderRadius}; - - [dir="rtl"] & { - border-top-left-radius: ${toolbarButtonBorderRadius}; - border-top-right-radius: 0; - } `} `; diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-submenu-item/styles.js b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-submenu-item/styles.js index 8c1d7d8b32..469b378a26 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-submenu-item/styles.js +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-submenu-item/styles.js @@ -32,6 +32,13 @@ const ButtonWrapper = styled.div` &:first-child > button { border-top-left-radius: ${toolbarButtonBorderRadius}; border-bottom-left-radius: ${toolbarButtonBorderRadius}; + + [dir="rtl"] & { + border-top-right-radius: ${toolbarButtonBorderRadius}; + border-bottom-right-radius: ${toolbarButtonBorderRadius}; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } } `; From 9bf697b01023821c1a0b7b8668951052e807a83c Mon Sep 17 00:00:00 2001 From: Pedro Beschorner Marin Date: Mon, 16 May 2022 13:33:08 -0300 Subject: [PATCH 078/268] build(pads): v1.2.1 Update `bbb-pads` version. https://github.com/bigbluebutton/bbb-pads/releases/tag/v1.2.1 --- bbb-pads.placeholder.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bbb-pads.placeholder.sh b/bbb-pads.placeholder.sh index 47d3f38b91..7bc05cdf65 100755 --- a/bbb-pads.placeholder.sh +++ b/bbb-pads.placeholder.sh @@ -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 From 5bddbdb4cf1e38d79742175a7cd99a980f98d511 Mon Sep 17 00:00:00 2001 From: Ramon Souza Date: Tue, 17 May 2022 09:39:15 -0300 Subject: [PATCH 079/268] adjustments --- .../org/bigbluebutton/core/apps/groupchats/GroupChat.scala | 6 ++++-- .../imports/api/group-chat/server/modifiers/addGroupChat.js | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/GroupChat.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/GroupChat.scala index ee49cfba11..d61cc7db67 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/GroupChat.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/GroupChat.scala @@ -51,8 +51,10 @@ object GroupChatApp { 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) } diff --git a/bigbluebutton-html5/imports/api/group-chat/server/modifiers/addGroupChat.js b/bigbluebutton-html5/imports/api/group-chat/server/modifiers/addGroupChat.js index eff45ec1d7..76a064f60b 100644 --- a/bigbluebutton-html5/imports/api/group-chat/server/modifiers/addGroupChat.js +++ b/bigbluebutton-html5/imports/api/group-chat/server/modifiers/addGroupChat.js @@ -37,9 +37,9 @@ export default function addGroupChat(meetingId, chat) { const { insertedId } = GroupChat.upsert(selector, modifier); if (insertedId) { - Logger.info(`Added group-chat name=${chat.name} meetingId=${meetingId}`); + Logger.info(`Added group-chat chatId=${chatDocument.chatId} meetingId=${meetingId}`); } else { - Logger.info(`Upserted group-chat name=${chat.name} meetingId=${meetingId}`); + Logger.info(`Upserted group-chat chatId=${chatDocument.chatId} meetingId=${meetingId}`); } } catch (err) { Logger.error(`Adding group-chat to collection: ${err}`); From 4278090983950d84a16945d573be25d9a8fdd9f4 Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Tue, 17 May 2022 10:45:20 -0400 Subject: [PATCH 080/268] chore: Bump release to rc.4 --- bigbluebutton-config/bigbluebutton-release | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-config/bigbluebutton-release b/bigbluebutton-config/bigbluebutton-release index a22c4b5974..e2174a7682 100644 --- a/bigbluebutton-config/bigbluebutton-release +++ b/bigbluebutton-config/bigbluebutton-release @@ -1 +1 @@ -BIGBLUEBUTTON_RELEASE=2.5.0-rc.3 +BIGBLUEBUTTON_RELEASE=2.5.0-rc.4 From 8986fe28531767d214099a75ff725a6920f3d07c Mon Sep 17 00:00:00 2001 From: Ramon Souza Date: Tue, 17 May 2022 13:09:41 -0300 Subject: [PATCH 081/268] prevent disconnect in same tab --- .../api/users/server/modifiers/updateUserConnectionId.js | 1 + bigbluebutton-html5/imports/startup/client/base.jsx | 6 ++++-- bigbluebutton-html5/imports/ui/services/auth/index.js | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/bigbluebutton-html5/imports/api/users/server/modifiers/updateUserConnectionId.js b/bigbluebutton-html5/imports/api/users/server/modifiers/updateUserConnectionId.js index 8a151d2f74..2d01e33022 100644 --- a/bigbluebutton-html5/imports/api/users/server/modifiers/updateUserConnectionId.js +++ b/bigbluebutton-html5/imports/api/users/server/modifiers/updateUserConnectionId.js @@ -12,6 +12,7 @@ export default function updateUserConnectionId(meetingId, userId, connectionId) const modifier = { $set: { currentConnectionId: connectionId, + connectionIdUpdateTime: new Date().getTime(), }, }; diff --git a/bigbluebutton-html5/imports/startup/client/base.jsx b/bigbluebutton-html5/imports/startup/client/base.jsx index 4f08812103..a75e26c11c 100755 --- a/bigbluebutton-html5/imports/startup/client/base.jsx +++ b/bigbluebutton-html5/imports/startup/client/base.jsx @@ -430,6 +430,7 @@ export default withTracker(() => { inactivityCheck: 1, responseDelay: 1, currentConnectionId: 1, + connectionIdUpdateTime: 1, }; const User = Users.findOne({ intId: credentials.requesterUserId }, { fields }); const meeting = Meetings.findOne({ meetingId }, { @@ -449,9 +450,10 @@ export default withTracker(() => { const ejectedReason = User?.ejectedReason; const meetingEndedReason = meeting?.meetingEndedReason; const currentConnectionId = User?.currentConnectionId; - const { connectionID } = Auth; + const { connectionID, connectionAuthTime } = Auth; + const connectionIdUpdateTime = User?.connectionIdUpdateTime; - if (currentConnectionId && currentConnectionId !== connectionID) { + if (currentConnectionId && currentConnectionId !== connectionID && connectionIdUpdateTime > connectionAuthTime) { Session.set('codeError', 403); Session.set('errorMessageDescription', 'joined_another_window_reason') } diff --git a/bigbluebutton-html5/imports/ui/services/auth/index.js b/bigbluebutton-html5/imports/ui/services/auth/index.js index 1496de13ae..8eea3bfdf5 100755 --- a/bigbluebutton-html5/imports/ui/services/auth/index.js +++ b/bigbluebutton-html5/imports/ui/services/auth/index.js @@ -253,6 +253,7 @@ class Auth { initAnnotationsStreamListener(); clearTimeout(validationTimeout); this.connectionID = authenticationTokenValidation.connectionId; + this.connectionAuthTime = new Date().getTime(); Session.set('userWillAuth', false); setTimeout(() => resolve(true), 100); break; From aa9719efff338db8e2d2d966e07de9062df48081 Mon Sep 17 00:00:00 2001 From: gabriellpr Date: Mon, 16 May 2022 16:17:10 -0300 Subject: [PATCH 082/268] saving user-status for future reload changed the loading message to be more understandable ... ... --- .../private/static/guest-wait/guest-wait.html | 44 ++++++++++++++++--- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/bigbluebutton-html5/private/static/guest-wait/guest-wait.html b/bigbluebutton-html5/private/static/guest-wait/guest-wait.html index 64f16995ef..de20d54bfa 100755 --- a/bigbluebutton-html5/private/static/guest-wait/guest-wait.html +++ b/bigbluebutton-html5/private/static/guest-wait/guest-wait.html @@ -261,8 +261,6 @@ updateMessage(_('app.guest.guestWait')); enableAnimation(); try { - const ATTEMPT_EVERY_MS = 10 * 1000; // 10 seconds - const sessionToken = getSearchParam('sessionToken'); if (!sessionToken) { @@ -270,8 +268,32 @@ updateMessage(_('app.guest.noSessionToken')); return; } + //First, check that we already have a response + const statusFromStorage = sessionStorage.getItem(`guestStatus_${sessionToken}`); - pollGuestStatus(sessionToken, ATTEMPT_EVERY_MS); + if(statusFromStorage) { + stopUpdatingWaitingPosition(); + + const statusParsed = JSON.parse(statusFromStorage); + const { status, response } = statusParsed; + + if(status === 'ALLOW'){ + updateLobbyMessage(_('app.guest.allow')); + setTimeout(() => { + disableAnimation(); + window.location = response.url; + }, MESSAGE_TIMEOUT); + } else { + redirect( + _('app.guest.' + response.messageKey), + response.url, + ); + } + + return; + } + + pollGuestStatus(sessionToken, 0); } catch (e) { disableAnimation(); console.error(e); @@ -309,21 +331,28 @@ .then((data) => { const code = data.response.returncode; - if (code === 'FAILED') { + const response = data.response; + + const saveStatusResponse = (status, response, token) => { stopUpdatingWaitingPosition(); + sessionStorage.setItem(`guestStatus_${token}`, JSON.stringify({ status, response })); + }; + + if (code === 'FAILED') { + saveStatusResponse(status, response, token); return redirect(_('app.guest.' + data.response.messageKey), data.response.url); } const status = data.response.guestStatus; if (status === 'DENY') { - stopUpdatingWaitingPosition(); + saveStatusResponse(status, response, token); return redirect(_('app.guest.' + data.response.messageKey), data.response.url); } if (status === 'ALLOW') { updateLobbyMessage(_('app.guest.allow')); - stopUpdatingWaitingPosition(); + saveStatusResponse(status, response, token); // Timeout is required by accessibility to allow viewing of the message for a minimum of 3 seconds // before redirecting. setTimeout(() => { @@ -335,7 +364,8 @@ updatePositionInWaitingQueue(data.response.positionInWaitingQueue); updateLobbyMessage(data.response.lobbyMessage); - return pollGuestStatus(token, everyMs); + const ATTEMPT_EVERY_MS = 10 * 1000; // 10 seconds + return pollGuestStatus(token, ATTEMPT_EVERY_MS); }); }, everyMs); }; From 292b6889a64e441ed5b84a8361cdd19be4b106fa Mon Sep 17 00:00:00 2001 From: gabriellpr Date: Tue, 17 May 2022 13:55:58 -0300 Subject: [PATCH 083/268] Update bigbluebutton-html5/private/static/guest-wait/guest-wait.html MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: João Victor Nunes <62393923+JoVictorNunes@users.noreply.github.com> --- bigbluebutton-html5/private/static/guest-wait/guest-wait.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-html5/private/static/guest-wait/guest-wait.html b/bigbluebutton-html5/private/static/guest-wait/guest-wait.html index de20d54bfa..22bb238761 100755 --- a/bigbluebutton-html5/private/static/guest-wait/guest-wait.html +++ b/bigbluebutton-html5/private/static/guest-wait/guest-wait.html @@ -339,7 +339,7 @@ }; if (code === 'FAILED') { - saveStatusResponse(status, response, token); + saveStatusResponse(code, response, token); return redirect(_('app.guest.' + data.response.messageKey), data.response.url); } From e28a5008f7e1e0d9956465335b17519055d3ee1a Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Tue, 17 May 2022 20:00:16 +0200 Subject: [PATCH 084/268] (tldraw): Support text properties with native SVG --- export-annotations/workers/collector.js | 1 + export-annotations/workers/process.js | 178 +++++++++++------------- 2 files changed, 81 insertions(+), 98 deletions(-) diff --git a/export-annotations/workers/collector.js b/export-annotations/workers/collector.js index 9a760d0399..0bdb77e699 100644 --- a/export-annotations/workers/collector.js +++ b/export-annotations/workers/collector.js @@ -67,6 +67,7 @@ let exportJob = JSON.parse(job); let extactSlideAsPDFCommands = [ 'pdftocairo', '-png', + '-scale-to', '1600', '-f', pageNumber, '-l', pageNumber, '-singlefile', diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js index 98b57d5267..af7455ac1b 100644 --- a/export-annotations/workers/process.js +++ b/export-annotations/workers/process.js @@ -28,38 +28,50 @@ const kickOffNotifierWorker = (jobType, filename) => { } function color_to_hex(color) { - color = parseInt(color).toString(16) - return '0'.repeat(6 - color.length) + color + switch(color) { + case 'white': return '#1d1d1d' + case 'lightGray': return '#c6cbd1' + case 'gray': return '#788492' + case 'black': return '#1d1d1d' + case 'green': return '#36b24d' + case 'cyan': return '#0e98ad' + case 'blue': return '#1c7ed6' + case 'indigo': return '#4263eb' + case 'violet': return '#7746f1' + case 'red': return '#ff2133' + case 'orange': return '#ff9433' + case 'yellow': return '#ffc936' + + default: return color + } +} + +function text_size_to_px(size) { + switch(size) { + case 'small': return 28 + case 'medium': return 48 + case 'large': return 96 + + default: return 28 + } +} + +function rad_to_degree(angle) { + return angle * (180 / Math.PI) +} + +function determine_font_from_family(family) { + switch(family) { + case 'script': return 'Caveat Brush' + + default: return family + } } function scale_shape(dimension, coord) { return (coord / 100.0 * dimension); } -function render_HTMLTextBox(htmlFilePath, id, width, height) { - let commands = [ - 'wkhtmltoimage', - '--format', 'png', - '--encoding', `${config.process.whiteboardTextEncoding}`, - '--transparent', - '--crop-w', width, - '--crop-h', height, - '--log-level', 'none', - '--quality', '100', - htmlFilePath, path.join(dropbox, `text${id}.png`) - ] - - execSync(commands.join(' '), (error, stderr) => { - if (error) { - logger.error(`Error when rendering text box for string "${string}" with wkhtmltoimage: ${error.message}`); - } - - if (stderr) { - logger.error(`stderr when rendering text box for string "${string}" with wkhtmltoimage: ${stderr}`); - } - }) -} - function overlay_ellipse(svg, annotation, w, h) { let shapeColor = color_to_hex(annotation.color); let fill = annotation.fill ? `#${shapeColor}` : 'none'; @@ -258,81 +270,57 @@ function overlay_triangle(svg, annotation, w, h) { }).up() } -function overlay_text(svg, annotation, w, h) { +function overlay_text(svg, annotation) { + let fontColor = color_to_hex(annotation.style.color); + let fontSize = text_size_to_px(annotation.style.size); + let rotation = rad_to_degree(annotation.rotation); + let font = determine_font_from_family(annotation.style.font); - let fontColor = color_to_hex(annotation.fontColor); - let textBoxWidth = Math.round(scale_shape(w, annotation.textBoxWidth)); - let textBoxHeight = Math.round(scale_shape(h, annotation.textBoxHeight)); - let textBox_x = Math.round(scale_shape(w, annotation.x)); - let textBox_y = Math.round(scale_shape(h, annotation.y)); + let [textBox_x, textBox_y] = annotation.point; + let textNode = svg.ele('text', { + 'x': textBox_x, + 'y': textBox_y, + 'transform-box': 'fill-box', + 'transform-origin': 'center center', + 'transform': `translate(${textBox_x}, ${textBox_y}) rotate(${rotation}) scale(${annotation.style.scale})`, + 'font-size': fontSize, + 'font-family': font, + 'text-anchor': annotation.style.textAlign, + 'fill': fontColor, + }); - let fontSize = scale_shape(h, annotation.calcedFontSize) - let style = [ - `width:${textBoxWidth}px;`, - `height:${textBoxHeight}px;`, - `color:#${fontColor};`, - "word-wrap:break-word;", - "font-family:Arial;", - `font-size:${fontSize}px` - ] - - var html = twemoji.parse( - ` - - -

- ${annotation.text.split('\n').join('
')} -

- `); - - var htmlFilePath = path.join(dropbox, `text${annotation.id}.html`) - - fs.writeFileSync(htmlFilePath, html, function (err) { - if (err) logger.error(err) - }) - - render_HTMLTextBox(htmlFilePath, annotation.id, textBoxWidth, textBoxHeight) - - svg.ele('image', { - 'xlink:href': `file://${dropbox}/text${annotation.id}.png`, - x: textBox_x, - y: textBox_y - fontSize, - width: textBoxWidth, - height: textBoxHeight, - }).up(); + for (let line of annotation.text.split('\n')) { + if (line === '\n') { line = '' } + textNode.ele('tspan', { x: textBox_x, dy: '1em' }).txt(line).up() + } } function overlay_annotations(svg, currentSlideAnnotations, w, h) { for(let annotation of currentSlideAnnotations) { - switch (annotation.annotationType) { - case 'ellipse': - overlay_ellipse(svg, annotation.annotationInfo, w, h); - break; - case 'line': - overlay_line(svg, annotation.annotationInfo, w, h); - break; - case 'poll_result': - overlay_poll(svg, annotation.annotationInfo, w, h); - break; - case 'pencil': - overlay_pencil(svg, annotation.annotationInfo, w, h); - break; - case 'rectangle': - overlay_rectangle(svg, annotation.annotationInfo, w, h); - break; + switch (annotation.annotationInfo.type) { + // case 'ellipse': + // overlay_ellipse(svg, annotation.annotationInfo, w, h); + // break; + // case 'line': + // overlay_line(svg, annotation.annotationInfo, w, h); + // break; + // case 'poll_result': + // overlay_poll(svg, annotation.annotationInfo, w, h); + // break; + // case 'pencil': + // overlay_pencil(svg, annotation.annotationInfo, w, h); + // break; + // case 'rectangle': + // overlay_rectangle(svg, annotation.annotationInfo, w, h); + // break; case 'text': - overlay_text(svg, annotation.annotationInfo, w, h); - break; - case 'triangle': - overlay_triangle(svg, annotation.annotationInfo, w, h); + overlay_text(svg, annotation.annotationInfo); break; + // case 'triangle': + // overlay_triangle(svg, annotation.annotationInfo, w, h); + // break; default: - logger.error(`Unknown annotation type ${annotation.annotationType}.`); + logger.error(`Unknown annotation type ${annotation.annotationInfo.type}.`); } } } @@ -356,11 +344,6 @@ for (let currentSlide of pages) { var slideWidth = dimensions.width; var slideHeight = dimensions.height; - var panzoom_x = -currentSlide.xOffset * MAGIC_MYSTERY_NUMBER / 100.0 * slideWidth - var panzoom_y = -currentSlide.yOffset * MAGIC_MYSTERY_NUMBER / 100.0 * slideHeight - var panzoom_w = scale_shape(slideWidth, currentSlide.widthRatio) - var panzoom_h = scale_shape(slideHeight, currentSlide.heightRatio) - // Create the SVG slide with the background image let svg = create({ version: '1.0', encoding: 'UTF-8' }) .ele('svg', { @@ -368,7 +351,6 @@ for (let currentSlide of pages) { 'xmlns:xlink': 'http://www.w3.org/1999/xlink', width: slideWidth, height: slideHeight, - viewBox: `${panzoom_x} ${panzoom_y} ${panzoom_w} ${panzoom_h}` }) .dtd({ pubID: '-//W3C//DTD SVG 1.1//EN', From aa53d88da70febe8277c3c8b5940c549b24b17e3 Mon Sep 17 00:00:00 2001 From: Ramon Souza Date: Wed, 18 May 2022 15:46:50 -0300 Subject: [PATCH 085/268] removes userlist debounce, adds userlist throttle --- .../components-data/users-context/service.js | 24 +++++++++---------- .../user-participants/container.jsx | 6 ++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/components-data/users-context/service.js b/bigbluebutton-html5/imports/ui/components/components-data/users-context/service.js index 5e05471f4c..72d966dc8d 100644 --- a/bigbluebutton-html5/imports/ui/components/components-data/users-context/service.js +++ b/bigbluebutton-html5/imports/ui/components/components-data/users-context/service.js @@ -1,8 +1,10 @@ -import { useState, useContext, useEffect } from 'react'; +import { + useState, useContext, useRef, useEffect, +} from 'react'; import { UsersContext } from '/imports/ui/components/components-data/users-context/context'; +import { throttle } from 'lodash'; -const USER_JOIN_UPDATE_TIMEOUT = 1000; -let updateTimeout = null; +const USER_JOIN_UPDATE_THROTTLE_TIME = 1000; export default function useContextUsers() { const usingUsersContext = useContext(UsersContext); @@ -11,17 +13,15 @@ export default function useContextUsers() { const [users, setUsers] = useState(null); const [isReady, setIsReady] = useState(true); + const throttledSetUsers = useRef(throttle(() => { + setUsers(contextUsers); + setIsReady(true); + }, + USER_JOIN_UPDATE_THROTTLE_TIME, { trailing: true })); + useEffect(() => { setIsReady(false); - - if (updateTimeout) { - clearTimeout(updateTimeout); - } - - updateTimeout = setTimeout(() => { - setUsers(contextUsers); - setIsReady(true); - }, USER_JOIN_UPDATE_TIMEOUT); + throttledSetUsers.current(); }, [contextUsers]); return { diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/container.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/container.jsx index e7aafe52e0..5645bbd853 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/container.jsx @@ -22,9 +22,9 @@ const UserParticipantsContainer = (props) => { const { videoUsers, whiteboardUsers } = props; const { users: contextUsers, isReady } = useContextUsers(); - const currentUser = contextUsers ? contextUsers[Auth.meetingID][Auth.userID] : null; - const usersArray = contextUsers ? Object.values(contextUsers[Auth.meetingID]) : null; - const users = contextUsers ? formatUsers(usersArray, videoUsers, whiteboardUsers) : []; + const currentUser = contextUsers && isReady ? contextUsers[Auth.meetingID][Auth.userID] : null; + const usersArray = contextUsers && isReady ? Object.values(contextUsers[Auth.meetingID]) : null; + const users = contextUsers && isReady ? formatUsers(usersArray, videoUsers, whiteboardUsers) : []; return ( Date: Thu, 19 May 2022 15:53:48 -0300 Subject: [PATCH 086/268] fix export chat on breakout room --- .../imports/ui/components/chat/chat-dropdown/component.jsx | 5 ++--- .../imports/ui/components/chat/chat-dropdown/container.jsx | 7 ++----- bigbluebutton-html5/imports/ui/components/chat/service.js | 4 ++-- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx index 70a57770ea..c0ded217d0 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx @@ -58,7 +58,6 @@ class ChatDropdown extends PureComponent { meetingIsBreakout, meetingName, timeWindowsValues, - users, } = this.props; const clearIcon = 'delete'; @@ -84,7 +83,7 @@ class ChatDropdown extends PureComponent { link.setAttribute( 'href', `data: ${mimeType} ;charset=utf-8,` - + `${encodeURIComponent(ChatService.exportChat(timeWindowsValues, users, intl))}`, + + `${encodeURIComponent(ChatService.exportChat(timeWindowsValues, intl))}`, ); link.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window })); } @@ -101,7 +100,7 @@ class ChatDropdown extends PureComponent { dataTest: "chatCopy", label: intl.formatMessage(intlMessages.copy), onClick: () => { - let chatHistory = ChatService.exportChat(timeWindowsValues, users, intl); + let chatHistory = ChatService.exportChat(timeWindowsValues, intl); navigator.clipboard.writeText(chatHistory).then(() => { alertScreenReader(intl.formatMessage(intlMessages.copySuccess)); }).catch(() => { diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/container.jsx index 7888bb64c1..84e3433e20 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/container.jsx @@ -1,17 +1,14 @@ -import React, { useContext } from 'react'; +import React from 'react'; import { withTracker } from 'meteor/react-meteor-data'; import Auth from '/imports/ui/services/auth'; import Meetings from '/imports/api/meetings'; -import { UsersContext } from '/imports/ui/components/components-data/users-context/context'; import ChatDropdown from './component'; import { layoutSelect } from '../../layout/context'; const ChatDropdownContainer = ({ ...props }) => { - const usingUsersContext = useContext(UsersContext); - const { users } = usingUsersContext; const isRTL = layoutSelect((i) => i.isRTL); - return ; + return ; }; export default withTracker(() => { diff --git a/bigbluebutton-html5/imports/ui/components/chat/service.js b/bigbluebutton-html5/imports/ui/components/chat/service.js index 3e2a658535..b983c4ced5 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/service.js +++ b/bigbluebutton-html5/imports/ui/components/chat/service.js @@ -256,7 +256,7 @@ const htmlDecode = (input) => { }; // Export the chat as [Hour:Min] user: message -const exportChat = (timeWindowList, users, intl) => { +const exportChat = (timeWindowList, intl) => { const messageList = timeWindowList.reduce((acc, timeWindow) => { const msgs = timeWindow.content.map((message) => { const date = new Date(message.time); @@ -270,7 +270,7 @@ const exportChat = (timeWindowList, users, intl) => { let userName = message.id.startsWith(SYSTEM_CHAT_TYPE) ? '' - : `${users[timeWindow.sender].name}: `; + : `${timeWindow.senderName}: `; let messageText = ''; if (message.text === PUBLIC_CHAT_CLEAR) { message.text = intl.formatMessage(intlMessages.publicChatClear); From 148e1bd3552d78ee50facad0296fd4bc00e1cdb5 Mon Sep 17 00:00:00 2001 From: GuiLeme Date: Fri, 20 May 2022 09:18:32 -0300 Subject: [PATCH 087/268] Updated nokogiri to version 1.13.5 --- record-and-playback/core/Gemfile | 2 +- record-and-playback/core/Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/record-and-playback/core/Gemfile b/record-and-playback/core/Gemfile index 9b6b5a3c1b..154131b964 100644 --- a/record-and-playback/core/Gemfile +++ b/record-and-playback/core/Gemfile @@ -26,7 +26,7 @@ gem 'journald-logger', '~> 3.0' gem 'jwt', '~> 2.2' gem 'locale', '~> 2.1' gem 'loofah', '~> 2.3' -gem 'nokogiri', '~> 1.13', '>= 1.13.4' +gem 'nokogiri', '~> 1.13', '>= 1.13.5' gem 'open4', '~> 1.3' gem 'rb-inotify', '~> 0.10' gem 'redis', '~> 4.1' diff --git a/record-and-playback/core/Gemfile.lock b/record-and-playback/core/Gemfile.lock index 8b62e5f3a3..e7f4c92675 100644 --- a/record-and-playback/core/Gemfile.lock +++ b/record-and-playback/core/Gemfile.lock @@ -33,7 +33,7 @@ GEM multi_json (1.15.0) mustermann (1.1.1) ruby2_keywords (~> 0.0.1) - nokogiri (1.13.4) + nokogiri (1.13.5) mini_portile2 (~> 2.8.0) racc (~> 1.4) open4 (1.3.4) @@ -95,7 +95,7 @@ DEPENDENCIES locale (~> 2.1) loofah (~> 2.3) minitest (~> 5.14.1) - nokogiri (~> 1.13, >= 1.13.4) + nokogiri (~> 1.13, >= 1.13.5) open4 (~> 1.3) optimist rake (>= 12.3, < 14) From 1c1b3561611b7b41782f1a8a5f976ed06bfbccd1 Mon Sep 17 00:00:00 2001 From: gabriellpr Date: Thu, 19 May 2022 17:14:56 -0300 Subject: [PATCH 088/268] removing white spaces inside menus refactoring code ... --- .../imports/ui/components/common/menu/component.jsx | 8 ++++---- .../imports/ui/components/common/menu/styles.js | 8 ++++++++ .../ui/stylesheets/styled-components/globalStyles.js | 4 ++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/common/menu/component.jsx b/bigbluebutton-html5/imports/ui/components/common/menu/component.jsx index c09695ac9c..8a0189e329 100644 --- a/bigbluebutton-html5/imports/ui/components/common/menu/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/common/menu/component.jsx @@ -74,8 +74,8 @@ class BBBMenu extends React.Component { paddingRight: '4px', paddingTop: '8px', paddingBottom: '8px', - marginLeft: '4px', - marginRight: '4px' + marginLeft: '0px', + marginRight: '0px', }; if (a.customStyles) { @@ -99,11 +99,11 @@ class BBBMenu extends React.Component { if (close) this.handleClose(event); event.stopPropagation(); }}> -
+ {a.icon ? : null} {label} {a.iconRight ? : null} -
+ , a.divider && ]; diff --git a/bigbluebutton-html5/imports/ui/components/common/menu/styles.js b/bigbluebutton-html5/imports/ui/components/common/menu/styles.js index 75b4a61d7c..47ed616993 100644 --- a/bigbluebutton-html5/imports/ui/components/common/menu/styles.js +++ b/bigbluebutton-html5/imports/ui/components/common/menu/styles.js @@ -6,6 +6,13 @@ import { colorWhite, colorPrimary } from '/imports/ui/stylesheets/styled-compone import { fontSizeLarge } from '/imports/ui/stylesheets/styled-components/typography'; import { mediumUp } from '/imports/ui/stylesheets/styled-components/breakpoints'; +const MenuItemWrapper = styled.div` + display: flex; + flex-flow: row; + width: 100%; + align-items: center; +`; + const Option = styled.div` line-height: 1; margin-right: 1.65rem; @@ -78,6 +85,7 @@ const BBBMenuItem = styled(MenuItem)` `; export default { + MenuItemWrapper, Option, CloseButton, IconRight, diff --git a/bigbluebutton-html5/imports/ui/stylesheets/styled-components/globalStyles.js b/bigbluebutton-html5/imports/ui/stylesheets/styled-components/globalStyles.js index c9cb8b69d0..aa1436e748 100644 --- a/bigbluebutton-html5/imports/ui/stylesheets/styled-components/globalStyles.js +++ b/bigbluebutton-html5/imports/ui/stylesheets/styled-components/globalStyles.js @@ -26,6 +26,10 @@ const GlobalStyle = createGlobalStyle` } } + .MuiList-padding { + padding: 0 !important; + } + .MuiPaper-root { background-color: ${dropdownBg}; border-radius: ${borderRadius}; From fd03bd21bc4a7b0b8a2fff2598019fbed4acec71 Mon Sep 17 00:00:00 2001 From: Joao Victor Date: Fri, 20 May 2022 12:00:28 -0300 Subject: [PATCH 089/268] improvement: chat and user-list export file name date string --- .../chat/chat-dropdown/component.jsx | 9 +++------ .../ui/components/user-list/service.js | 8 +++----- .../imports/utils/string-utils.js | 19 ++++++++++++++++++- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx index c0ded217d0..6593dc6325 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx @@ -4,7 +4,7 @@ import { withModalMounter } from '/imports/ui/components/common/modal/service'; import _ from 'lodash'; import BBBMenu from "/imports/ui/components/common/menu/component"; import Button from '/imports/ui/components/common/button/component'; - +import { getDateString } from '/imports/utils/string-utils'; import { alertScreenReader } from '/imports/utils/dom-utils'; import ChatService from '../service'; @@ -76,13 +76,10 @@ class ChatDropdown extends PureComponent { onClick: () => { const link = document.createElement('a'); const mimeType = 'text/plain'; - const date = new Date(); - const time = `${date.getHours()}-${date.getMinutes()}`; - const dateString = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}_${time}`; - link.setAttribute('download', `bbb-${meetingName}[public-chat]_${dateString}.txt`); + link.setAttribute('download', `bbb-${meetingName}[public-chat]_${getDateString()}.txt`); link.setAttribute( 'href', - `data: ${mimeType} ;charset=utf-8,` + `data: ${mimeType};charset=utf-8,` + `${encodeURIComponent(ChatService.exportChat(timeWindowsValues, intl))}`, ); link.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window })); diff --git a/bigbluebutton-html5/imports/ui/components/user-list/service.js b/bigbluebutton-html5/imports/ui/components/user-list/service.js index 8a48c3754a..f4c1734b3b 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/service.js +++ b/bigbluebutton-html5/imports/ui/components/user-list/service.js @@ -14,6 +14,7 @@ import VideoService from '/imports/ui/components/video-provider/service'; import logger from '/imports/startup/client/logger'; import WhiteboardService from '/imports/ui/components/whiteboard/service'; import { Session } from 'meteor/session'; +import { getDateString } from '/imports/utils/string-utils'; const CHAT_CONFIG = Meteor.settings.public.chat; const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id; @@ -662,13 +663,10 @@ export const getUserNamesLink = (docTitle, fnSortedLabel, lnSortedLabel) => { const link = document.createElement('a'); const meeting = Meetings.findOne({ meetingId: Auth.meetingID }, { fields: { 'meetingProp.name': 1 } }); - const date = new Date(); - const time = `${date.getHours()}-${date.getMinutes()}`; - const dateString = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}_${time}`; - link.setAttribute('download', `bbb-${meeting.meetingProp.name}[users-list]_${dateString}.txt`); + link.setAttribute('download', `bbb-${meeting.meetingProp.name}[users-list]_${getDateString()}.txt`); link.setAttribute( 'href', - `data: ${mimeType} ;charset=utf-16,${encodeURIComponent(namesListsString)}`, + `data: ${mimeType};charset=utf-16,${encodeURIComponent(namesListsString)}`, ); return link; }; diff --git a/bigbluebutton-html5/imports/utils/string-utils.js b/bigbluebutton-html5/imports/utils/string-utils.js index c44a9a0a38..2484dd0923 100644 --- a/bigbluebutton-html5/imports/utils/string-utils.js +++ b/bigbluebutton-html5/imports/utils/string-utils.js @@ -1,3 +1,20 @@ export const capitalizeFirstLetter = (s = '') => s.charAt(0).toUpperCase() + s.slice(1); -export default { capitalizeFirstLetter }; \ No newline at end of file +/** + * Returns a string in the format 'Year-Month-Day_Hour-Minutes'. + * @param {Date} [date] - The Date object. + */ +export const getDateString = (date = new Date()) => { + const hours = date.getHours().toString().padStart(2, 0); + const minutes = date.getMinutes().toString().padStart(2, 0); + const month = (date.getMonth() + 1).toString().padStart(2, 0); + const dayOfMonth = date.getDate().toString().padStart(2, 0); + const time = `${hours}-${minutes}`; + const dateString = `${date.getFullYear()}-${month}-${dayOfMonth}_${time}`; + return dateString; +}; + +export default { + capitalizeFirstLetter, + getDateString, +}; From 8a6c5628bd1fa6864be7a99d63edc8210c3e637f Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Fri, 20 May 2022 13:09:33 -0400 Subject: [PATCH 090/268] fix: set target to jvm-1.11 in akka-bbb-fsesl --- akka-bbb-fsesl/build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/akka-bbb-fsesl/build.sbt b/akka-bbb-fsesl/build.sbt index 0b08fd59ee..5171f7ebef 100755 --- a/akka-bbb-fsesl/build.sbt +++ b/akka-bbb-fsesl/build.sbt @@ -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( From ce93125d3b4192f0908feb1a034d17f96f18ffb0 Mon Sep 17 00:00:00 2001 From: Tiago Jacobs Date: Mon, 25 Apr 2022 09:11:57 -0300 Subject: [PATCH 091/268] Add github workflow to run automated tests --- .github/workflows/automated-tests.yml | 137 ++++++++++++++++++ .../playwright/breakout/breakout.spec.js | 2 +- .../playwright/playwright.config.js | 1 + 3 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/automated-tests.yml diff --git a/.github/workflows/automated-tests.yml b/.github/workflows/automated-tests.yml new file mode 100644 index 0000000000..b5343b82b9 --- /dev/null +++ b/.github/workflows/automated-tests.yml @@ -0,0 +1,137 @@ +name: 'Automated tests' +on: + push: + branches: + - '*' + pull_request: + types: [opened, synchronize, reopened] +jobs: + build-install-and-test: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + - run: ./build/get_external_dependencies.sh + - run: ./build/setup.sh bbb-apps-akka + - run: ./build/setup.sh bbb-config + - run: ./build/setup.sh bbb-etherpad + - run: ./build/setup.sh bbb-freeswitch-core + - run: ./build/setup.sh bbb-freeswitch-sounds + - run: ./build/setup.sh bbb-fsesl-akka + - run: ./build/setup.sh bbb-html5 + - run: ./build/setup.sh bbb-learning-dashboard + - run: ./build/setup.sh bbb-libreoffice-docker + - run: ./build/setup.sh bbb-mkclean + - run: ./build/setup.sh bbb-pads + - run: ./build/setup.sh bbb-playback + - run: ./build/setup.sh bbb-playback-notes + - run: ./build/setup.sh bbb-playback-podcast + - run: ./build/setup.sh bbb-playback-presentation + - run: ./build/setup.sh bbb-playback-screenshare + - run: ./build/setup.sh bbb-record-core + - run: ./build/setup.sh bbb-web + - run: ./build/setup.sh bbb-webrtc-sfu + - run: ./build/setup.sh bigbluebutton + - run: tar cvf artifacts.tar artifacts/ + - name: Archive packages + uses: actions/upload-artifact@v3 + with: + name: artifacts.tar + path: | + artifacts.tar + # - name: Fake package build + # run: | + # sudo sh -c ' + # echo "Faking a package build (to speed up installation test)" + # cd / + # wget -q "http://ci.bbbvm.imdt.com.br/artifacts.tar" + # tar xf artifacts.tar + # ' + - name: Generate CA + run: | + 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 + run: | + sudo sh -c ' + sudo mkdir /usr/local/share/ca-certificates/bbb-dev/ + 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: | + sudo sh -c ' + cd /root/bbb-ci-ssl/ + echo "127.0.0.1 bbb-ci.test" >> /etc/hosts + openssl genrsa -out bbb-ci.test.key 2048 + rm bbb-ci.test.csr bbb-ci.test.crt bbb-ci.test.key + cat > bbb-ci.test.ext << EOF + authorityKeyIdentifier=keyid,issuer + basicConstraints=CA:FALSE + keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment + subjectAltName = @alt_names + [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 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 + + mkdir -p /local/certs/ + cp /root/bbb-ci-ssl/bbb-dev-ca.crt /local/certs/ + 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: | + sudo sh -c ' + apt install -yq dpkg-dev + cd /root && wget -q http://ci.bbbvm.imdt.com.br/cache-3rd-part-packages.tar + cp -r /home/runner/work/bigbluebutton/bigbluebutton/artifacts/ /artifacts/ + cd /artifacts && tar xf /root/cache-3rd-part-packages.tar + cd /artifacts && dpkg-scanpackages . /dev/null | gzip -9c > Packages.gz + echo "deb [trusted=yes] file:/artifacts/ ./" >> /etc/apt/sources.list + ' + - name: Prepare for install + run: | + sudo sh -c ' + apt --purge -y remove apache2-bin + ' + - 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/ + bbb-conf --salt bbbci + bbb-conf --restart + ' + - name: Run tests + 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 + ' diff --git a/bigbluebutton-tests/playwright/breakout/breakout.spec.js b/bigbluebutton-tests/playwright/breakout/breakout.spec.js index de97347d5d..fb5be7b1bd 100644 --- a/bigbluebutton-tests/playwright/breakout/breakout.spec.js +++ b/bigbluebutton-tests/playwright/breakout/breakout.spec.js @@ -3,7 +3,7 @@ const { Create } = require('./create'); const { Join } = require('./join'); test.describe.parallel('Breakout', () => { - test('Create Breakout room', async ({ browser, context, page }) => { + test('Create Breakout room @ci', async ({ browser, context, page }) => { const create = new Create(browser, context); await create.initPages(page); await create.create(); diff --git a/bigbluebutton-tests/playwright/playwright.config.js b/bigbluebutton-tests/playwright/playwright.config.js index 2de26aa7ab..68ba9eb02a 100644 --- a/bigbluebutton-tests/playwright/playwright.config.js +++ b/bigbluebutton-tests/playwright/playwright.config.js @@ -15,6 +15,7 @@ const config = { launchOptions: { args: [ '--no-sandbox', + '--ignore-certificate-errors', '--use-fake-ui-for-media-stream', '--use-fake-device-for-media-stream', ] From a120aeab63b731b115f615e20bff5ece62659e13 Mon Sep 17 00:00:00 2001 From: Tiago Jacobs Date: Mon, 25 Apr 2022 20:20:42 -0300 Subject: [PATCH 092/268] Filter branches that CI tests will run (>=2.5.x) --- .github/workflows/automated-tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/automated-tests.yml b/.github/workflows/automated-tests.yml index b5343b82b9..de8a4c182a 100644 --- a/.github/workflows/automated-tests.yml +++ b/.github/workflows/automated-tests.yml @@ -2,7 +2,9 @@ name: 'Automated tests' on: push: branches: - - '*' + - 'develop' + - 'v2.[5-9].x-release' + - 'v[3-9].*.x-release' pull_request: types: [opened, synchronize, reopened] jobs: From 44d11903e97ffc2d6762e89bdd592a4feb31678a Mon Sep 17 00:00:00 2001 From: Ramon Souza Date: Mon, 23 May 2022 11:45:53 -0300 Subject: [PATCH 093/268] increase dropdown menu padding --- .../imports/ui/components/common/menu/component.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/common/menu/component.jsx b/bigbluebutton-html5/imports/ui/components/common/menu/component.jsx index 8a0189e329..6211a13447 100644 --- a/bigbluebutton-html5/imports/ui/components/common/menu/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/common/menu/component.jsx @@ -70,10 +70,10 @@ class BBBMenu extends React.Component { const emojiSelected = key?.toLowerCase()?.includes(selectedEmoji?.toLowerCase()); let customStyles = { - paddingLeft: '4px', - paddingRight: '4px', - paddingTop: '8px', - paddingBottom: '8px', + paddingLeft: '8px', + paddingRight: '8px', + paddingTop: '12px', + paddingBottom: '12px', marginLeft: '0px', marginRight: '0px', }; From 295ef43f7e82a14d3a7dba600ed25c821581ba8f Mon Sep 17 00:00:00 2001 From: Ramon Souza Date: Mon, 23 May 2022 13:58:51 -0300 Subject: [PATCH 094/268] increase dropdown menu padding --- .../imports/ui/components/common/menu/component.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/common/menu/component.jsx b/bigbluebutton-html5/imports/ui/components/common/menu/component.jsx index 6211a13447..65f8306fcf 100644 --- a/bigbluebutton-html5/imports/ui/components/common/menu/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/common/menu/component.jsx @@ -70,8 +70,8 @@ class BBBMenu extends React.Component { const emojiSelected = key?.toLowerCase()?.includes(selectedEmoji?.toLowerCase()); let customStyles = { - paddingLeft: '8px', - paddingRight: '8px', + paddingLeft: '16px', + paddingRight: '16px', paddingTop: '12px', paddingBottom: '12px', marginLeft: '0px', From a77ea836d88971a5abfd6b1193ef1095c0ce0dbd Mon Sep 17 00:00:00 2001 From: gabriellpr Date: Mon, 23 May 2022 16:22:31 -0300 Subject: [PATCH 095/268] adding border inside shared notes for viewers --- .../imports/ui/components/pads/content/styles.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bigbluebutton-html5/imports/ui/components/pads/content/styles.js b/bigbluebutton-html5/imports/ui/components/pads/content/styles.js index 15d604e381..d59b578ab0 100644 --- a/bigbluebutton-html5/imports/ui/components/pads/content/styles.js +++ b/bigbluebutton-html5/imports/ui/components/pads/content/styles.js @@ -1,6 +1,7 @@ import styled from 'styled-components'; import { colorGray, + colorGrayLightest } from '/imports/ui/stylesheets/styled-components/palette'; const Wrapper = styled.div` @@ -42,6 +43,8 @@ top: 0; const Iframe = styled.iframe` border-width: 0; width: 100%; + border-top: 1px solid ${colorGrayLightest}; + border-bottom: 1px solid ${colorGrayLightest}; `; export default { From 7bfc43b19d506292e922f6189c3fed467e37c403 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Tue, 24 May 2022 10:59:03 +0200 Subject: [PATCH 096/268] Add temporaryPresentationId parameter for upload --- export-annotations/workers/notifier.js | 1 + 1 file changed, 1 insertion(+) diff --git a/export-annotations/workers/notifier.js b/export-annotations/workers/notifier.js index 474516a560..a733e41ba7 100644 --- a/export-annotations/workers/notifier.js +++ b/export-annotations/workers/notifier.js @@ -60,6 +60,7 @@ async function upload(exportJob) { 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() }); From 4ae6eb793379cb4f37ab8ead5d5fe0d839581082 Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Tue, 24 May 2022 13:58:50 +0000 Subject: [PATCH 097/268] build(webhooks): v2.6.0 Update 'bbb-webhooks' version. https://github.com/bigbluebutton/bbb-webhooks/releases/tag/v2.6.0 --- bbb-webhooks.placeholder.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bbb-webhooks.placeholder.sh b/bbb-webhooks.placeholder.sh index 57424645b9..5c1d75386a 100755 --- a/bbb-webhooks.placeholder.sh +++ b/bbb-webhooks.placeholder.sh @@ -1 +1 @@ -git clone --branch v2.3.0 --depth 1 https://github.com/bigbluebutton/bbb-webhooks bbb-webhooks +git clone --branch v2.6.0 --depth 1 https://github.com/bigbluebutton/bbb-webhooks bbb-webhooks From b99186085f3c0450ecee9829bf7e582ac49b2625 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Tue, 24 May 2022 18:35:08 +0200 Subject: [PATCH 098/268] Sticky note support --- export-annotations/config/settings.json | 3 - export-annotations/workers/collector.js | 4 +- export-annotations/workers/process.js | 169 +++++++++++++++--------- 3 files changed, 111 insertions(+), 65 deletions(-) diff --git a/export-annotations/config/settings.json b/export-annotations/config/settings.json index 5962900ba1..6f0f77bcb1 100644 --- a/export-annotations/config/settings.json +++ b/export-annotations/config/settings.json @@ -16,9 +16,6 @@ "protocol": "https", "host": "localhost" }, - "genPollSVG": { - "path": "/usr/local/bigbluebutton/core/scripts/utils/gen_poll_svg" - }, "bbbWeb": { "host": "127.0.0.1", "port": 8090 diff --git a/export-annotations/workers/collector.js b/export-annotations/workers/collector.js index 0bdb77e699..8cf604ba41 100644 --- a/export-annotations/workers/collector.js +++ b/export-annotations/workers/collector.js @@ -67,7 +67,7 @@ let exportJob = JSON.parse(job); let extactSlideAsPDFCommands = [ 'pdftocairo', '-png', - '-scale-to', '1600', + '-scale-to', '1920', '-f', pageNumber, '-l', pageNumber, '-singlefile', @@ -95,7 +95,7 @@ let exportJob = JSON.parse(job); 'convert', `${presentationFile}.jpeg`, '-background', 'white', - '-resize', '1600x1600', + '-resize', '1920x1920', '-auto-orient', '-flatten', `${outputFile}.png` diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js index af7455ac1b..2253043ca4 100644 --- a/export-annotations/workers/process.js +++ b/export-annotations/workers/process.js @@ -27,29 +27,57 @@ const kickOffNotifierWorker = (jobType, filename) => { }) } -function color_to_hex(color) { +function color_to_hex(color, isStickyNote = false) { + if(isStickyNote) { color = `sticky-${color}`} + switch(color) { case 'white': return '#1d1d1d' + case 'sticky-white': return '#fddf8e' case 'lightGray': return '#c6cbd1' + case 'sticky-lightGray': return '#dde0e3' case 'gray': return '#788492' + case 'sticky-gray': return '#b3b9c1' case 'black': return '#1d1d1d' + case 'sticky-black': return '#fddf8e' case 'green': return '#36b24d' + case 'sticky-green': return '#8ed29b' case 'cyan': return '#0e98ad' + case 'sticky-cyan': return '#78c4d0' case 'blue': return '#1c7ed6' + case 'sticky-blue': return '#80b6e6' case 'indigo': return '#4263eb' + case 'sticky-indigo': return '#95a7f2' case 'violet': return '#7746f1' + case 'sticky-violet': return '#b297f5' case 'red': return '#ff2133' + case 'sticky-red': return '#fd838d' case 'orange': return '#ff9433' + case 'sticky-orange': return '#fdc28d' case 'yellow': return '#ffc936' + case 'sticky-yellow': return '#fddf8e' default: return color } } -function text_size_to_px(size) { +function align_to_css_property(alignment) { + switch(alignment) { + case 'start': return 'left' + case 'middle': return 'center' + case 'end': return 'right' + default: return alignment + } +} + +function text_size_to_px(size, isStickyNote = false) { + if(isStickyNote) { size = `sticky-${size}`} + switch(size) { + case 'sticky-small': return 24 case 'small': return 28 + case 'sticky-medium': return 36 case 'medium': return 48 + case 'sticky-large': return 48 case 'large': return 96 default: return 28 @@ -72,6 +100,22 @@ function scale_shape(dimension, coord) { return (coord / 100.0 * dimension); } +function render_HTMLTextBox(htmlFilePath, id, width, height) { + let commands = [ + 'wkhtmltoimage', + '--format', 'png', + '--encoding', `${config.process.whiteboardTextEncoding}`, + '--transparent', + '--crop-w', width, + '--crop-h', height, + '--log-level', 'none', + '--quality', '100', + htmlFilePath, path.join(dropbox, `text${id}.png`) + ] + + execSync(commands.join(' ')); +} + function overlay_ellipse(svg, annotation, w, h) { let shapeColor = color_to_hex(annotation.color); let fill = annotation.fill ? `#${shapeColor}` : 'none'; @@ -181,57 +225,6 @@ function overlay_pencil(svg, annotation, w, h) { } } -function overlay_poll(svg, annotation, w, h) { - if (annotation.result.length == 0) { - return; - } - - let poll_x = scale_shape(w, annotation.points[0]); - let poll_y = scale_shape(h, annotation.points[1]); - let poll_width = Math.round(scale_shape(w, annotation.points[2])); - let poll_height = Math.round(scale_shape(h, annotation.points[3])); - let pollId = annotation.id.replace(/\//g, ''); - let pollSVG = path.join(dropbox, `poll-${pollId}.svg`); - let pollJSON = path.join(dropbox, `poll-${pollId}.json`); - - // Rename 'numVotes' key to 'num_votes' - let pollJSONContent = annotation.result.map(result => { - result.num_votes = result.numVotes; - delete result.numVotes; - return result; - }); - - // Store the poll result in a JSON file - fs.writeFileSync(pollJSON, JSON.stringify(pollJSONContent), function(err) { - if(err) { return logger.error(err); } - }); - - // Create empty SVG poll - fs.writeFileSync(pollSVG, '', function(err) { - if(err) { return logger.error(err); } - }); - - // Render the poll SVG using gen_poll_svg script - execSync(`${config.genPollSVG.path} -i ${pollJSON} -w ${poll_width} -h ${poll_height} -n ${annotation.numResponders} -o ${pollSVG}`, (error, stderr) => { - if (error) { - return logger.error(`Poll generation failed with error: ${error.message}`); - } - - if (stderr) { - return logger.error(`Poll generation failed with stderr: ${stderr}`); - } - }); - - // Add poll image element - svg.ele('image', { - 'xlink:href': `file://${pollSVG}`, - x: poll_x, - y: poll_y, - width: poll_width, - height: poll_height, - }) -} - function overlay_rectangle(svg, annotation, w, h) { let shapeColor = color_to_hex(annotation.color); let fill = annotation.fill ? `#${shapeColor}` : 'none'; @@ -271,6 +264,9 @@ function overlay_triangle(svg, annotation, w, h) { } function overlay_text(svg, annotation) { + + logger.info(annotation); + let fontColor = color_to_hex(annotation.style.color); let fontSize = text_size_to_px(annotation.style.size); let rotation = rad_to_degree(annotation.rotation); @@ -280,12 +276,8 @@ function overlay_text(svg, annotation) { let textNode = svg.ele('text', { 'x': textBox_x, 'y': textBox_y, - 'transform-box': 'fill-box', - 'transform-origin': 'center center', - 'transform': `translate(${textBox_x}, ${textBox_y}) rotate(${rotation}) scale(${annotation.style.scale})`, 'font-size': fontSize, 'font-family': font, - 'text-anchor': annotation.style.textAlign, 'fill': fontColor, }); @@ -295,7 +287,64 @@ function overlay_text(svg, annotation) { } } +function overlay_sticky(svg, annotation) { + + let backgroundColor = color_to_hex(annotation.style.color, true); + let fontSize = text_size_to_px(annotation.style.size, true); + let rotation = rad_to_degree(annotation.rotation); + let font = determine_font_from_family(annotation.style.font); + let textAlign = align_to_css_property(annotation.style.textAlign); + + let [textBoxWidth, textBoxHeight] = annotation.size; + let [textBox_x, textBox_y] = annotation.point; + + var html = twemoji.parse( + ` + + +

${annotation.text.split('\n').join('
')}

+ `); + + var htmlFilePath = path.join(dropbox, `text${annotation.id}.html`) + + fs.writeFileSync(htmlFilePath, html, function (err) { + if (err) logger.error(err); + }) + + render_HTMLTextBox(htmlFilePath, annotation.id, textBoxWidth, textBoxHeight) + + svg.ele('image', { + 'xlink:href': `file://${dropbox}/text${annotation.id}.png`, + x: textBox_x, + y: textBox_y, + width: textBoxWidth, + height: textBoxHeight, + transform: `rotate(${rotation}, ${textBox_x + (textBoxWidth / 2)}, ${textBox_y + (textBoxHeight / 2)})` + }).up(); +} + function overlay_annotations(svg, currentSlideAnnotations, w, h) { + + logger.info(currentSlideAnnotations) + // Order slide annotations by z-index + // currentSlideAnnotations.sort(function (a, b) { + // return a.annotationInfo.childIndex < b.annotationInfo.childIndex; + // }); + // logger.info("SORTED!") + // logger.info(currentSlideAnnotations) + for(let annotation of currentSlideAnnotations) { switch (annotation.annotationInfo.type) { // case 'ellipse': @@ -304,15 +353,15 @@ function overlay_annotations(svg, currentSlideAnnotations, w, h) { // case 'line': // overlay_line(svg, annotation.annotationInfo, w, h); // break; - // case 'poll_result': - // overlay_poll(svg, annotation.annotationInfo, w, h); - // break; // case 'pencil': // overlay_pencil(svg, annotation.annotationInfo, w, h); // break; // case 'rectangle': // overlay_rectangle(svg, annotation.annotationInfo, w, h); // break; + case 'sticky': + overlay_sticky(svg, annotation.annotationInfo); + break; case 'text': overlay_text(svg, annotation.annotationInfo); break; From 3ae69643b2247ee2579a72e41d331ad24c936f5f Mon Sep 17 00:00:00 2001 From: Tainan Felipe Date: Tue, 24 May 2022 17:08:56 -0300 Subject: [PATCH 099/268] Make sure that user is a chat participant when send a message --- .../SendGroupChatMessageMsgHdlr.scala | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/SendGroupChatMessageMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/SendGroupChatMessageMsgHdlr.scala index 7f9a103701..aa522c4db6 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/SendGroupChatMessageMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/SendGroupChatMessageMsgHdlr.scala @@ -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 { From 2a09629698136f725cbbd95bddfff145b53dee8b Mon Sep 17 00:00:00 2001 From: Joao Victor Date: Thu, 19 May 2022 14:28:58 -0300 Subject: [PATCH 100/268] refactor: sidebar content panel UI polishing --- .../breakout-dropdown/component.jsx | 9 +- .../ui/components/breakout-room/component.jsx | 23 ++-- .../ui/components/breakout-room/styles.js | 38 ------ .../ui/components/captions/component.jsx | 89 +++++++------- .../imports/ui/components/captions/styles.js | 69 +---------- .../chat/chat-dropdown/component.jsx | 10 +- .../imports/ui/components/chat/component.jsx | 112 ++++++++---------- .../imports/ui/components/chat/styles.js | 74 +----------- .../ui/components/common/button/styles.js | 7 +- .../common/control-header/component.jsx | 40 +++++++ .../common/control-header/left/component.jsx | 30 +++++ .../common/control-header/left/styles.js | 31 +++++ .../common/control-header/right/component.jsx | 31 +++++ .../common/control-header/right/styles.js | 26 ++++ .../common/control-header/styles.js | 20 ++++ .../imports/ui/components/notes/component.jsx | 38 +++--- .../imports/ui/components/notes/styles.js | 67 +---------- .../imports/ui/components/poll/component.jsx | 41 +++---- .../imports/ui/components/poll/styles.js | 56 --------- .../ui/components/waiting-users/component.jsx | 16 ++- .../ui/components/waiting-users/styles.js | 57 --------- 21 files changed, 344 insertions(+), 540 deletions(-) create mode 100644 bigbluebutton-html5/imports/ui/components/common/control-header/component.jsx create mode 100644 bigbluebutton-html5/imports/ui/components/common/control-header/left/component.jsx create mode 100644 bigbluebutton-html5/imports/ui/components/common/control-header/left/styles.js create mode 100644 bigbluebutton-html5/imports/ui/components/common/control-header/right/component.jsx create mode 100644 bigbluebutton-html5/imports/ui/components/common/control-header/right/styles.js create mode 100644 bigbluebutton-html5/imports/ui/components/common/control-header/styles.js diff --git a/bigbluebutton-html5/imports/ui/components/breakout-room/breakout-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/breakout-room/breakout-dropdown/component.jsx index 06a02b49a5..36420b48f9 100644 --- a/bigbluebutton-html5/imports/ui/components/breakout-room/breakout-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/breakout-room/breakout-dropdown/component.jsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import { defineMessages, injectIntl } from 'react-intl'; import BBBMenu from "/imports/ui/components/common/menu/component"; -import Button from '/imports/ui/components/common/button/component'; +import Trigger from "/imports/ui/components/common/control-header/right/component"; const intlMessages = defineMessages({ options: { @@ -72,14 +72,9 @@ class BreakoutDropdown extends PureComponent { <> null} diff --git a/bigbluebutton-html5/imports/ui/components/breakout-room/component.jsx b/bigbluebutton-html5/imports/ui/components/breakout-room/component.jsx index 28bcd852f6..f7503a5303 100644 --- a/bigbluebutton-html5/imports/ui/components/breakout-room/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/breakout-room/component.jsx @@ -1,7 +1,6 @@ import React, { PureComponent } from 'react'; import { defineMessages, injectIntl } from 'react-intl'; import _ from 'lodash'; -import Button from '/imports/ui/components/common/button/component'; import { Session } from 'meteor/session'; import logger from '/imports/startup/client/logger'; import Styled from './styles'; @@ -16,6 +15,7 @@ import Settings from '/imports/ui/services/settings'; import BreakoutDropdown from '/imports/ui/components/breakout-room/breakout-dropdown/component'; import Users from '/imports/api/users'; import Auth from '/imports/ui/services/auth'; +import Header from '/imports/ui/components/common/control-header/component'; const intlMessages = defineMessages({ breakoutTitle: { @@ -551,16 +551,15 @@ class BreakoutRoom extends PureComponent { } = this.props; return ( this.panel = n}> - - { +
{ this.closePanel(); - }} - /> - { amIModerator && ( + }, + }} + customRightButton={amIModerator && ( { @@ -571,8 +570,8 @@ class BreakoutRoom extends PureComponent { amIModerator={amIModerator} isRTL={isRTL} /> - ) } - + )} + /> {this.renderDuration()} {amIModerator ? ( diff --git a/bigbluebutton-html5/imports/ui/components/breakout-room/styles.js b/bigbluebutton-html5/imports/ui/components/breakout-room/styles.js index afe8b7f125..ccea3cc3c5 100644 --- a/bigbluebutton-html5/imports/ui/components/breakout-room/styles.js +++ b/bigbluebutton-html5/imports/ui/components/breakout-room/styles.js @@ -1,6 +1,5 @@ import styled, { css, keyframes } from 'styled-components'; import { - systemMessageBorderColor, mdPaddingX, borderSize, listItemBgHover, borderSizeSmall, @@ -11,7 +10,6 @@ import { colorPrimary, colorGray, colorDanger, - colorGrayDark, userListBg, colorWhite, colorGrayLighter, @@ -229,33 +227,6 @@ const Panel = styled(ScrollboxVertical)` height: 100%; `; -const HeaderButton = styled(Button)` - display: flex; - flex-direction: row; - justify-content: space-between; - position: relative; - padding-left: 0; - padding-right: inherit; - background: none !important; - - [dir="rtl"] & { - margin: 0 0 2rem auto; - padding-left: inherit; - padding-right: 0; - } - - & > i { - color: ${colorGrayDark}; - - [dir="rtl"] & { - -webkit-transform: scale(-1, 1); - -moz-transform: scale(-1, 1); - -ms-transform: scale(-1, 1); - -o-transform: scale(-1, 1); - transform: scale(-1, 1); - } - }`; - const Separator = styled.div` position: relative; width: 100%; @@ -265,13 +236,6 @@ const Separator = styled.div` margin: 30px 0px; `; -const Header = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - padding-bottom: ${jumboPaddingY}; -`; - const FlexRow = styled.div` display: flex; flex-wrap: nowrap; @@ -297,8 +261,6 @@ export default { EndButton, Duration, Panel, - HeaderButton, Separator, - Header, FlexRow, }; diff --git a/bigbluebutton-html5/imports/ui/components/captions/component.jsx b/bigbluebutton-html5/imports/ui/components/captions/component.jsx index 7f15414f83..3b5f4a5a82 100644 --- a/bigbluebutton-html5/imports/ui/components/captions/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/captions/component.jsx @@ -8,6 +8,7 @@ import Service from '/imports/ui/components/captions/service'; import Styled from './styles'; import { PANELS, ACTIONS } from '/imports/ui/components/layout/enums'; import browserInfo from '/imports/utils/browserInfo'; +import Header from '/imports/ui/components/common/control-header/component'; const intlMessages = defineMessages({ hide: { @@ -71,54 +72,50 @@ const Captions = ({ return ( - - - { - layoutContextDispatch({ - type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN, - value: false, - }); - layoutContextDispatch({ - type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL, - value: PANELS.NONE, - }); - }} - aria-label={intl.formatMessage(intlMessages.hide)} - label={name} - icon={isRTL ? 'right_arrow' : 'left_arrow'} - /> - - {Service.amICaptionsOwner(ownerId) - ? ( - -