Merge branch 'v2.6.x-release' into ask-before-leave

This commit is contained in:
Ramón Souza 2022-12-06 09:04:28 -03:00 committed by GitHub
commit 55ae884f5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
355 changed files with 11528 additions and 14960 deletions

View File

@ -74,7 +74,6 @@ class BigBlueButtonActor(
case m: CreateMeetingReqMsg => handleCreateMeetingReqMsg(m)
case m: RegisterUserReqMsg => handleRegisterUserReqMsg(m)
case m: EjectDuplicateUserReqMsg => handleEjectDuplicateUserReqMsg(m)
case m: GetAllMeetingsReqMsg => handleGetAllMeetingsReqMsg(m)
case m: GetRunningMeetingsReqMsg => handleGetRunningMeetingsReqMsg(m)
case m: CheckAlivePingSysMsg => handleCheckAlivePingSysMsg(m)
@ -105,16 +104,6 @@ class BigBlueButtonActor(
}
}
def handleEjectDuplicateUserReqMsg(msg: EjectDuplicateUserReqMsg): Unit = {
log.debug("RECEIVED EjectDuplicateUserReqMsg msg {}", msg)
for {
m <- RunningMeetings.findWithId(meetings, msg.header.meetingId)
} yield {
log.debug("FORWARDING EjectDuplicateUserReqMsg")
m.actorRef forward (msg)
}
}
def handleCreateMeetingReqMsg(msg: CreateMeetingReqMsg): Unit = {
log.debug("RECEIVED CreateMeetingReqMsg msg {}", msg)

View File

@ -104,6 +104,21 @@ case class SendMessageToBreakoutRoomInternalMsg(parentId: String, breakoutId: St
*/
case class EjectUserFromBreakoutInternalMsg(parentId: String, breakoutId: String, extUserId: String, ejectedBy: String, reason: String, reasonCode: String, ban: Boolean) extends InMessage
/**
* Sent by parent meeting to breakout room to import annotated slides.
* @param userId
* @param parentMeetingId
* @param allPages
*/
case class CapturePresentationReqInternalMsg(userId: String, parentMeetingId: String, allPages: Boolean = true) extends InMessage
/**
* Sent by parent meeting to breakout room to import shared notes.
* @param parentMeetingId
* @param meetingName
*/
case class CaptureSharedNotesReqInternalMsg(parentMeetingId: String, meetingName: String) extends InMessage
// DeskShare
case class DeskShareStartedRequest(conferenceName: String, callerId: String, callerIdName: String) extends InMessage
case class DeskShareStoppedRequest(conferenceName: String, callerId: String, callerIdName: String) extends InMessage

View File

@ -13,9 +13,11 @@ object BreakoutModel {
isDefaultName: Boolean,
freeJoin: Boolean,
voiceConf: String,
assignedUsers: Vector[String]
assignedUsers: Vector[String],
captureNotes: Boolean,
captureSlides: Boolean,
): BreakoutRoom2x = {
new BreakoutRoom2x(id, externalId, name, parentId, sequence, shortName, isDefaultName, freeJoin, voiceConf, assignedUsers, Vector(), Vector(), None, false)
new BreakoutRoom2x(id, externalId, name, parentId, sequence, shortName, isDefaultName, freeJoin, voiceConf, assignedUsers, Vector(), Vector(), None, false, captureNotes, captureSlides)
}
}

View File

@ -1,9 +1,6 @@
package org.bigbluebutton.core.apps
import org.bigbluebutton.core.util.jhotdraw.BezierWrapper
import scala.collection.immutable.List
import scala.collection.immutable.HashMap
import scala.collection.JavaConverters._
import org.bigbluebutton.common2.msgs.AnnotationVO
import org.bigbluebutton.core.apps.whiteboard.Whiteboard
import org.bigbluebutton.SystemConfiguration
@ -24,86 +21,83 @@ class WhiteboardModel extends SystemConfiguration {
}
private def createWhiteboard(wbId: String): Whiteboard = {
new Whiteboard(
Whiteboard(
wbId,
Array.empty[String],
Array.empty[String],
System.currentTimeMillis(),
new HashMap[String, Map[String, AnnotationVO]]()
new HashMap[String, AnnotationVO]
)
}
private def getAnnotationsByUserId(wb: Whiteboard, id: String): Map[String, AnnotationVO] = {
wb.annotationsMap.get(id).getOrElse(Map[String, AnnotationVO]())
private def deepMerge(test: Map[String, _], that: Map[String, _]): Map[String, _] =
(for (k <- test.keys ++ that.keys) yield {
val newValue =
(test.get(k), that.get(k)) match {
case (Some(v), None) => v
case (None, Some(v)) => v
case (Some(v1), Some(v2)) =>
if (v1.isInstanceOf[Map[String, _]] && v2.isInstanceOf[Map[String, _]])
deepMerge(v1.asInstanceOf[Map[String, _]], v2.asInstanceOf[Map[String, _]])
else v2
case (_, _) => ???
}
k -> newValue
}).toMap
def addAnnotations(wbId: String, userId: String, annotations: Array[AnnotationVO]): Array[AnnotationVO] = {
def addAnnotations(wbId: String, userId: String, annotations: Array[AnnotationVO], isPresenter: Boolean, isModerator: Boolean): Array[AnnotationVO] = {
var annotationsAdded = Array[AnnotationVO]()
val wb = getWhiteboard(wbId)
val usersAnnotations = getAnnotationsByUserId(wb, userId)
var newUserAnnotations = usersAnnotations
var newAnnotationsMap = wb.annotationsMap
for (annotation <- annotations) {
newUserAnnotations = newUserAnnotations + (annotation.id -> annotation)
println("Adding annotation to page [" + wb.id + "]. After numAnnotations=[" + newUserAnnotations.size + "].")
val oldAnnotation = wb.annotationsMap.get(annotation.id)
if (!oldAnnotation.isEmpty) {
val hasPermission = isPresenter || isModerator || oldAnnotation.get.userId == userId
if (hasPermission) {
val newAnnotation = oldAnnotation.get.copy(annotationInfo = deepMerge(oldAnnotation.get.annotationInfo, annotation.annotationInfo))
newAnnotationsMap += (annotation.id -> newAnnotation)
annotationsAdded :+= annotation
println(s"Updated annotation onpage [${wb.id}]. After numAnnotations=[${newAnnotationsMap.size}].")
} else {
println(s"User $userId doesn't have permission to edit annotation ${annotation.id}, ignoring...")
}
} else if (annotation.annotationInfo.contains("type")) {
newAnnotationsMap += (annotation.id -> annotation)
annotationsAdded :+= annotation
println(s"Adding annotation to page [${wb.id}]. After numAnnotations=[${newAnnotationsMap.size}].")
} else {
println(s"New annotation [${annotation.id}] with no type, ignoring (probably received a remove message before and now the shape is incomplete, ignoring...")
}
}
val newAnnotationsMap = wb.annotationsMap + (userId -> newUserAnnotations)
val newWb = wb.copy(annotationsMap = newAnnotationsMap)
saveWhiteboard(newWb)
annotations
annotationsAdded
}
def getHistory(wbId: String): Array[AnnotationVO] = {
//wb.annotationsMap.values.flatten.toArray.sortBy(_.position);
val wb = getWhiteboard(wbId)
var annotations = Array[AnnotationVO]()
// TODO: revisit this, probably there is a one-liner simple solution
wb.annotationsMap.values.foreach(
user => user.values.foreach(
annotation => annotations = annotations :+ annotation
)
)
annotations
wb.annotationsMap.values.toArray
}
def clearWhiteboard(wbId: String, userId: String): Option[Boolean] = {
var cleared: Option[Boolean] = None
if (hasWhiteboard(wbId)) {
val wb = getWhiteboard(wbId)
if (wb.multiUser.contains(userId)) {
if (wb.annotationsMap.contains(userId)) {
val newWb = wb.copy(annotationsMap = wb.annotationsMap - userId)
saveWhiteboard(newWb)
cleared = Some(false)
}
} else {
if (wb.annotationsMap.nonEmpty) {
val newWb = wb.copy(annotationsMap = new HashMap[String, Map[String, AnnotationVO]]())
saveWhiteboard(newWb)
cleared = Some(true)
}
}
}
cleared
}
def deleteAnnotations(wbId: String, userId: String, annotationsIds: Array[String]): Array[String] = {
def deleteAnnotations(wbId: String, userId: String, annotationsIds: Array[String], isPresenter: Boolean, isModerator: Boolean): Array[String] = {
var annotationsIdsRemoved = Array[String]()
val wb = getWhiteboard(wbId)
var newAnnotationsMap = wb.annotationsMap
val usersAnnotations = getAnnotationsByUserId(wb, userId)
var newUserAnnotations = usersAnnotations
for (annotationId <- annotationsIds) {
val annotation = usersAnnotations.get(annotationId)
val annotation = wb.annotationsMap.get(annotationId)
//not empty and annotation exists
if (!usersAnnotations.isEmpty && !annotation.isEmpty) {
newUserAnnotations = newUserAnnotations - annotationId
println("Removing annotation on page [" + wb.id + "]. After numAnnotations=[" + newUserAnnotations.size + "].")
annotationsIdsRemoved = annotationsIdsRemoved :+ annotationId
if (!annotation.isEmpty) {
val hasPermission = isPresenter || isModerator || annotation.get.userId == userId
if (hasPermission) {
newAnnotationsMap -= annotationId
println("Removing annotation on page [" + wb.id + "]. After numAnnotations=[" + newAnnotationsMap.size + "].")
annotationsIdsRemoved :+= annotationId
} else {
println("User doesn't have permission to remove this annotation, ignoring...")
}
}
}
val newAnnotationsMap = wb.annotationsMap + (userId -> newUserAnnotations)
val newWb = wb.copy(annotationsMap = newAnnotationsMap)
saveWhiteboard(newWb)
annotationsIdsRemoved

View File

@ -52,7 +52,7 @@ trait BreakoutRoomCreatedMsgHdlr {
(redirectToHtml5JoinURL, redirectJoinURL) <- BreakoutHdlrHelpers.getRedirectUrls(liveMeeting, user, r.externalId, r.sequence.toString())
} yield (user -> redirectToHtml5JoinURL)
new BreakoutRoomInfo(r.name, r.externalId, r.id, r.sequence, r.shortName, r.isDefaultName, r.freeJoin, html5JoinUrls.toMap)
new BreakoutRoomInfo(r.name, r.externalId, r.id, r.sequence, r.shortName, r.isDefaultName, r.freeJoin, html5JoinUrls.toMap, r.captureNotes, r.captureSlides)
}
log.info("Sending breakout rooms list to {} with containing {} room(s)", liveMeeting.props.meetingProp.intId, breakoutRooms.length)
@ -79,7 +79,7 @@ trait BreakoutRoomCreatedMsgHdlr {
BbbCommonEnvCoreMsg(envelope, event)
}
val breakoutInfo = BreakoutRoomInfo(room.name, room.externalId, room.id, room.sequence, room.shortName, room.isDefaultName, room.freeJoin, Map())
val breakoutInfo = BreakoutRoomInfo(room.name, room.externalId, room.id, room.sequence, room.shortName, room.isDefaultName, room.freeJoin, Map(), room.captureNotes, room.captureSlides)
val event = build(liveMeeting.props.meetingProp.intId, breakoutInfo)
outGW.send(event)

View File

@ -28,7 +28,7 @@ trait BreakoutRoomsListMsgHdlr {
breakoutModel <- state.breakout
} yield {
val rooms = breakoutModel.rooms.values.toVector map { r =>
new BreakoutRoomInfo(r.name, r.externalId, r.id, r.sequence, r.shortName, r.isDefaultName, r.freeJoin, Map())
new BreakoutRoomInfo(r.name, r.externalId, r.id, r.sequence, r.shortName, r.isDefaultName, r.freeJoin, Map(), r.captureNotes, r.captureSlides)
}
val ready = breakoutModel.hasAllStarted()
broadcastEvent(rooms, ready)

View File

@ -52,7 +52,7 @@ trait CreateBreakoutRoomsCmdMsgHdlr extends RightsManagementTrait {
val (internalId, externalId) = BreakoutRoomsUtil.createMeetingIds(liveMeeting.props.meetingProp.intId, i)
val voiceConf = BreakoutRoomsUtil.createVoiceConfId(liveMeeting.props.voiceProp.voiceConf, i)
val breakout = BreakoutModel.create(parentId, internalId, externalId, room.name, room.sequence, room.shortName, room.isDefaultName, room.freeJoin, voiceConf, room.users)
val breakout = BreakoutModel.create(parentId, internalId, externalId, room.name, room.sequence, room.shortName, room.isDefaultName, room.freeJoin, voiceConf, room.users, msg.body.captureNotes, msg.body.captureSlides)
rooms = rooms + (breakout.id -> breakout)
}
@ -70,7 +70,9 @@ trait CreateBreakoutRoomsCmdMsgHdlr extends RightsManagementTrait {
liveMeeting.props.password.moderatorPass,
liveMeeting.props.password.viewerPass,
presId, presSlide, msg.body.record,
liveMeeting.props.breakoutProps.privateChatEnabled
liveMeeting.props.breakoutProps.privateChatEnabled,
breakout.captureNotes,
breakout.captureSlides,
)
val event = buildCreateBreakoutRoomSysCmdMsg(liveMeeting.props.meetingProp.intId, roomDetail)

View File

@ -14,8 +14,8 @@ trait EndAllBreakoutRoomsMsgHdlr extends RightsManagementTrait {
val outGW: OutMsgRouter
def handleEndAllBreakoutRoomsMsg(msg: EndAllBreakoutRoomsMsg, state: MeetingState2x): MeetingState2x = {
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
val meetingId = liveMeeting.props.meetingProp.intId
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
val reason = "No permission to end breakout rooms for meeting."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)
state
@ -24,11 +24,11 @@ trait EndAllBreakoutRoomsMsgHdlr extends RightsManagementTrait {
model <- state.breakout
} yield {
model.rooms.values.foreach { room =>
eventBus.publish(BigBlueButtonEvent(room.id, EndBreakoutRoomInternalMsg(props.breakoutProps.parentId, room.id, MeetingEndReason.BREAKOUT_ENDED_BY_MOD)))
eventBus.publish(BigBlueButtonEvent(room.id, EndBreakoutRoomInternalMsg(meetingId, room.id, MeetingEndReason.BREAKOUT_ENDED_BY_MOD)))
}
val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg(
liveMeeting.props.meetingProp.intId,
meetingId,
"info",
"rooms",
"app.toast.breakoutRoomEnded",

View File

@ -1,7 +1,7 @@
package org.bigbluebutton.core.apps.breakout
import org.bigbluebutton.core.api.EndBreakoutRoomInternalMsg
import org.bigbluebutton.core.bus.{ InternalEventBus }
import org.bigbluebutton.core.api.{ CaptureSharedNotesReqInternalMsg, CapturePresentationReqInternalMsg, EndBreakoutRoomInternalMsg }
import org.bigbluebutton.core.bus.{ BigBlueButtonEvent, InternalEventBus }
import org.bigbluebutton.core.running.{ BaseMeetingActor, HandlerHelpers, LiveMeeting, OutMsgRouter }
trait EndBreakoutRoomInternalMsgHdlr extends HandlerHelpers {
@ -12,6 +12,18 @@ trait EndBreakoutRoomInternalMsgHdlr extends HandlerHelpers {
val eventBus: InternalEventBus
def handleEndBreakoutRoomInternalMsg(msg: EndBreakoutRoomInternalMsg): Unit = {
if (liveMeeting.props.breakoutProps.captureSlides) {
val captureSlidesEvent = BigBlueButtonEvent(msg.breakoutId, CapturePresentationReqInternalMsg("system", msg.parentId))
eventBus.publish(captureSlidesEvent)
}
if (liveMeeting.props.breakoutProps.captureNotes) {
val meetingName: String = liveMeeting.props.meetingProp.name
val captureNotesEvent = BigBlueButtonEvent(msg.breakoutId, CaptureSharedNotesReqInternalMsg(msg.parentId, meetingName))
eventBus.publish(captureNotesEvent)
}
log.info("Breakout room {} ended by parent meeting {}.", msg.breakoutId, msg.parentId)
sendEndMeetingDueToExpiry(msg.reason, eventBus, outGW, liveMeeting, "system")
}

View File

@ -84,10 +84,12 @@ trait CreateGroupChatReqMsgHdlr extends SystemConfiguration {
BbbCoreEnvelope(name, routing)
}
def makeBody(chatId: String,
def makeBody(
chatId: String,
access: String, correlationId: String,
createdBy: GroupChatUser, users: Vector[GroupChatUser],
msgs: Vector[GroupChatMsgToUser]): GroupChatCreatedEvtMsgBody = {
msgs: Vector[GroupChatMsgToUser]
): GroupChatCreatedEvtMsgBody = {
GroupChatCreatedEvtMsgBody(correlationId, chatId, createdBy,
access, users, msgs)
}

View File

@ -0,0 +1,38 @@
package org.bigbluebutton.core.apps.pads
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.models.Pads
import org.bigbluebutton.core.running.LiveMeeting
trait PadPinnedReqMsgHdlr extends RightsManagementTrait {
this: PadsApp2x =>
def handle(msg: PadPinnedReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
def broadcastEvent(groupId: String, pinned: Boolean): Unit = {
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
val envelope = BbbCoreEnvelope(PadPinnedEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(PadPinnedEvtMsg.NAME, liveMeeting.props.meetingProp.intId, "not-used")
val body = PadPinnedEvtMsgBody(groupId, pinned)
val event = PadPinnedEvtMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
bus.outGW.send(msgEvent)
}
if (Pads.hasAccess(liveMeeting, msg.body.externalId, msg.header.userId)) {
if (permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "You need to be the presenter to pin Shared Notes"
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
} else {
Pads.getGroup(liveMeeting.pads, msg.body.externalId) match {
case Some(group) => broadcastEvent(group.externalId, msg.body.pinned)
case _ =>
}
}
}
}
}

View File

@ -13,6 +13,7 @@ class PadsApp2x(implicit val context: ActorContext)
with PadUpdatedSysMsgHdlr
with PadContentSysMsgHdlr
with PadPatchSysMsgHdlr
with PadUpdatePubMsgHdlr {
with PadUpdatePubMsgHdlr
with PadPinnedReqMsgHdlr {
}

View File

@ -45,6 +45,7 @@ trait RespondToPollReqMsgHdlr {
bus.outGW.send(msgEvent)
}
if (Polls.checkUserResponded(msg.body.pollId, msg.header.userId, liveMeeting.polls) == false) {
for {
(pollId: String, updatedPoll: SimplePollResultOutVO) <- Polls.handleRespondToPollReqMsg(msg.header.userId, msg.body.pollId,
msg.body.questionId, msg.body.answerIds, liveMeeting)
@ -67,5 +68,8 @@ trait RespondToPollReqMsgHdlr {
broadcastUserRespondedToPollRespMsg(msg, pollId, msg.body.answerIds, presenter.intId)
}
}
} else {
log.info("Ignoring typed answer from user {} once user already added an answer to this poll {} in meeting {}", msg.header.userId, msg.body.pollId, msg.header.meetingId)
}
}
}

View File

@ -34,6 +34,9 @@ trait RespondToTypedPollReqMsgHdlr {
bus.outGW.send(msgEvent)
}
if (Polls.isResponsePollType(msg.body.pollId, liveMeeting.polls) &&
Polls.checkUserResponded(msg.body.pollId, msg.header.userId, liveMeeting.polls) == false &&
Polls.checkUserAddedQuestion(msg.body.pollId, msg.header.userId, liveMeeting.polls) == false) {
for {
(pollId: String, updatedPoll: SimplePollResultOutVO) <- Polls.handleRespondToTypedPollReqMsg(msg.header.userId, msg.body.pollId,
msg.body.questionId, msg.body.answer, liveMeeting)
@ -46,5 +49,8 @@ trait RespondToTypedPollReqMsgHdlr {
broadcastUserRespondedToTypedPollRespMsg(msg, pollId, msg.body.answer, presenter.intId)
}
}
} else {
log.info("Ignoring typed answer from user {} once user already added an answer to this poll {} in meeting {}", msg.header.userId, msg.body.pollId, msg.header.meetingId)
}
}
}

View File

@ -20,7 +20,7 @@ trait PreuploadedPresentationsPubMsgHdlr {
val pages = new collection.mutable.HashMap[String, PageVO]()
pres.pages.foreach { p =>
val page = new PageVO(p.id, p.num, p.thumbUri, p.swfUri, p.txtUri, p.svgUri, p.current, p.xOffset, p.yOffset,
val page = new PageVO(p.id, p.num, p.thumbUri, p.txtUri, p.svgUri, p.current, p.xOffset, p.yOffset,
p.widthRatio, p.heightRatio)
pages += page.id -> page
}

View File

@ -0,0 +1,38 @@
package org.bigbluebutton.core.apps.presentationpod
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.running.LiveMeeting
trait PresentationHasInvalidMimeTypeErrorPubMsgHdlr {
this: PresentationPodHdlrs =>
def handle(
msg: PresentationHasInvalidMimeTypeErrorSysPubMsg, state: MeetingState2x,
liveMeeting: LiveMeeting, bus: MessageBus
): MeetingState2x = {
def broadcastEvent(msg: PresentationHasInvalidMimeTypeErrorSysPubMsg): Unit = {
val routing = Routing.addMsgToClientRouting(
MessageTypes.BROADCAST_TO_MEETING,
liveMeeting.props.meetingProp.intId, msg.header.userId
)
val envelope = BbbCoreEnvelope(PresentationHasInvalidMimeTypeErrorEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(
PresentationHasInvalidMimeTypeErrorEvtMsg.NAME,
liveMeeting.props.meetingProp.intId, msg.header.userId
)
val body = PresentationHasInvalidMimeTypeErrorEvtMsgBody(msg.body.podId, msg.body.meetingId,
msg.body.presentationName, msg.body.temporaryPresentationId,
msg.body.presentationId, msg.body.messageKey, msg.body.fileMime, msg.body.fileExtension)
val event = PresentationHasInvalidMimeTypeErrorEvtMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
bus.outGW.send(msgEvent)
}
broadcastEvent(msg)
state
}
}

View File

@ -25,7 +25,9 @@ class PresentationPodHdlrs(implicit val context: ActorContext)
with RemovePresentationPodPubMsgHdlr
with PresentationPageConvertedSysMsgHdlr
with PresentationPageConversionStartedSysMsgHdlr
with PresentationConversionEndedSysMsgHdlr {
with PresentationConversionEndedSysMsgHdlr
with PresentationUploadedFileTimeoutErrorPubMsgHdlr
with PresentationHasInvalidMimeTypeErrorPubMsgHdlr {
val log = Logging(context.system, getClass)
}

View File

@ -47,7 +47,6 @@ object PresentationPodsApp {
id = page.id,
num = page.num,
thumbUri = page.urls.getOrElse("thumb", ""),
swfUri = page.urls.getOrElse("swf", ""),
txtUri = page.urls.getOrElse("text", ""),
svgUri = page.urls.getOrElse("svg", ""),
current = page.current,
@ -80,7 +79,6 @@ object PresentationPodsApp {
id = page.id,
num = page.num,
thumbUri = page.urls.getOrElse("thumb", ""),
swfUri = page.urls.getOrElse("swf", ""),
txtUri = page.urls.getOrElse("text", ""),
svgUri = page.urls.getOrElse("svg", ""),
current = page.current,

View File

@ -0,0 +1,38 @@
package org.bigbluebutton.core.apps.presentationpod
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.running.LiveMeeting
trait PresentationUploadedFileTimeoutErrorPubMsgHdlr {
this: PresentationPodHdlrs =>
def handle(
msg: PresentationUploadedFileTimeoutErrorSysPubMsg, state: MeetingState2x,
liveMeeting: LiveMeeting, bus: MessageBus
): MeetingState2x = {
def broadcastEvent(msg: PresentationUploadedFileTimeoutErrorSysPubMsg): Unit = {
val routing = Routing.addMsgToClientRouting(
MessageTypes.BROADCAST_TO_MEETING,
liveMeeting.props.meetingProp.intId, msg.header.userId
)
val envelope = BbbCoreEnvelope(PresentationUploadedFileTooLargeErrorEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(
PresentationUploadedFileTooLargeErrorEvtMsg.NAME,
liveMeeting.props.meetingProp.intId, msg.header.userId
)
val body = PresentationUploadedFileTimeoutErrorEvtMsgBody(msg.body.podId, msg.body.meetingId,
msg.body.presentationName, msg.body.page, msg.body.messageKey, msg.body.temporaryPresentationId,
msg.body.presentationId, msg.body.maxNumberOfAttempts)
val event = PresentationUploadedFileTimeoutErrorEvtMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
bus.outGW.send(msgEvent)
}
broadcastEvent(msg)
state
}
}

View File

@ -1,6 +1,7 @@
package org.bigbluebutton.core.apps.presentationpod
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.api.{ CapturePresentationReqInternalMsg, CaptureSharedNotesReqInternalMsg }
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.domain.MeetingState2x
@ -42,6 +43,16 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
BbbCommonEnvCoreMsg(envelope, event)
}
def buildBroadcastPresAnnStatusMsg(presAnnStatusMsg: PresAnnStatusMsg, liveMeeting: LiveMeeting): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, liveMeeting.props.meetingProp.intId, "not-used")
val envelope = BbbCoreEnvelope(PresentationPageConvertedEventMsg.NAME, routing)
val header = BbbClientMsgHeader(PresAnnStatusEvtMsg.NAME, liveMeeting.props.meetingProp.intId, "not-used")
val body = PresAnnStatusEvtMsgBody(presId = presAnnStatusMsg.body.presId, pageNumber = presAnnStatusMsg.body.pageNumber, totalPages = presAnnStatusMsg.body.totalPages, status = presAnnStatusMsg.body.status, error = presAnnStatusMsg.body.error)
val event = PresAnnStatusEvtMsg(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)
@ -122,32 +133,27 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
}
}
def handle(m: ExportPresentationWithAnnotationReqMsg, state: MeetingState2x, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
def handle(m: CapturePresentationReqInternalMsg, state: MeetingState2x, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
val meetingId = liveMeeting.props.meetingProp.intId
val userId = m.header.userId
val userId = m.userId
val presentationPods: Vector[PresentationPod] = state.presentationPodManager.getAllPresentationPodsInMeeting()
val currentPres: Option[PresentationInPod] = presentationPods.flatMap(_.getCurrentPresentation()).headOption
if (liveMeeting.props.meetingProp.disabledFeatures.contains("importPresentationWithAnnotationsFromBreakoutRooms")) {
val reason = "Importing slides from breakout rooms disabled for this meeting."
PermissionCheck.ejectUserForFailedPermission(meetingId, userId, reason, bus.outGW, liveMeeting)
} else if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, userId)) {
val reason = "No permission to export presentation."
PermissionCheck.ejectUserForFailedPermission(meetingId, userId, reason, bus.outGW, liveMeeting)
log.error(s"Capturing breakout rooms slides disabled in meeting ${meetingId}.")
} else if (currentPres.isEmpty) {
log.error(s"No presentation set in meeting ${meetingId}")
} else {
val jobId: String = RandomStringGenerator.randomAlphanumericString(16);
val jobType = "PresentationWithAnnotationExportJob"
val allPages: Boolean = m.body.allPages
val allPages: Boolean = m.allPages
val pageCount = currentPres.get.pages.size
val presId: String = PresentationPodsApp.getAllPresentationPodsInMeeting(state).flatMap(_.getCurrentPresentation.map(_.id)).mkString
val presLocation = List("var", "bigbluebutton", meetingId, meetingId, presId).mkString(File.separator, File.separator, "");
val parentMeetingId: String = m.body.parentMeetingId
val parentMeetingId: String = m.parentMeetingId
val currentPage: PresentationPage = PresentationInPod.getCurrentPage(currentPres.get).get
val pagesRange: List[Int] = if (allPages) (1 to pageCount).toList else List(currentPage.num)
@ -183,7 +189,36 @@ trait PresentationWithAnnotationsMsgHdlr extends RightsManagementTrait {
log.info("Received NewPresAnnFileAvailableMsg meetingId={} presId={} fileUrl={}", liveMeeting.props.meetingProp.intId, m.body.presId, m.body.fileURI)
bus.outGW.send(buildBroadcastNewPresAnnFileAvailable(m, liveMeeting))
}
def handle(m: CaptureSharedNotesReqInternalMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
val meetingId = liveMeeting.props.meetingProp.intId
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, "not-used")
val envelope = BbbCoreEnvelope(PresentationPageConversionStartedEventMsg.NAME, routing)
val header = BbbClientMsgHeader(CaptureSharedNotesReqEvtMsg.NAME, meetingId, "not-used")
val body = CaptureSharedNotesReqEvtMsgBody(m.parentMeetingId, m.meetingName)
val event = CaptureSharedNotesReqEvtMsg(header, body)
bus.outGW.send(BbbCommonEnvCoreMsg(envelope, event))
}
def handle(m: PresAnnStatusMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
bus.outGW.send(buildBroadcastPresAnnStatusMsg(m, liveMeeting))
}
def handle(m: PadCapturePubMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
val userId: String = "system"
val jobId: String = s"${m.body.breakoutId}-notes" // Used as the temporaryPresentationId upon upload
val jobType = "PadCaptureJob"
val filename = m.body.filename
val presentationUploadToken: String = PresentationPodsApp.generateToken("DEFAULT_PRESENTATION_POD", userId)
bus.outGW.send(buildPresentationUploadTokenSysPubMsg(m.body.parentMeetingId, userId, presentationUploadToken, filename))
val exportJob = new ExportJob(jobId, jobType, filename, m.body.padId, "", true, List(), m.body.parentMeetingId, presentationUploadToken)
val job = buildStoreExportJobInRedisSysMsg(exportJob, liveMeeting)
bus.outGW.send(job)
}
}

View File

@ -1,26 +0,0 @@
package org.bigbluebutton.core.apps.users
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.models.{ EjectReasonCode, SystemUser }
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
import org.bigbluebutton.core2.message.senders.Sender
trait EjectDuplicateUserReqMsgHdlr {
this: UsersApp =>
val liveMeeting: LiveMeeting
val outGW: OutMsgRouter
def handleEjectDuplicateUserReqMsg(msg: EjectDuplicateUserReqMsg) {
val meetingId = liveMeeting.props.meetingProp.intId
val userId = msg.body.intUserId
val ejectedBy = SystemUser.ID
val reason = "user ejected because of duplicate external userid"
UsersApp.ejectUserFromMeeting(outGW, liveMeeting, userId, ejectedBy, reason, EjectReasonCode.DUPLICATE_USER, ban = false)
// send a system message to force disconnection
Sender.sendDisconnectClientSysMsg(meetingId, userId, ejectedBy, EjectReasonCode.DUPLICATE_USER, outGW)
}
}

View File

@ -3,7 +3,7 @@ package org.bigbluebutton.core.apps.users
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.models._
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
import org.bigbluebutton.core2.message.senders.MsgBuilder
import org.bigbluebutton.core2.message.senders.{ MsgBuilder, Sender }
trait RegisterUserReqMsgHdlr {
this: UsersApp =>
@ -22,12 +22,44 @@ trait RegisterUserReqMsgHdlr {
val event = UserRegisteredRespMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
def checkUserConcurrentAccesses(regUser: RegisteredUser): Unit = {
//Remove concurrent accesses over the limit
if (liveMeeting.props.usersProp.maxUserConcurrentAccesses > 0) {
val userConcurrentAccesses = RegisteredUsers.findAllWithExternUserId(regUser.externId, liveMeeting.registeredUsers)
.filter(u => !u.loggedOut)
.sortWith((u1, u2) => u1.registeredOn > u2.registeredOn) //Remove older first
val userAvailableSlots = liveMeeting.props.usersProp.maxUserConcurrentAccesses - userConcurrentAccesses.length
if (userAvailableSlots <= 0) {
(liveMeeting.props.usersProp.maxUserConcurrentAccesses to userConcurrentAccesses.length) foreach {
idxUserToRemove =>
{
val userToRemove = userConcurrentAccesses(idxUserToRemove - 1)
val meetingId = liveMeeting.props.meetingProp.intId
log.info(s"User ${regUser.id} with extId=${regUser.externId} has ${userConcurrentAccesses.length} concurrent accesses and limit is ${liveMeeting.props.usersProp.maxUserConcurrentAccesses}. " +
s"Ejecting the oldest=${userToRemove.id} in meetingId=${meetingId}")
val reason = "user ejected because of duplicate external userid"
UsersApp.ejectUserFromMeeting(outGW, liveMeeting, userToRemove.id, SystemUser.ID, reason, EjectReasonCode.DUPLICATE_USER, ban = false)
// send a system message to force disconnection
Sender.sendDisconnectClientSysMsg(meetingId, userToRemove.id, SystemUser.ID, EjectReasonCode.DUPLICATE_USER, outGW)
}
}
}
}
}
val guestStatus = msg.body.guestStatus
val regUser = RegisteredUsers.create(msg.body.intUserId, msg.body.extUserId,
msg.body.name, msg.body.role, msg.body.authToken,
msg.body.avatarURL, msg.body.guest, msg.body.authed, guestStatus, msg.body.excludeFromDashboard, false)
checkUserConcurrentAccesses(regUser)
RegisteredUsers.add(liveMeeting.registeredUsers, regUser)
log.info("Register user success. meetingId=" + liveMeeting.props.meetingProp.intId

View File

@ -3,8 +3,9 @@ package org.bigbluebutton.core.apps.users
import org.bigbluebutton.common2.msgs.UserJoinMeetingReqMsg
import org.bigbluebutton.core.apps.breakout.BreakoutHdlrHelpers
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.models.{ Users2x, VoiceUsers }
import org.bigbluebutton.core.models.{ RegisteredUser, RegisteredUsers, Users2x, VoiceUsers }
import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting, MeetingActor, OutMsgRouter }
import org.bigbluebutton.core2.message.senders.MsgBuilder
trait UserJoinMeetingReqMsgHdlr extends HandlerHelpers {
this: MeetingActor =>
@ -26,16 +27,52 @@ trait UserJoinMeetingReqMsgHdlr extends HandlerHelpers {
state
case None =>
// Check if maxParticipants has been reached
// User are able to reenter if he already joined previously with the same extId
val userHasJoinedAlready = RegisteredUsers.findWithUserId(msg.body.userId, liveMeeting.registeredUsers) match {
case Some(regUser: RegisteredUser) => RegisteredUsers.checkUserExtIdHasJoined(regUser.externId, liveMeeting.registeredUsers)
case None => false
}
val hasReachedMaxParticipants = liveMeeting.props.usersProp.maxUsers > 0 &&
RegisteredUsers.numUniqueJoinedUsers(liveMeeting.registeredUsers) >= liveMeeting.props.usersProp.maxUsers &&
userHasJoinedAlready == false
if (!hasReachedMaxParticipants) {
val newState = userJoinMeeting(outGW, msg.body.authToken, msg.body.clientType, liveMeeting, state)
if (liveMeeting.props.meetingProp.isBreakout) {
BreakoutHdlrHelpers.updateParentMeetingWithUsers(liveMeeting, eventBus)
}
// Warn previous users that someone connected with same Id
for {
regUser <- RegisteredUsers.getRegisteredUserWithToken(msg.body.authToken, msg.body.userId,
liveMeeting.registeredUsers)
} yield {
RegisteredUsers.findAllWithExternUserId(regUser.externId, liveMeeting.registeredUsers)
.filter(u => u.id != regUser.id)
.foreach { previousUser =>
val notifyUserEvent = MsgBuilder.buildNotifyUserInMeetingEvtMsg(
previousUser.id,
liveMeeting.props.meetingProp.intId,
"info",
"promote",
"app.mobileAppModal.userConnectedWithSameId",
"Notification to warn that user connect again from other browser/device",
Vector(regUser.name)
)
outGW.send(notifyUserEvent)
}
}
// fresh user joined (not due to reconnection). Clear (pop) the cached voice user
VoiceUsers.recoverVoiceUser(liveMeeting.voiceUsers, msg.body.userId)
newState
} else {
log.info("Ignoring user {} attempt to join, once the meeting {} has reached max participants: {}", msg.body.userId, msg.header.meetingId, liveMeeting.props.usersProp.maxUsers)
state
}
}
}
}

View File

@ -158,7 +158,6 @@ class UsersApp(
with SelectRandomViewerReqMsgHdlr
with AssignPresenterReqMsgHdlr
with ChangeUserPinStateReqMsgHdlr
with EjectDuplicateUserReqMsgHdlr
with EjectUserFromMeetingCmdMsgHdlr
with EjectUserFromMeetingSysMsgHdlr
with MuteUserCmdMsgHdlr {

View File

@ -5,7 +5,7 @@ import org.bigbluebutton.core.bus.InternalEventBus
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.models._
import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting, OutMsgRouter }
import org.bigbluebutton.core2.message.senders.{ MsgBuilder, Sender }
import org.bigbluebutton.core2.message.senders.{ MsgBuilder }
trait ValidateAuthTokenReqMsgHdlr extends HandlerHelpers {
this: UsersApp =>
@ -24,10 +24,16 @@ trait ValidateAuthTokenReqMsgHdlr extends HandlerHelpers {
liveMeeting.registeredUsers)
regUser match {
case Some(u) =>
// Check if maxParticipants has been reached
// User are able to reenter if he already joined previously with the same extId
val hasReachedMaxParticipants = liveMeeting.props.usersProp.maxUsers > 0 &&
RegisteredUsers.numUniqueJoinedUsers(liveMeeting.registeredUsers) >= liveMeeting.props.usersProp.maxUsers &&
RegisteredUsers.checkUserExtIdHasJoined(u.externId, liveMeeting.registeredUsers) == false
// Check if banned user is rejoining.
// Fail validation if ejected user is rejoining.
// ralam april 21, 2020
if (u.guestStatus == GuestStatus.ALLOW && !u.banned && !u.loggedOut) {
if (u.guestStatus == GuestStatus.ALLOW && !u.banned && !u.loggedOut && !hasReachedMaxParticipants) {
userValidated(u, state)
} else {
if (u.banned) {
@ -36,6 +42,9 @@ trait ValidateAuthTokenReqMsgHdlr extends HandlerHelpers {
} else if (u.loggedOut) {
failReason = "User had logged out"
failReasonCode = EjectReasonCode.USER_LOGGED_OUT
} else if (hasReachedMaxParticipants) {
failReason = "The maximum number of participants allowed for this meeting has been reached."
failReasonCode = EjectReasonCode.MAX_PARTICIPANTS
}
validateTokenFailed(
outGW,

View File

@ -28,11 +28,7 @@ trait ClearWhiteboardPubMsgHdlr extends RightsManagementTrait {
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
}
} else {
for {
fullClear <- clearWhiteboard(msg.body.whiteboardId, msg.header.userId, liveMeeting)
} yield {
broadcastEvent(msg, fullClear)
}
log.error("Ignoring message ClearWhiteboardPubMsg since this functions is not available in the new Whiteboard")
}
}
}

View File

@ -21,14 +21,24 @@ trait DeleteWhiteboardAnnotationsPubMsgHdlr extends RightsManagementTrait {
bus.outGW.send(msgEvent)
}
if (filterWhiteboardMessage(msg.body.whiteboardId, msg.header.userId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
val isUserAmongPresenters = !permissionFailed(
PermissionCheck.GUEST_LEVEL,
PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId
)
val isUserModerator = !permissionFailed(
PermissionCheck.MOD_LEVEL,
PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId
)
if (filterWhiteboardMessage(msg.body.whiteboardId, msg.header.userId, liveMeeting) && !isUserAmongPresenters) {
if (isNonEjectionGracePeriodOver(msg.body.whiteboardId, msg.header.userId, liveMeeting)) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to delete an annotation."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
}
} else {
val deletedAnnotations = deleteWhiteboardAnnotations(msg.body.whiteboardId, msg.header.userId, msg.body.annotationsIds, liveMeeting)
val deletedAnnotations = deleteWhiteboardAnnotations(msg.body.whiteboardId, msg.header.userId, msg.body.annotationsIds, liveMeeting, isUserAmongPresenters, isUserModerator)
if (!deletedAnnotations.isEmpty) {
broadcastEvent(msg, deletedAnnotations)
}

View File

@ -46,13 +46,18 @@ trait SendWhiteboardAnnotationsPubMsgHdlr extends RightsManagementTrait {
PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId
)
val isUserModerator = !permissionFailed(
PermissionCheck.MOD_LEVEL,
PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId
)
if (isUserOneOfPermited || isUserAmongPresenters) {
println("============= Printing Sanitized annotations ============")
for (annotation <- msg.body.annotations) {
printAnnotationInfo(annotation)
}
println("============= Printed Sanitized annotations ============")
val annotations = sendWhiteboardAnnotations(msg.body.whiteboardId, msg.header.userId, msg.body.annotations, liveMeeting)
val annotations = sendWhiteboardAnnotations(msg.body.whiteboardId, msg.header.userId, msg.body.annotations, liveMeeting, isUserAmongPresenters, isUserModerator)
broadcastEvent(msg, msg.body.whiteboardId, annotations, msg.body.html5InstanceId)
} else {
//val meetingId = liveMeeting.props.meetingProp.intId

View File

@ -11,7 +11,7 @@ case class Whiteboard(
multiUser: Array[String],
oldMultiUser: Array[String],
changedModeOn: Long,
annotationsMap: Map[String, Map[String, AnnotationVO]]
annotationsMap: Map[String, AnnotationVO]
)
class WhiteboardApp2x(implicit val context: ActorContext)
@ -24,9 +24,16 @@ class WhiteboardApp2x(implicit val context: ActorContext)
val log = Logging(context.system, getClass)
def sendWhiteboardAnnotations(whiteboardId: String, requesterId: String, annotations: Array[AnnotationVO], liveMeeting: LiveMeeting): Array[AnnotationVO] = {
def sendWhiteboardAnnotations(
whiteboardId: String,
requesterId: String,
annotations: Array[AnnotationVO],
liveMeeting: LiveMeeting,
isPresenter: Boolean,
isModerator: Boolean
): Array[AnnotationVO] = {
// println("Received whiteboard annotation. status=[" + status + "], annotationType=[" + annotationType + "]")
liveMeeting.wbModel.addAnnotations(whiteboardId, requesterId, annotations)
liveMeeting.wbModel.addAnnotations(whiteboardId, requesterId, annotations, isPresenter, isModerator)
}
def getWhiteboardAnnotations(whiteboardId: String, liveMeeting: LiveMeeting): Array[AnnotationVO] = {
@ -34,12 +41,15 @@ class WhiteboardApp2x(implicit val context: ActorContext)
liveMeeting.wbModel.getHistory(whiteboardId)
}
def clearWhiteboard(whiteboardId: String, requesterId: String, liveMeeting: LiveMeeting): Option[Boolean] = {
liveMeeting.wbModel.clearWhiteboard(whiteboardId, requesterId)
}
def deleteWhiteboardAnnotations(whiteboardId: String, requesterId: String, annotationsIds: Array[String], liveMeeting: LiveMeeting): Array[String] = {
liveMeeting.wbModel.deleteAnnotations(whiteboardId, requesterId, annotationsIds)
def deleteWhiteboardAnnotations(
whiteboardId: String,
requesterId: String,
annotationsIds: Array[String],
liveMeeting: LiveMeeting,
isPresenter: Boolean,
isModerator: Boolean
): Array[String] = {
liveMeeting.wbModel.deleteAnnotations(whiteboardId, requesterId, annotationsIds, isPresenter, isModerator)
}
def getWhiteboardAccess(whiteboardId: String, liveMeeting: LiveMeeting): Array[String] = {

View File

@ -14,7 +14,9 @@ case class BreakoutRoom2x(
users: Vector[BreakoutUser],
voiceUsers: Vector[BreakoutVoiceUser],
startedOn: Option[Long],
started: Boolean
started: Boolean,
captureNotes: Boolean,
captureSlides: Boolean,
) {
}

View File

@ -10,8 +10,8 @@ object BreakoutRooms {
def breakoutRoomsdurationInMinutes(status: BreakoutRooms, duration: Int) = status.breakoutRoomsdurationInMinutes = duration
def newBreakoutRoom(parentRoomId: String, id: String, externalMeetingId: String, name: String, sequence: Integer, freeJoin: Boolean,
voiceConfId: String, assignedUsers: Vector[String], breakoutRooms: BreakoutRooms): Option[BreakoutRoomVO] = {
val brvo = new BreakoutRoomVO(id, externalMeetingId, name, parentRoomId, sequence, freeJoin, voiceConfId, assignedUsers, Vector())
voiceConfId: String, assignedUsers: Vector[String], captureNotes: Boolean, captureSlides: Boolean, breakoutRooms: BreakoutRooms): Option[BreakoutRoomVO] = {
val brvo = new BreakoutRoomVO(id, externalMeetingId, name, parentRoomId, sequence, freeJoin, voiceConfId, assignedUsers, Vector(), captureNotes, captureSlides)
breakoutRooms.add(brvo)
Some(brvo)
}

View File

@ -112,7 +112,7 @@ object Polls {
shape = pollResultToWhiteboardShape(result)
annot <- send(result, shape)
} yield {
lm.wbModel.addAnnotations(annot.wbId, requesterId, Array[AnnotationVO](annot))
lm.wbModel.addAnnotations(annot.wbId, requesterId, Array[AnnotationVO](annot), false, false)
showPollResult(pollId, lm.polls)
(result, annot)
}
@ -238,7 +238,7 @@ object Polls {
private def handleRespondToTypedPoll(poll: SimplePollResultOutVO, requesterId: String, pollId: String, questionId: Int,
answer: String, lm: LiveMeeting): Option[SimplePollResultOutVO] = {
addQuestionResponse(poll.id, questionId, answer, lm.polls)
addQuestionResponse(poll.id, questionId, answer, requesterId, lm.polls)
for {
updatedPoll <- getSimplePollResult(poll.id, lm.polls)
} yield {
@ -355,6 +355,45 @@ object Polls {
pvo
}
def checkUserResponded(pollId: String, userId: String, polls: Polls): Boolean = {
polls.polls.get(pollId) match {
case Some(p) => {
if (p.getResponders().filter(p => p.userId == userId).length > 0) {
true
} else {
false
}
}
case None => false
}
}
def checkUserAddedQuestion(pollId: String, userId: String, polls: Polls): Boolean = {
polls.polls.get(pollId) match {
case Some(p) => {
if (p.getTypedPollResponders().filter(responderId => responderId == userId).length > 0) {
true
} else {
false
}
}
case None => false
}
}
def isResponsePollType(pollId: String, polls: Polls): Boolean = {
polls.polls.get(pollId) match {
case Some(p) => {
if (p.questions.filter(q => q.questionType == PollType.ResponsePollType).length > 0) {
true
} else {
false
}
}
case None => false
}
}
def showPollResult(pollId: String, polls: Polls) {
polls.get(pollId) foreach {
p =>
@ -375,11 +414,14 @@ object Polls {
}
}
def addQuestionResponse(pollId: String, questionID: Int, answer: String, polls: Polls) {
def addQuestionResponse(pollId: String, questionID: Int, answer: String, requesterId: String, polls: Polls) {
polls.polls.get(pollId) match {
case Some(p) => {
if (!p.getTypedPollResponders().contains(requesterId)) {
p.addTypedPollResponder(requesterId)
p.addQuestionResponse(questionID, answer)
}
}
case None =>
}
}
@ -545,6 +587,7 @@ class Poll(val id: String, val questions: Array[Question], val numRespondents: I
private var _showResult: Boolean = false
private var _numResponders: Int = 0
private var _responders = new ArrayBuffer[Responder]()
private var _respondersTypedPoll = new ArrayBuffer[String]()
def showingResult() { _showResult = true }
def showResult(): Boolean = { _showResult }
@ -561,6 +604,8 @@ class Poll(val id: String, val questions: Array[Question], val numRespondents: I
def addResponder(responder: Responder) { _responders += (responder) }
def getResponders(): ArrayBuffer[Responder] = { return _responders }
def addTypedPollResponder(responderId: String) { _respondersTypedPoll += (responderId) }
def getTypedPollResponders(): ArrayBuffer[String] = { return _respondersTypedPoll }
def hasResponses(): Boolean = {
questions.foreach(q => {

View File

@ -64,6 +64,14 @@ object RegisteredUsers {
} yield user
}
def checkUserExtIdHasJoined(externId: String, regUsers: RegisteredUsers): Boolean = {
regUsers.toVector.filter(_.externId == externId).filter(_.joined).length > 0
}
def numUniqueJoinedUsers(regUsers: RegisteredUsers): Int = {
regUsers.toVector.filter(_.joined).map(_.externId).distinct.length
}
def add(users: RegisteredUsers, user: RegisteredUser): Vector[RegisteredUser] = {
findWithExternUserId(user.externId, users) match {

View File

@ -407,4 +407,5 @@ object EjectReasonCode {
val USER_INACTIVITY = "user_inactivity_eject_reason"
val BANNED_USER_REJOINING = "banned_user_rejoining_reason"
val USER_LOGGED_OUT = "user_logged_out_reason"
val MAX_PARTICIPANTS = "max_participants_reason"
}

View File

@ -65,8 +65,6 @@ class ReceivedJsonMsgHandlerActor(
// Route via meeting manager as there is a race condition if we send directly to meeting
// because the meeting actor might not have been created yet.
route[RegisterUserReqMsg](meetingManagerChannel, envelope, jsonNode)
case EjectDuplicateUserReqMsg.NAME =>
route[EjectDuplicateUserReqMsg](meetingManagerChannel, envelope, jsonNode)
case UserJoinMeetingReqMsg.NAME =>
routeGenericMsg[UserJoinMeetingReqMsg](envelope, jsonNode)
case UserJoinMeetingAfterReconnectReqMsg.NAME =>
@ -175,6 +173,10 @@ class ReceivedJsonMsgHandlerActor(
routePadMsg[PadPatchSysMsg](envelope, jsonNode)
case PadUpdatePubMsg.NAME =>
routeGenericMsg[PadUpdatePubMsg](envelope, jsonNode)
case PadCapturePubMsg.NAME =>
routePadMsg[PadCapturePubMsg](envelope, jsonNode)
case PadPinnedReqMsg.NAME =>
routeGenericMsg[PadPinnedReqMsg](envelope, jsonNode)
// Voice
case RecordingStartedVoiceConfEvtMsg.NAME =>
@ -286,6 +288,10 @@ class ReceivedJsonMsgHandlerActor(
routeGenericMsg[PreuploadedPresentationsSysPubMsg](envelope, jsonNode)
case PresentationUploadedFileTooLargeErrorSysPubMsg.NAME =>
routeGenericMsg[PresentationUploadedFileTooLargeErrorSysPubMsg](envelope, jsonNode)
case PresentationHasInvalidMimeTypeErrorSysPubMsg.NAME =>
routeGenericMsg[PresentationHasInvalidMimeTypeErrorSysPubMsg](envelope, jsonNode)
case PresentationUploadedFileTimeoutErrorSysPubMsg.NAME =>
routeGenericMsg[PresentationUploadedFileTimeoutErrorSysPubMsg](envelope, jsonNode)
case PresentationConversionUpdateSysPubMsg.NAME =>
routeGenericMsg[PresentationConversionUpdateSysPubMsg](envelope, jsonNode)
case PresentationPageCountErrorSysPubMsg.NAME =>
@ -308,10 +314,10 @@ class ReceivedJsonMsgHandlerActor(
routeGenericMsg[AssignPresenterReqMsg](envelope, jsonNode)
case MakePresentationWithAnnotationDownloadReqMsg.NAME =>
routeGenericMsg[MakePresentationWithAnnotationDownloadReqMsg](envelope, jsonNode)
case ExportPresentationWithAnnotationReqMsg.NAME =>
routeGenericMsg[ExportPresentationWithAnnotationReqMsg](envelope, jsonNode)
case NewPresAnnFileAvailableMsg.NAME =>
routeGenericMsg[NewPresAnnFileAvailableMsg](envelope, jsonNode)
case PresAnnStatusMsg.NAME =>
routeGenericMsg[PresAnnStatusMsg](envelope, jsonNode)
// Presentation Pods
case CreateNewPresentationPodPubMsg.NAME =>

View File

@ -226,7 +226,7 @@ trait HandlerHelpers extends SystemConfiguration {
model <- state.breakout
} yield {
model.rooms.values.foreach { room =>
eventBus.publish(BigBlueButtonEvent(room.id, EndBreakoutRoomInternalMsg(liveMeeting.props.breakoutProps.parentId, room.id, reason)))
eventBus.publish(BigBlueButtonEvent(room.id, EndBreakoutRoomInternalMsg(liveMeeting.props.meetingProp.intId, room.id, reason)))
}
}

View File

@ -250,7 +250,6 @@ class MeetingActor(
// Handling RegisterUserReqMsg as it is forwarded from BBBActor and
// its type is not BbbCommonEnvCoreMsg
case m: RegisterUserReqMsg => usersApp.handleRegisterUserReqMsg(m)
case m: EjectDuplicateUserReqMsg => usersApp.handleEjectDuplicateUserReqMsg(m)
case m: GetAllMeetingsReqMsg => handleGetAllMeetingsReqMsg(m)
case m: GetRunningMeetingStateReqMsg => handleGetRunningMeetingStateReqMsg(m)
case m: ValidateConnAuthTokenSysMsg => handleValidateConnAuthTokenSysMsg(m)
@ -283,7 +282,8 @@ class MeetingActor(
case msg: SendMessageToBreakoutRoomInternalMsg => state = handleSendMessageToBreakoutRoomInternalMsg(msg, state, liveMeeting, msgBus)
case msg: SendBreakoutTimeRemainingInternalMsg =>
handleSendBreakoutTimeRemainingInternalMsg(msg)
case msg: CapturePresentationReqInternalMsg => presentationPodsApp.handle(msg, state, liveMeeting, msgBus)
case msg: CaptureSharedNotesReqInternalMsg => presentationPodsApp.handle(msg, liveMeeting, msgBus)
case msg: SendRecordingTimerInternalMsg =>
state = usersApp.handleSendRecordingTimerInternalMsg(msg, state)
@ -492,6 +492,7 @@ class MeetingActor(
case m: PadContentSysMsg => padsApp2x.handle(m, liveMeeting, msgBus)
case m: PadPatchSysMsg => padsApp2x.handle(m, liveMeeting, msgBus)
case m: PadUpdatePubMsg => padsApp2x.handle(m, liveMeeting, msgBus)
case m: PadPinnedReqMsg => padsApp2x.handle(m, liveMeeting, msgBus)
// Lock Settings
case m: ChangeLockSettingsInMeetingCmdMsg =>
@ -505,8 +506,9 @@ class MeetingActor(
case m: PreuploadedPresentationsSysPubMsg => presentationApp2x.handle(m, liveMeeting, msgBus)
case m: AssignPresenterReqMsg => state = handlePresenterChange(m, state)
case m: MakePresentationWithAnnotationDownloadReqMsg => presentationPodsApp.handle(m, state, liveMeeting, msgBus)
case m: ExportPresentationWithAnnotationReqMsg => presentationPodsApp.handle(m, state, liveMeeting, msgBus)
case m: NewPresAnnFileAvailableMsg => presentationPodsApp.handle(m, liveMeeting, msgBus)
case m: PresAnnStatusMsg => presentationPodsApp.handle(m, liveMeeting, msgBus)
case m: PadCapturePubMsg => presentationPodsApp.handle(m, liveMeeting, msgBus)
// Presentation Pods
case m: CreateNewPresentationPodPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
@ -521,6 +523,8 @@ class MeetingActor(
case m: SetPresentationDownloadablePubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
case m: PresentationConversionUpdateSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
case m: PresentationUploadedFileTooLargeErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
case m: PresentationHasInvalidMimeTypeErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
case m: PresentationUploadedFileTimeoutErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
case m: PresentationPageGeneratedSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
case m: PresentationPageCountErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
case m: PresentationUploadTokenReqMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)

View File

@ -117,7 +117,6 @@ class AnalyticsActor(val includeChat: Boolean) extends Actor with ActorLogging {
// case m: StoreAnnotationsInRedisSysMsg => logMessage(msg)
// case m: StoreExportJobInRedisSysMsg => logMessage(msg)
case m: MakePresentationWithAnnotationDownloadReqMsg => logMessage(msg)
case m: ExportPresentationWithAnnotationReqMsg => logMessage(msg)
case m: NewPresAnnFileAvailableMsg => logMessage(msg)
case m: PresentationPageConversionStartedSysMsg => logMessage(msg)
case m: PresentationConversionEndedSysMsg => logMessage(msg)
@ -201,6 +200,7 @@ class AnalyticsActor(val includeChat: Boolean) extends Actor with ActorLogging {
case m: PadUpdatedEvtMsg => logMessage(msg)
case m: PadUpdatePubMsg => logMessage(msg)
case m: PadUpdateCmdMsg => logMessage(msg)
case m: PadCapturePubMsg => logMessage(msg)
case _ => // ignore message
}

View File

@ -47,7 +47,7 @@ trait AppsTestFixtures {
val meetingLayout = ""
val metadata: collection.immutable.Map[String, String] = Map("foo" -> "bar", "bar" -> "baz", "baz" -> "foo")
val breakoutProps = BreakoutProps(parentId = parentMeetingId, sequence = sequence, freeJoin = false, breakoutRooms = Vector())
val breakoutProps = BreakoutProps(parentId = parentMeetingId, sequence = sequence, freeJoin = false, captureNotes = false, captureSlides = false, breakoutRooms = Vector())
val meetingProp = MeetingProp(name = meetingName, extId = externalMeetingId, intId = meetingId,
meetingCameraCap = meetingCameraCap,

View File

@ -27,7 +27,9 @@ case class BreakoutProps(
freeJoin: Boolean,
breakoutRooms: Vector[String],
record: Boolean,
privateChatEnabled: Boolean
privateChatEnabled: Boolean,
captureNotes: Boolean,
captureSlides: Boolean,
)
case class PasswordProp(moderatorPass: String, viewerPass: String, learningDashboardAccessToken: String)
@ -40,6 +42,7 @@ case class VoiceProp(telVoice: String, voiceConf: String, dialNumber: String, mu
case class UsersProp(
maxUsers: Int,
maxUserConcurrentAccesses:Int,
webcamsOnlyForModerator: Boolean,
userCameraCap: Int,
guestPolicy: String,

View File

@ -3,7 +3,7 @@ package org.bigbluebutton.common2.domain
case class PresentationVO(id: String, temporaryPresentationId: String, name: String, current: Boolean = false,
pages: Vector[PageVO], downloadable: Boolean, removable: Boolean)
case class PageVO(id: String, num: Int, thumbUri: String = "", swfUri: String,
case class PageVO(id: String, num: Int, thumbUri: String = "",
txtUri: String, svgUri: String, current: Boolean = false, xOffset: Double = 0,
yOffset: Double = 0, widthRatio: Double = 100D, heightRatio: Double = 100D)

View File

@ -13,7 +13,7 @@ case class BreakoutRoomJoinURLEvtMsgBody(parentId: String, breakoutId: String, e
object BreakoutRoomsListEvtMsg { val NAME = "BreakoutRoomsListEvtMsg" }
case class BreakoutRoomsListEvtMsg(header: BbbClientMsgHeader, body: BreakoutRoomsListEvtMsgBody) extends BbbCoreMsg
case class BreakoutRoomsListEvtMsgBody(meetingId: String, rooms: Vector[BreakoutRoomInfo], roomsReady: Boolean)
case class BreakoutRoomInfo(name: String, externalId: String, breakoutId: String, sequence: Int, shortName: String, isDefaultName: Boolean, freeJoin: Boolean, html5JoinUrls: Map[String, String])
case class BreakoutRoomInfo(name: String, externalId: String, breakoutId: String, sequence: Int, shortName: String, isDefaultName: Boolean, freeJoin: Boolean, html5JoinUrls: Map[String, String], captureNotes: Boolean, captureSlides: Boolean)
object BreakoutRoomsListMsg { val NAME = "BreakoutRoomsListMsg" }
case class BreakoutRoomsListMsg(header: BbbClientMsgHeader, body: BreakoutRoomsListMsgBody) extends StandardMsg
@ -58,7 +58,9 @@ case class BreakoutRoomDetail(
sourcePresentationId: String,
sourcePresentationSlide: Int,
record: Boolean,
privateChatEnabled: Boolean
privateChatEnabled: Boolean,
captureNotes: Boolean,
captureSlides: Boolean,
)
/**
@ -66,7 +68,7 @@ case class BreakoutRoomDetail(
*/
object CreateBreakoutRoomsCmdMsg { val NAME = "CreateBreakoutRoomsCmdMsg" }
case class CreateBreakoutRoomsCmdMsg(header: BbbClientMsgHeader, body: CreateBreakoutRoomsCmdMsgBody) extends StandardMsg
case class CreateBreakoutRoomsCmdMsgBody(meetingId: String, durationInMinutes: Int, record: Boolean, rooms: Vector[BreakoutRoomMsgBody])
case class CreateBreakoutRoomsCmdMsgBody(meetingId: String, durationInMinutes: Int, record: Boolean, captureNotes: Boolean, captureSlides: Boolean, rooms: Vector[BreakoutRoomMsgBody])
case class BreakoutRoomMsgBody(name: String, sequence: Int, shortName: String, isDefaultName: Boolean, freeJoin: Boolean, users: Vector[String])
// Sent by user to request ending all the breakout rooms
@ -123,5 +125,5 @@ case class BreakoutUserVO(id: String, name: String)
case class BreakoutRoomVO(id: String, externalId: String, name: String, parentId: String,
sequence: Int, freeJoin: Boolean, voiceConf: String,
assignedUsers: Vector[String], users: Vector[BreakoutUserVO])
assignedUsers: Vector[String], users: Vector[BreakoutUserVO], captureNotes: Boolean, captureSlides: Boolean)

View File

@ -113,3 +113,18 @@ case class PadUpdatePubMsgBody(externalId: String, text: String)
object PadUpdateCmdMsg { val NAME = "PadUpdateCmdMsg" }
case class PadUpdateCmdMsg(header: BbbCoreHeaderWithMeetingId, body: PadUpdateCmdMsgBody) extends BbbCoreMsg
case class PadUpdateCmdMsgBody(groupId: String, name: String, text: String)
// pads -> apps
object PadCapturePubMsg { val NAME = "PadCapturePubMsg" }
case class PadCapturePubMsg(header: BbbCoreHeaderWithMeetingId, body: PadCapturePubMsgBody) extends PadStandardMsg
case class PadCapturePubMsgBody(parentMeetingId: String, breakoutId: String, padId: String, filename: String)
// client -> apps
object PadPinnedReqMsg { val NAME = "PadPinnedReqMsg" }
case class PadPinnedReqMsg(header: BbbClientMsgHeader, body: PadPinnedReqMsgBody) extends StandardMsg
case class PadPinnedReqMsgBody(externalId: String, pinned: Boolean)
// apps -> client
object PadPinnedEvtMsg { val NAME = "PadPinnedEvtMsg" }
case class PadPinnedEvtMsg(header: BbbClientMsgHeader, body: PadPinnedEvtMsgBody) extends BbbCoreMsg
case class PadPinnedEvtMsgBody(externalId: String, pinned: Boolean)

View File

@ -14,17 +14,13 @@ object MakePresentationWithAnnotationDownloadReqMsg { val NAME = "MakePresentati
case class MakePresentationWithAnnotationDownloadReqMsg(header: BbbClientMsgHeader, body: MakePresentationWithAnnotationDownloadReqMsgBody) extends StandardMsg
case class MakePresentationWithAnnotationDownloadReqMsgBody(presId: String, allPages: Boolean, pages: List[Int])
object ExportPresentationWithAnnotationReqMsg { val NAME = "ExportPresentationWithAnnotationReqMsg" }
case class ExportPresentationWithAnnotationReqMsg(header: BbbClientMsgHeader, body: ExportPresentationWithAnnotationReqMsgBody) extends StandardMsg
case class ExportPresentationWithAnnotationReqMsgBody(parentMeetingId: String, allPages: Boolean)
object NewPresAnnFileAvailableMsg { val NAME = "NewPresAnnFileAvailableMsg" }
case class NewPresAnnFileAvailableMsg(header: BbbClientMsgHeader, body: NewPresAnnFileAvailableMsgBody) extends StandardMsg
case class NewPresAnnFileAvailableMsgBody(fileURI: String, presId: String)
object NewPresAnnFileAvailableEvtMsg { val NAME = "NewPresAnnFileAvailableEvtMsg" }
case class NewPresAnnFileAvailableEvtMsg(header: BbbClientMsgHeader, body: NewPresAnnFileAvailableEvtMsgBody) extends BbbCoreMsg
case class NewPresAnnFileAvailableEvtMsgBody(fileURI: String, presId: String)
object PresAnnStatusMsg { val NAME = "PresAnnStatusMsg" }
case class PresAnnStatusMsg(header: BbbClientMsgHeader, body: PresAnnStatusMsgBody) extends StandardMsg
case class PresAnnStatusMsgBody(presId: String, pageNumber: Int, totalPages: Int, status: String, error: Boolean);
// ------------ bbb-common-web to akka-apps ------------
@ -40,4 +36,17 @@ case class PresenterUnassignedEvtMsgBody(intId: String, name: String, assignedBy
object NewPresentationEvtMsg { val NAME = "NewPresentationEvtMsg" }
case class NewPresentationEvtMsg(header: BbbClientMsgHeader, body: NewPresentationEvtMsgBody) extends BbbCoreMsg
case class NewPresentationEvtMsgBody(presentation: PresentationVO)
object NewPresAnnFileAvailableEvtMsg { val NAME = "NewPresAnnFileAvailableEvtMsg" }
case class NewPresAnnFileAvailableEvtMsg(header: BbbClientMsgHeader, body: NewPresAnnFileAvailableEvtMsgBody) extends BbbCoreMsg
case class NewPresAnnFileAvailableEvtMsgBody(fileURI: String, presId: String)
object PresAnnStatusEvtMsg { val NAME = "PresAnnStatusEvtMsg" }
case class PresAnnStatusEvtMsg(header: BbbClientMsgHeader, body: PresAnnStatusEvtMsgBody) extends BbbCoreMsg
case class PresAnnStatusEvtMsgBody(presId: String, pageNumber: Int, totalPages: Int, status: String, error: Boolean);
object CaptureSharedNotesReqEvtMsg { val NAME = "CaptureSharedNotesReqEvtMsg" }
case class CaptureSharedNotesReqEvtMsg(header: BbbClientMsgHeader, body: CaptureSharedNotesReqEvtMsgBody) extends BbbCoreMsg
case class CaptureSharedNotesReqEvtMsgBody(parentMeetingId: String, meetingName: String)
// ------------ akka-apps to client ------------

View File

@ -166,6 +166,39 @@ case class PresentationUploadedFileTooLargeErrorSysPubMsgBody(
maxFileSize: Int
)
object PresentationHasInvalidMimeTypeErrorSysPubMsg { val NAME = "PresentationHasInvalidMimeTypeErrorSysPubMsg" }
case class PresentationHasInvalidMimeTypeErrorSysPubMsg(
header: BbbClientMsgHeader,
body: PresentationHasInvalidMimeTypeErrorSysPubMsgBody
) extends StandardMsg
case class PresentationHasInvalidMimeTypeErrorSysPubMsgBody(
podId: String,
meetingId: String,
presentationName: String,
temporaryPresentationId: String,
presentationId: String,
messageKey: String,
fileMime: String,
fileExtension: String,
)
object PresentationUploadedFileTimeoutErrorSysPubMsg { val NAME = "PresentationUploadedFileTimeoutErrorSysPubMsg" }
case class PresentationUploadedFileTimeoutErrorSysPubMsg(
header: BbbClientMsgHeader,
body: PresentationUploadedFileTimeoutErrorSysPubMsgBody
) extends StandardMsg
case class PresentationUploadedFileTimeoutErrorSysPubMsgBody(
podId: String,
meetingId: String,
presentationName: String,
page: Int,
messageKey: String,
temporaryPresentationId: String,
presentationId: String,
maxNumberOfAttempts: Int,
)
// ------------ bbb-common-web to akka-apps ------------
// ------------ akka-apps to client ------------
@ -221,6 +254,20 @@ object PresentationUploadedFileTooLargeErrorEvtMsg { val NAME = "PresentationUpl
case class PresentationUploadedFileTooLargeErrorEvtMsg(header: BbbClientMsgHeader, body: PresentationUploadedFileTooLargeErrorEvtMsgBody) extends BbbCoreMsg
case class PresentationUploadedFileTooLargeErrorEvtMsgBody(podId: String, messageKey: String, code: String, presentationName: String, presentationToken: String, fileSize: Int, maxFileSize: Int)
object PresentationHasInvalidMimeTypeErrorEvtMsg { val NAME = "PresentationHasInvalidMimeTypeErrorEvtMsg" }
case class PresentationHasInvalidMimeTypeErrorEvtMsg(header: BbbClientMsgHeader, body: PresentationHasInvalidMimeTypeErrorEvtMsgBody) extends BbbCoreMsg
case class PresentationHasInvalidMimeTypeErrorEvtMsgBody(podId: String, meetingId: String, presentationName: String,
temporaryPresentationId: String, presentationId: String,
messageKey: String, fileMime: String, fileExtension: String,
)
object PresentationUploadedFileTimeoutErrorEvtMsg { val NAME = "PresentationUploadedFileTimeoutErrorEvtMsg" }
case class PresentationUploadedFileTimeoutErrorEvtMsg(header: BbbClientMsgHeader, body: PresentationUploadedFileTimeoutErrorEvtMsgBody) extends BbbCoreMsg
case class PresentationUploadedFileTimeoutErrorEvtMsgBody(podId: String, meetingId: String, presentationName: String,
page: Int, messageKey: String,
temporaryPresentationId: String, presentationId: String,
maxNumberOfAttempts: Int)
object PresentationConversionRequestReceivedEventMsg { val NAME = "PresentationConversionRequestReceivedEventMsg" }
case class PresentationConversionRequestReceivedEventMsg(
header: BbbClientMsgHeader,

View File

@ -1,13 +1,5 @@
package org.bigbluebutton.common2.msgs
object EjectDuplicateUserReqMsg { val NAME = "EjectDuplicateUserReqMsg" }
case class EjectDuplicateUserReqMsg(
header: BbbCoreHeaderWithMeetingId,
body: EjectDuplicateUserReqMsgBody
) extends BbbCoreMsg
case class EjectDuplicateUserReqMsgBody(meetingId: String, intUserId: String, name: String,
extUserId: String)
object RegisterUserReqMsg { val NAME = "RegisterUserReqMsg" }
case class RegisterUserReqMsg(
header: BbbCoreHeaderWithMeetingId,

View File

@ -49,7 +49,7 @@ trait TestFixtures {
meetingCameraCap = meetingCameraCap,
maxPinnedCameras = maxPinnedCameras,
isBreakout = isBreakout.booleanValue())
val breakoutProps = BreakoutProps(parentId = parentMeetingId, sequence = sequence, freeJoin = false, breakoutRooms = Vector())
val breakoutProps = BreakoutProps(parentId = parentMeetingId, sequence = sequence, freeJoin = false, captureNotes = false, captureSlides = false, breakoutRooms = Vector())
val durationProps = DurationProps(duration = durationInMinutes, createdTime = createTime, createdDate = createDate,
meetingExpireIfNoUserJoinedInMinutes = meetingExpireIfNoUserJoinedInMinutes, meetingExpireWhenLastUserLeftInMinutes = meetingExpireWhenLastUserLeftInMinutes,

View File

@ -31,7 +31,7 @@ object Dependencies {
val lang = "3.12.0"
val io = "2.11.0"
val pool = "2.11.1"
val text = "1.9"
val text = "1.10.0"
// BigBlueButton
val bbbCommons = "0.0.21-SNAPSHOT"

View File

@ -76,6 +76,8 @@ public class ApiParams {
public static final String UPLOAD_EXTERNAL_DESCRIPTION = "uploadExternalDescription";
public static final String UPLOAD_EXTERNAL_URL = "uploadExternalUrl";
public static final String BREAKOUT_ROOMS_CAPTURE_SLIDES = "breakoutRoomsCaptureSlides";
public static final String BREAKOUT_ROOMS_CAPTURE_NOTES = "breakoutRoomsCaptureNotes";
public static final String BREAKOUT_ROOMS_ENABLED = "breakoutRoomsEnabled";
public static final String BREAKOUT_ROOMS_RECORD = "breakoutRoomsRecord";
public static final String BREAKOUT_ROOMS_PRIVATE_CHAT_ENABLED = "breakoutRoomsPrivateChatEnabled";

View File

@ -19,9 +19,7 @@
package org.bigbluebutton.api;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.AbstractMap;
import java.util.Collection;
import java.util.Collections;
@ -43,14 +41,12 @@ import java.util.concurrent.LinkedBlockingQueue;
import com.google.gson.JsonObject;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.utils.URIBuilder;
import org.bigbluebutton.api.HTML5LoadBalancingService;
import org.bigbluebutton.api.domain.GuestPolicy;
import org.bigbluebutton.api.domain.Meeting;
import org.bigbluebutton.api.domain.Recording;
import org.bigbluebutton.api.domain.RegisteredUser;
import org.bigbluebutton.api.domain.User;
import org.bigbluebutton.api.domain.UserSession;
import org.bigbluebutton.api.domain.MeetingLayout;
import org.bigbluebutton.api.messaging.MessageListener;
import org.bigbluebutton.api.messaging.converters.messages.DestroyMeetingMessage;
import org.bigbluebutton.api.messaging.converters.messages.EndMeetingMessage;
@ -60,10 +56,9 @@ import org.bigbluebutton.api.messaging.converters.messages.DeletedRecordingMessa
import org.bigbluebutton.api.messaging.messages.*;
import org.bigbluebutton.api2.IBbbWebApiGWApp;
import org.bigbluebutton.api2.domain.UploadedTrack;
import org.bigbluebutton.common2.msgs.MeetingCreatedEvtMsg;
import org.bigbluebutton.common2.redis.RedisStorageService;
import org.bigbluebutton.presentation.PresentationUrlDownloadService;
import org.bigbluebutton.presentation.imp.SwfSlidesGenerationProgressNotifier;
import org.bigbluebutton.presentation.imp.SlidesGenerationProgressNotifier;
import org.bigbluebutton.web.services.WaitingGuestCleanupTimerTask;
import org.bigbluebutton.web.services.UserCleanupTimerTask;
import org.bigbluebutton.web.services.EnteredUserCleanupTimerTask;
@ -77,7 +72,6 @@ import com.google.gson.Gson;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.InputStream;
import org.springframework.data.domain.*;
@ -104,8 +98,7 @@ public class MeetingService implements MessageListener {
private StunTurnService stunTurnService;
private RedisStorageService storeService;
private CallbackUrlService callbackUrlService;
private HTML5LoadBalancingService html5LoadBalancingService;
private SwfSlidesGenerationProgressNotifier notifier;
private SlidesGenerationProgressNotifier notifier;
private long usersTimeout;
private long waitingGuestUsersTimeout;
@ -377,6 +370,8 @@ public class MeetingService implements MessageListener {
breakoutMetadata.put("meetingId", m.getExternalId());
breakoutMetadata.put("sequence", m.getSequence().toString());
breakoutMetadata.put("freeJoin", m.isFreeJoin().toString());
breakoutMetadata.put("captureSlides", m.isCaptureSlides().toString());
breakoutMetadata.put("captureNotes", m.isCaptureNotes().toString());
breakoutMetadata.put("parentMeetingId", m.getParentMeetingId());
storeService.recordBreakoutInfo(m.getInternalId(), breakoutMetadata);
}
@ -388,6 +383,8 @@ public class MeetingService implements MessageListener {
if (m.isBreakout()) {
logData.put("sequence", m.getSequence());
logData.put("freeJoin", m.isFreeJoin());
logData.put("captureSlides", m.isCaptureSlides());
logData.put("captureNotes", m.isCaptureNotes());
logData.put("parentMeetingId", m.getParentMeetingId());
}
logData.put("name", m.getName());
@ -415,7 +412,7 @@ public class MeetingService implements MessageListener {
m.getLearningDashboardAccessToken(), m.getCreateTime(),
formatPrettyDate(m.getCreateTime()), m.isBreakout(), m.getSequence(), m.isFreeJoin(), m.getMetadata(),
m.getGuestPolicy(), m.getAuthenticatedGuest(), m.getMeetingLayout(), m.getWelcomeMessageTemplate(), m.getWelcomeMessage(), m.getModeratorOnlyMessage(),
m.getDialNumber(), m.getMaxUsers(),
m.getDialNumber(), m.getMaxUsers(), m.getMaxUserConcurrentAccesses(),
m.getMeetingExpireIfNoUserJoinedInMinutes(), m.getMeetingExpireWhenLastUserLeftInMinutes(),
m.getUserInactivityInspectTimerInMinutes(), m.getUserInactivityThresholdInMinutes(),
m.getUserActivitySignResponseDelayInMinutes(), m.getEndWhenNoModerator(), m.getEndWhenNoModeratorDelayInMinutes(),
@ -434,33 +431,6 @@ public class MeetingService implements MessageListener {
}
private void processRegisterUser(RegisterUser message) {
Meeting m = getMeeting(message.meetingID);
if (m != null) {
User prevUser = m.getUserWithExternalId(message.externUserID);
if (prevUser != null) {
Map<String, Object> logData = new HashMap<>();
logData.put("meetingId", m.getInternalId());
logData.put("externalMeetingId", m.getExternalId());
logData.put("name", m.getName());
logData.put("extUserId", prevUser.getExternalUserId());
logData.put("intUserId", prevUser.getInternalUserId());
logData.put("username", prevUser.getFullname());
logData.put("logCode", "duplicate_user_with_external_userid");
logData.put("description", "Duplicate user with external userid.");
Gson gson = new Gson();
String logStr = gson.toJson(logData);
log.info(" --analytics-- data={}", logStr);
if (!m.allowDuplicateExtUserid) {
gw.ejectDuplicateUser(message.meetingID,
prevUser.getInternalUserId(), prevUser.getFullname(),
prevUser.getExternalUserId());
}
}
}
gw.registerUser(message.meetingID,
message.internalUserId, message.fullname, message.role,
message.externUserID, message.authToken, message.avatarURL, message.guest,
@ -661,6 +631,8 @@ public class MeetingService implements MessageListener {
params.put(ApiParams.IS_BREAKOUT, "true");
params.put(ApiParams.SEQUENCE, message.sequence.toString());
params.put(ApiParams.FREE_JOIN, message.freeJoin.toString());
params.put(ApiParams.BREAKOUT_ROOMS_CAPTURE_SLIDES, message.captureSlides.toString());
params.put(ApiParams.BREAKOUT_ROOMS_CAPTURE_NOTES, message.captureNotes.toString());
params.put(ApiParams.ATTENDEE_PW, message.viewerPassword);
params.put(ApiParams.MODERATOR_PW, message.moderatorPassword);
params.put(ApiParams.DIAL_NUMBER, message.dialNumber);
@ -951,9 +923,8 @@ public class MeetingService implements MessageListener {
message.name, message.role, message.avatarURL, message.guest, message.guestStatus,
message.clientType);
if(m.getMaxUsers() > 0 && m.getUsers().size() >= m.getMaxUsers()) {
if(m.getMaxUsers() > 0 && m.countUniqueExtIds() >= m.getMaxUsers()) {
m.removeEnteredUser(user.getInternalUserId());
gw.ejectDuplicateUser(message.meetingId, user.getInternalUserId(), user.getFullname(), user.getExternalUserId());
return;
}
@ -1345,7 +1316,7 @@ public class MeetingService implements MessageListener {
enteredUsersTimeout = value;
}
public void setSwfSlidesGenerationProgressNotifier(SwfSlidesGenerationProgressNotifier notifier) {
public void setSlidesGenerationProgressNotifier(SlidesGenerationProgressNotifier notifier) {
this.notifier = notifier;
}

View File

@ -66,6 +66,8 @@ public class ParamsProcessorUtil {
private String apiVersion;
private boolean serviceEnabled = false;
private String securitySalt;
private String supportedChecksumAlgorithms;
private String checksumHash;
private int defaultMaxUsers = 20;
private String defaultWelcomeMessage;
private String defaultWelcomeMessageFooter;
@ -106,6 +108,8 @@ public class ParamsProcessorUtil {
private boolean defaultBreakoutRoomsEnabled = true;
private boolean defaultBreakoutRoomsRecord;
private boolean defaultBreakoutRoomsCaptureSlides = false;
private boolean defaultBreakoutRoomsCaptureNotes = false;
private boolean defaultbreakoutRoomsPrivateChatEnabled;
private boolean defaultLockSettingsDisableCam;
@ -128,6 +132,8 @@ public class ParamsProcessorUtil {
private Integer userInactivityThresholdInMinutes = 30;
private Integer userActivitySignResponseDelayInMinutes = 5;
private Boolean defaultAllowDuplicateExtUserid = true;
private Integer maxUserConcurrentAccesses = 0;
private Boolean defaultEndWhenNoModerator = false;
private Integer defaultEndWhenNoModeratorDelayInMinutes = 1;
private Integer defaultHtml5InstanceId = 1;
@ -275,7 +281,19 @@ public class ParamsProcessorUtil {
breakoutRoomsPrivateChatEnabled = Boolean.parseBoolean(breakoutRoomsPrivateChatEnabledParam);
}
return new BreakoutRoomsParams(breakoutRoomsRecord, breakoutRoomsPrivateChatEnabled);
Boolean breakoutRoomsCaptureSlides = defaultBreakoutRoomsCaptureSlides;
String breakoutRoomsCaptureParam = params.get(ApiParams.BREAKOUT_ROOMS_CAPTURE_SLIDES);
if (!StringUtils.isEmpty(breakoutRoomsCaptureParam)) {
breakoutRoomsCaptureSlides = Boolean.parseBoolean(breakoutRoomsCaptureParam);
}
Boolean breakoutRoomsCaptureNotes = defaultBreakoutRoomsCaptureNotes;
String breakoutRoomsCaptureNotesParam = params.get(ApiParams.BREAKOUT_ROOMS_CAPTURE_NOTES);
if (!StringUtils.isEmpty(breakoutRoomsCaptureNotesParam)) {
breakoutRoomsCaptureNotes = Boolean.parseBoolean(breakoutRoomsCaptureNotesParam);
}
return new BreakoutRoomsParams(breakoutRoomsRecord, breakoutRoomsPrivateChatEnabled, breakoutRoomsCaptureNotes, breakoutRoomsCaptureSlides);
}
private LockSettingsParams processLockSettingsParams(Map<String, String> params) {
@ -680,6 +698,11 @@ public class ParamsProcessorUtil {
int html5InstanceId = processHtml5InstanceId(params.get(ApiParams.HTML5_INSTANCE_ID));
if(defaultAllowDuplicateExtUserid == false) {
log.warn("[DEPRECATION] use `maxUserConcurrentAccesses=1` instead of `allowDuplicateExtUserid=false`");
maxUserConcurrentAccesses = 1;
}
// Create the meeting with all passed in parameters.
Meeting meeting = new Meeting.Builder(externalMeetingId,
internalMeetingId, createTime).withName(meetingName)
@ -706,7 +729,7 @@ public class ParamsProcessorUtil {
.withMeetingLayout(meetingLayout)
.withBreakoutRoomsParams(breakoutParams)
.withLockSettingsParams(lockSettingsParams)
.withAllowDuplicateExtUserid(defaultAllowDuplicateExtUserid)
.withMaxUserConcurrentAccesses(maxUserConcurrentAccesses)
.withHTML5InstanceId(html5InstanceId)
.withLearningDashboardCleanupDelayInMinutes(learningDashboardCleanupMins)
.withLearningDashboardAccessToken(learningDashboardAccessToken)
@ -742,6 +765,8 @@ public class ParamsProcessorUtil {
if (isBreakout) {
meeting.setSequence(Integer.parseInt(params.get(ApiParams.SEQUENCE)));
meeting.setFreeJoin(Boolean.parseBoolean(params.get(ApiParams.FREE_JOIN)));
meeting.setCaptureSlides(Boolean.parseBoolean(params.get(ApiParams.BREAKOUT_ROOMS_CAPTURE_SLIDES)));
meeting.setCaptureNotes(Boolean.parseBoolean(params.get(ApiParams.BREAKOUT_ROOMS_CAPTURE_NOTES)));
meeting.setParentMeetingId(parentMeetingId);
}
@ -978,11 +1003,39 @@ public class ParamsProcessorUtil {
log.info("CHECKSUM={} length={}", checksum, checksum.length());
String data = apiCall + queryString + securitySalt;
String cs = DigestUtils.sha1Hex(data);
if (checksum.length() == 64) {
int checksumLength = checksum.length();
String cs = null;
switch(checksumLength) {
case 40:
if(supportedChecksumAlgorithms.contains("sha1")) {
cs = DigestUtils.sha1Hex(data);
log.info("SHA1 {}", cs);
}
break;
case 64:
if(supportedChecksumAlgorithms.contains("sha256")) {
cs = DigestUtils.sha256Hex(data);
log.info("SHA256 {}", cs);
}
break;
case 96:
if(supportedChecksumAlgorithms.contains("sha384")) {
cs = DigestUtils.sha384Hex(data);
log.info("SHA384 {}", cs);
}
break;
case 128:
if(supportedChecksumAlgorithms.contains("sha512")) {
cs = DigestUtils.sha512Hex(data);
log.info("SHA512 {}", cs);
}
break;
default:
log.info("No algorithm could be found that matches the provided checksum length");
}
if (cs == null || !cs.equals(checksum)) {
log.info("query string after checksum removed: [{}]", queryString);
log.info("checksumError: query string checksum failed. our: [{}], client: [{}]", cs, checksum);
@ -1068,6 +1121,10 @@ public class ParamsProcessorUtil {
this.securitySalt = securitySalt;
}
public void setSupportedChecksumAlgorithms(String supportedChecksumAlgorithms) { this.supportedChecksumAlgorithms = supportedChecksumAlgorithms; }
public void setChecksumHash(String checksumHash) { this.checksumHash = checksumHash; }
public void setDefaultMaxUsers(int defaultMaxUsers) {
this.defaultMaxUsers = defaultMaxUsers;
}
@ -1367,6 +1424,10 @@ public class ParamsProcessorUtil {
this.defaultAllowDuplicateExtUserid = allow;
}
public void setMaxUserConcurrentAccesses(Integer maxUserConcurrentAccesses) {
this.maxUserConcurrentAccesses = maxUserConcurrentAccesses;
}
public void setEndWhenNoModerator(Boolean val) {
this.defaultEndWhenNoModerator = val;
}

View File

@ -3,9 +3,13 @@ package org.bigbluebutton.api.domain;
public class BreakoutRoomsParams {
public final Boolean record;
public final Boolean privateChatEnabled;
public final Boolean captureNotes;
public final Boolean captureSlides;
public BreakoutRoomsParams(Boolean record, Boolean privateChatEnabled) {
public BreakoutRoomsParams(Boolean record, Boolean privateChatEnabled, Boolean captureNotes, Boolean captureSlides) {
this.record = record;
this.privateChatEnabled = privateChatEnabled;
this.captureNotes = captureNotes;
this.captureSlides = captureSlides;
}
}

View File

@ -42,6 +42,8 @@ public class Meeting {
private String parentMeetingId = "bbb-none"; // Initialize so we don't send null in the json message.
private Integer sequence = 0;
private Boolean freeJoin = false;
private Boolean captureSlides = false;
private Boolean captureNotes = false;
private Integer duration = 0;
private long createdTime = 0;
private long startTime = 0;
@ -109,7 +111,7 @@ public class Meeting {
public final BreakoutRoomsParams breakoutRoomsParams;
public final LockSettingsParams lockSettingsParams;
public final Boolean allowDuplicateExtUserid;
public final Integer maxUserConcurrentAccesses;
private String meetingEndedCallbackURL = "";
@ -163,7 +165,7 @@ public class Meeting {
allowRequestsWithoutSession = builder.allowRequestsWithoutSession;
breakoutRoomsParams = builder.breakoutRoomsParams;
lockSettingsParams = builder.lockSettingsParams;
allowDuplicateExtUserid = builder.allowDuplicateExtUserid;
maxUserConcurrentAccesses = builder.maxUserConcurrentAccesses;
endWhenNoModerator = builder.endWhenNoModerator;
endWhenNoModeratorDelayInMinutes = builder.endWhenNoModeratorDelayInMinutes;
html5InstanceId = builder.html5InstanceId;
@ -197,6 +199,28 @@ public class Meeting {
return users;
}
public Integer countUniqueExtIds() {
List<String> uniqueExtIds = new ArrayList<String>();
for (User user : users.values()) {
if(!uniqueExtIds.contains(user.getExternalUserId())) {
uniqueExtIds.add(user.getExternalUserId());
}
}
return uniqueExtIds.size();
}
public List<String> getUsersWithExtId(String externalUserId) {
List<String> usersWithExtId = new ArrayList<String>();
for (User user : users.values()) {
if(user.getExternalUserId().equals(externalUserId)) {
usersWithExtId.add(user.getInternalUserId());
}
}
return usersWithExtId;
}
public void guestIsWaiting(String userId) {
RegisteredUser ruser = registeredUsers.get(userId);
if (ruser != null) {
@ -288,6 +312,22 @@ public class Meeting {
this.freeJoin = freeJoin;
}
public Boolean isCaptureSlides() {
return captureSlides;
}
public void setCaptureSlides(Boolean capture) {
this.captureSlides = captureSlides;
}
public Boolean isCaptureNotes() {
return captureNotes;
}
public void setCaptureNotes(Boolean capture) {
this.captureNotes = captureNotes;
}
public Integer getDuration() {
return duration;
}
@ -504,6 +544,10 @@ public class Meeting {
return maxUsers;
}
public Integer getMaxUserConcurrentAccesses() {
return maxUserConcurrentAccesses;
}
public int getLogoutTimer() {
return logoutTimer;
}
@ -633,21 +677,19 @@ public class Meeting {
return this.users.get(id);
}
public User getUserWithExternalId(String externalUserId) {
for (Map.Entry<String, User> entry : users.entrySet()) {
User u = entry.getValue();
if (u.getExternalUserId().equals(externalUserId)) {
return u;
}
}
return null;
}
public int getNumUsers(){
return this.users.size();
}
public int getNumUsersOnline(){
int countUsersOnline = 0;
for (User user : this.users.values()) {
if(!user.hasLeft()) countUsersOnline++;
}
return countUsersOnline;
}
public int getNumModerators() {
int sum = 0;
for (Map.Entry<String, User> entry : users.entrySet()) {
@ -843,7 +885,8 @@ public class Meeting {
private String meetingLayout;
private BreakoutRoomsParams breakoutRoomsParams;
private LockSettingsParams lockSettingsParams;
private Boolean allowDuplicateExtUserid;
private Integer maxUserConcurrentAccesses;
private Boolean endWhenNoModerator;
private Integer endWhenNoModeratorDelayInMinutes;
private int html5InstanceId;
@ -1035,8 +1078,8 @@ public class Meeting {
return this;
}
public Builder withAllowDuplicateExtUserid(Boolean allowDuplicateExtUserid) {
this.allowDuplicateExtUserid = allowDuplicateExtUserid;
public Builder withMaxUserConcurrentAccesses(Integer maxUserConcurrentAccesses) {
this.maxUserConcurrentAccesses = maxUserConcurrentAccesses;
return this;
}

View File

@ -19,6 +19,8 @@ public class CreateBreakoutRoom implements IMessage {
public final Integer sourcePresentationSlide;
public final Boolean record;
public final Boolean privateChatEnabled;
public final Boolean captureNotes; // Upload shared notes to main room after breakout room end
public final Boolean captureSlides; // Upload annotated breakout slides to main room after breakout room end
public CreateBreakoutRoom(String meetingId,
String parentMeetingId,
@ -35,7 +37,9 @@ public class CreateBreakoutRoom implements IMessage {
String sourcePresentationId,
Integer sourcePresentationSlide,
Boolean record,
Boolean privateChatEnabled) {
Boolean privateChatEnabled,
Boolean captureNotes,
Boolean captureSlides) {
this.meetingId = meetingId;
this.parentMeetingId = parentMeetingId;
this.name = name;
@ -52,5 +56,7 @@ public class CreateBreakoutRoom implements IMessage {
this.sourcePresentationSlide = sourcePresentationSlide;
this.record = record;
this.privateChatEnabled = privateChatEnabled;
this.captureNotes = captureNotes;
this.captureSlides = captureSlides;
}
}

View File

@ -1,22 +0,0 @@
package org.bigbluebutton.api.model.constraint;
import org.bigbluebutton.api.model.validator.MaxParticipantsValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Constraint(validatedBy = { MaxParticipantsValidator.class })
@Target(FIELD)
@Retention(RUNTIME)
public @interface MaxParticipantsConstraint {
String key() default "maxParticipantsReached";
String message() default "The maximum number of participants for the meeting has been reached";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -0,0 +1,39 @@
package org.bigbluebutton.api.model.request;
import org.bigbluebutton.api.model.constraint.UserSessionConstraint;
import java.util.Map;
public class GetJoinUrl implements Request<GetJoinUrl.Params> {
public enum Params implements RequestParameters {
SESSION_TOKEN("sessionToken");
private final String value;
Params(String value) { this.value = value; }
public String getValue() { return value; }
}
@UserSessionConstraint
private String sessionToken;
public String getSessionToken() {
return sessionToken;
}
public void setSessionToken(String sessionToken) {
this.sessionToken = sessionToken;
}
@Override
public void populateFromParamsMap(Map<String, String[]> params) {
if(params.containsKey(GetJoinUrl.Params.SESSION_TOKEN.getValue())) setSessionToken(params.get(GetJoinUrl.Params.SESSION_TOKEN.getValue())[0]);
}
@Override
public void convertParamsFromString() {
}
}

View File

@ -1,6 +1,5 @@
package org.bigbluebutton.api.model.request;
import org.bigbluebutton.api.model.constraint.MaxParticipantsConstraint;
import org.bigbluebutton.api.model.constraint.MeetingEndedConstraint;
import org.bigbluebutton.api.model.constraint.MeetingExistsConstraint;
import org.bigbluebutton.api.model.constraint.UserSessionConstraint;

View File

@ -20,6 +20,7 @@ public class GetChecksumValidator implements ConstraintValidator<GetChecksumCons
@Override
public boolean isValid(GetChecksum checksum, ConstraintValidatorContext context) {
String securitySalt = ServiceUtils.getValidationService().getSecuritySalt();
String supportedChecksumAlgorithms = ServiceUtils.getValidationService().getSupportedChecksumAlgorithms();
if (securitySalt.isEmpty()) {
log.warn("Security is disabled in this service. Make sure this is intentional.");
@ -41,13 +42,38 @@ public class GetChecksumValidator implements ConstraintValidator<GetChecksumCons
}
String data = checksum.getApiCall() + queryStringWithoutChecksum + securitySalt;
String createdCheckSum = DigestUtils.sha1Hex(data);
if (providedChecksum.length() == 64) {
log.debug("providedChecksum.length() == 64");
int checksumLength = providedChecksum.length();
String createdCheckSum = null;
switch(checksumLength) {
case 40:
if(supportedChecksumAlgorithms.contains("sha1")) {
createdCheckSum = DigestUtils.sha1Hex(data);
log.info("SHA1 {}", createdCheckSum);
}
break;
case 64:
if(supportedChecksumAlgorithms.contains("sha256")) {
createdCheckSum = DigestUtils.sha256Hex(data);
log.info("SHA256 {}", createdCheckSum);
}
break;
case 96:
if(supportedChecksumAlgorithms.contains("sha384")) {
createdCheckSum = DigestUtils.sha384Hex(data);
log.info("SHA384 {}", createdCheckSum);
}
break;
case 128:
if(supportedChecksumAlgorithms.contains("sha512")) {
createdCheckSum = DigestUtils.sha512Hex(data);
log.info("SHA512 {}", createdCheckSum);
}
break;
default:
log.info("No algorithm could be found that matches the provided checksum length");
}
if (createdCheckSum == null || !createdCheckSum.equals(providedChecksum)) {
log.info("checksumError: query string checksum failed. our: [{}], client: [{}]", createdCheckSum, providedChecksum);

View File

@ -1,50 +0,0 @@
package org.bigbluebutton.api.model.validator;
import org.bigbluebutton.api.MeetingService;
import org.bigbluebutton.api.domain.Meeting;
import org.bigbluebutton.api.domain.UserSession;
import org.bigbluebutton.api.model.constraint.MaxParticipantsConstraint;
import org.bigbluebutton.api.service.ServiceUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class MaxParticipantsValidator implements ConstraintValidator<MaxParticipantsConstraint, String> {
@Override
public void initialize(MaxParticipantsConstraint constraintAnnotation) {}
@Override
public boolean isValid(String sessionToken, ConstraintValidatorContext constraintValidatorContext) {
if(sessionToken == null) {
return false;
}
MeetingService meetingService = ServiceUtils.getMeetingService();
UserSession userSession = meetingService.getUserSessionWithAuthToken(sessionToken);
if(userSession == null) {
return false;
}
Meeting meeting = meetingService.getMeeting(userSession.meetingID);
if(meeting == null) {
return false;
}
int maxParticipants = meeting.getMaxUsers();
boolean enabled = maxParticipants > 0;
boolean rejoin = meeting.getUserById(userSession.internalUserId) != null;
boolean reenter = meeting.getEnteredUserById(userSession.internalUserId) != null;
int joinedUsers = meeting.getUsers().size();
boolean reachedMax = joinedUsers >= maxParticipants;
if(enabled && !rejoin && !reenter && reachedMax) {
return false;
}
return true;
}
}

View File

@ -40,6 +40,7 @@ public class ValidationService {
STUNS("stuns", RequestType.GET),
SIGN_OUT("signOut", RequestType.GET),
LEARNING_DASHBOARD("learningDashboard", RequestType.GET),
GET_JOIN_URL("getJoinUrl", RequestType.GET),
INSERT_DOCUMENT("insertDocument", RequestType.GET);
private final String name;
@ -55,6 +56,7 @@ public class ValidationService {
}
private String securitySalt;
private String supportedChecksumAlgorithms;
private Boolean allowRequestsWithoutSession;
private ValidatorFactory validatorFactory;
@ -136,6 +138,9 @@ public class ValidationService {
case LEARNING_DASHBOARD:
request = new LearningDashboard();
break;
case GET_JOIN_URL:
request = new GetJoinUrl();
break;
}
}
@ -266,6 +271,9 @@ public class ValidationService {
public void setSecuritySalt(String securitySalt) { this.securitySalt = securitySalt; }
public String getSecuritySalt() { return securitySalt; }
public void setSupportedChecksumAlgorithms(String supportedChecksumAlgorithms) { this.supportedChecksumAlgorithms = supportedChecksumAlgorithms; }
public String getSupportedChecksumAlgorithms() { return supportedChecksumAlgorithms; }
public void setAllowRequestsWithoutSession(Boolean allowRequestsWithoutSession) {
this.allowRequestsWithoutSession = allowRequestsWithoutSession;
}

View File

@ -1,7 +1,6 @@
package org.bigbluebutton.api2;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.bigbluebutton.api.domain.BreakoutRoomsParams;
@ -26,7 +25,7 @@ public interface IBbbWebApiGWApp {
String moderatorPass, String viewerPass, String learningDashboardAccessToken, Long createTime,
String createDate, Boolean isBreakout, Integer sequence, Boolean freejoin, Map<String, String> metadata,
String guestPolicy, Boolean authenticatedGuest, String meetingLayout, String welcomeMsgTemplate, String welcomeMsg, String modOnlyMessage,
String dialNumber, Integer maxUsers,
String dialNumber, Integer maxUsers, Integer maxUserConcurrentAccesses,
Integer meetingExpireIfNoUserJoinedInMinutes,
Integer meetingExpireWhenLastUserLeftInMinutes,
Integer userInactivityInspectTimerInMinutes,
@ -50,8 +49,6 @@ public interface IBbbWebApiGWApp {
void registerUser(String meetingID, String internalUserId, String fullname, String role,
String externUserID, String authToken, String avatarURL,
Boolean guest, Boolean authed, String guestStatus, Boolean excludeFromDashboard);
void ejectDuplicateUser(String meetingID, String internalUserId, String fullname,
String externUserID);
void guestWaitingLeft(String meetingID, String internalUserId);
void destroyMeeting(DestroyMeetingMessage msg);

View File

@ -38,6 +38,7 @@ public class ConversionMessageConstants {
public static final String GENERATED_SVGIMAGES_KEY = "GENERATED_SVGIMAGES";
public static final String CONVERSION_STARTED_KEY = "CONVERSION_STARTED_KEY";
public static final String CONVERSION_COMPLETED_KEY = "CONVERSION_COMPLETED";
public static final String CONVERSION_TIMEOUT_KEY = "CONVERSION_TIMEOUT";
private ConversionMessageConstants() {
throw new IllegalStateException("ConversionMessageConstants is a utility class. Instanciation is forbidden.");

View File

@ -94,7 +94,6 @@ public class ConversionUpdateMessage {
Map<String, String> page = new HashMap<String, String>();
page.put("num", Integer.toString(i));
page.put("thumb", basePresUrl + "/thumbnail/" + i);
page.put("swf", basePresUrl + "/slide/" + i);
page.put("text", basePresUrl + "/textfiles/" + i);
pages.add(page);

View File

@ -21,4 +21,5 @@ package org.bigbluebutton.presentation;
public interface DocumentConversionService {
void processDocument(UploadedPresentation pres);
void sendDocConversionFailedOnMimeType(UploadedPresentation pres, String fileMime, String fileExtension);
}

View File

@ -19,21 +19,25 @@
package org.bigbluebutton.presentation;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import org.bigbluebutton.api2.IBbbWebApiGWApp;
import org.bigbluebutton.presentation.imp.*;
import org.bigbluebutton.presentation.messages.DocConversionRequestReceived;
import org.bigbluebutton.presentation.messages.DocInvalidMimeType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import static org.bigbluebutton.presentation.Util.deleteDirectoryFromFileHandlingErrors;
public class DocumentConversionServiceImp implements DocumentConversionService {
private static Logger log = LoggerFactory.getLogger(DocumentConversionServiceImp.class);
private IBbbWebApiGWApp gw;
private OfficeToPdfConversionService officeToPdfConversionService;
private SwfSlidesGenerationProgressNotifier notifier;
private SlidesGenerationProgressNotifier notifier;
private PresentationFileProcessor presentationFileProcessor;
@ -93,6 +97,9 @@ public class DocumentConversionServiceImp implements DocumentConversionService {
}
} else {
File presentationFile = pres.getUploadedFile();
deleteDirectoryFromFileHandlingErrors(presentationFile);
Map<String, Object> logData = new HashMap<String, Object>();
logData = new HashMap<String, Object>();
logData.put("podId", pres.getPodId());
@ -124,6 +131,11 @@ public class DocumentConversionServiceImp implements DocumentConversionService {
}
}
public void sendDocConversionFailedOnMimeType(UploadedPresentation pres, String fileMime,
String fileExtension) {
notifier.sendInvalidMimeTypeMessage(pres, fileMime, fileExtension);
}
private void sendDocConversionRequestReceived(UploadedPresentation pres) {
if (! pres.isConversionStarted()) {
Map<String, Object> logData = new HashMap<String, Object>();
@ -166,7 +178,7 @@ public class DocumentConversionServiceImp implements DocumentConversionService {
officeToPdfConversionService = s;
}
public void setSwfSlidesGenerationProgressNotifier(SwfSlidesGenerationProgressNotifier notifier) {
public void setSlidesGenerationProgressNotifier(SlidesGenerationProgressNotifier notifier) {
this.notifier = notifier;
}

View File

@ -39,5 +39,6 @@ public final class FileTypeConstants {
public static final String JPG = "jpg";
public static final String JPEG = "jpeg";
public static final String PNG = "png";
public static final String SVG = "svg";
private FileTypeConstants() {} // Prevent instantiation
}

View File

@ -1,94 +0,0 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.presentation;
import java.io.File;
import java.io.IOException;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ImageToSwfSlide {
private static Logger log = LoggerFactory.getLogger(ImageToSwfSlide.class);
private UploadedPresentation pres;
private int page;
private PageConverter imageToSwfConverter;
private String BLANK_SLIDE;
private boolean done = false;
private File slide;
public ImageToSwfSlide(UploadedPresentation pres, int page) {
this.pres = pres;
this.page = page;
}
public ImageToSwfSlide createSlide() {
File presentationFile = pres.getUploadedFile();
slide = new File(presentationFile.getParent() + File.separatorChar + "slide-" + page + ".swf");
log.debug("Creating slide {}", slide.getAbsolutePath());
imageToSwfConverter.convert(presentationFile, slide, page, pres);
// If all fails, generate a blank slide.
if (!slide.exists()) {
log.warn("Creating blank slide for {}", slide.getAbsolutePath());
generateBlankSlide();
}
done = true;
return this;
}
public void generateBlankSlide() {
if (BLANK_SLIDE != null) {
copyBlankSlide(slide);
} else {
log.error("Blank slide has not been set");
}
}
private void copyBlankSlide(File slide) {
try {
FileUtils.copyFile(new File(BLANK_SLIDE), slide);
} catch (IOException e) {
log.error("IOException while copying blank slide.", e);
}
}
public void setPageConverter(PageConverter converter) {
this.imageToSwfConverter = converter;
}
public void setBlankSlide(String blankSlide) {
this.BLANK_SLIDE = blankSlide;
}
public boolean isDone() {
return done;
}
public int getPageNumber() {
return page;
}
}

View File

@ -0,0 +1,67 @@
package org.bigbluebutton.presentation;
import java.util.*;
import static org.bigbluebutton.presentation.FileTypeConstants.*;
public class MimeTypeUtils {
private static final String XLS = "application/vnd.ms-excel";
private static final String XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
private static final String DOC = "application/msword";
private static final String DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
private static final String PPT = "application/vnd.ms-powerpoint";
private static final String PPTX = "application/vnd.openxmlformats-officedocument.presentationml.presentation";
private static final String ODT = "application/vnd.oasis.opendocument.text";
private static final String RTF = "application/rtf";
private static final String TXT = "text/plain";
private static final String ODS = "application/vnd.oasis.opendocument.spreadsheet";
private static final String ODP = "application/vnd.oasis.opendocument.presentation";
private static final String PDF = "application/pdf";
private static final String JPEG = "image/jpeg";
private static final String PNG = "image/png";
private static final String SVG = "image/svg+xml";
private static final HashMap<String,String> EXTENSIONS_MIME = new HashMap<String,String>(16) {
{
// Add all the supported files
put(FileTypeConstants.XLS, XLS);
put(FileTypeConstants.XLSX, XLSX);
put(FileTypeConstants.DOC, DOC);
put(FileTypeConstants.DOCX, DOCX);
put(FileTypeConstants.PPT, PPT);
put(FileTypeConstants.PPTX, PPTX);
put(FileTypeConstants.ODT, ODT);
put(FileTypeConstants.RTF, RTF);
put(FileTypeConstants.TXT, TXT);
put(FileTypeConstants.ODS, ODS);
put(FileTypeConstants.ODP, ODP);
put(FileTypeConstants.PDF, PDF);
put(FileTypeConstants.JPG, JPEG);
put(FileTypeConstants.JPEG, JPEG);
put(FileTypeConstants.PNG, PNG);
put(FileTypeConstants.SVG, SVG);
}
};
public Boolean extensionMatchMimeType(String mimeType, String finalExtension) {
if(EXTENSIONS_MIME.containsKey(finalExtension.toLowerCase()) &&
EXTENSIONS_MIME.get(finalExtension.toLowerCase()).equalsIgnoreCase(mimeType)) {
return true;
} else if(EXTENSIONS_MIME.containsKey(finalExtension.toLowerCase() + 'x') &&
EXTENSIONS_MIME.get(finalExtension.toLowerCase() + 'x').equalsIgnoreCase(mimeType)) {
//Exception for MS Office files named with old extension but using internally the new mime type
//e.g. a file named with extension `ppt` but has the content of a `pptx`
return true;
}
return false;
}
public List<String> getValidMimeTypes() {
List<String> validMimeTypes = Arrays.asList(XLS, XLSX,
DOC, DOCX, PPT, PPTX, ODT, RTF, TXT, ODS, ODP,
PDF, JPEG, PNG, SVG
);
return validMimeTypes;
}
}

View File

@ -1,122 +0,0 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.presentation;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
public class PdfToSwfSlide {
private static Logger log = LoggerFactory.getLogger(PdfToSwfSlide.class);
private UploadedPresentation pres;
private int page;
private PageConverter pdfToSwfConverter;
private String BLANK_SLIDE;
private int MAX_SWF_FILE_SIZE;
private volatile boolean done = false;
private File slide;
private File pageFile;
public PdfToSwfSlide(UploadedPresentation pres, int page, File pageFile) {
this.pres = pres;
this.page = page;
this.pageFile = pageFile;
}
public PdfToSwfSlide createSlide() {
slide = new File(pageFile.getParent() + File.separatorChar + "slide-" + page + ".swf");
pdfToSwfConverter.convert(pageFile, slide, page, pres);
// If all fails, generate a blank slide.
if (!slide.exists()) {
log.warn("Failed to create slide. Creating blank slide for " + slide.getAbsolutePath());
generateBlankSlide();
}
done = true;
return this;
}
public void generateBlankSlide() {
if (BLANK_SLIDE != null) {
Map<String, Object> logData = new HashMap<>();
logData.put("meetingId", pres.getMeetingId());
logData.put("presId", pres.getId());
logData.put("filename", pres.getName());
logData.put("page", page);
Gson gson = new Gson();
String logStr = gson.toJson(logData);
log.warn("Creating blank slide: data={}", logStr);
copyBlankSlide(slide);
} else {
Map<String, Object> logData = new HashMap<>();
logData.put("meetingId", pres.getMeetingId());
logData.put("presId", pres.getId());
logData.put("filename", pres.getName());
logData.put("page", page);
Gson gson = new Gson();
String logStr = gson.toJson(logData);
log.warn("Failed to create blank slide: data={}", logStr);
}
}
private void copyBlankSlide(File slide) {
try {
FileUtils.copyFile(new File(BLANK_SLIDE), slide);
} catch (IOException e) {
log.error("IOException while copying blank slide.", e);
}
}
public void setPageConverter(PageConverter converter) {
this.pdfToSwfConverter = converter;
}
public void setBlankSlide(String blankSlide) {
this.BLANK_SLIDE = blankSlide;
}
public void setMaxSwfFileSize(int size) {
this.MAX_SWF_FILE_SIZE = size;
}
public boolean isDone() {
return done;
}
public int getPageNumber() {
return page;
}
}

View File

@ -19,8 +19,15 @@
package org.bigbluebutton.presentation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.bigbluebutton.presentation.FileTypeConstants.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Collections;
@ -28,6 +35,9 @@ import java.util.Collections;
@SuppressWarnings("serial")
public final class SupportedFileTypes {
private static Logger log = LoggerFactory.getLogger(SupportedFileTypes.class);
private static MimeTypeUtils mimeTypeUtils = new MimeTypeUtils();
private static final List<String> SUPPORTED_FILE_LIST = Collections.unmodifiableList(new ArrayList<String>(15) {
{
// Add all the supported files
@ -76,4 +86,56 @@ public final class SupportedFileTypes {
public static boolean isImageFile(String fileExtension) {
return IMAGE_FILE_LIST.contains(fileExtension.toLowerCase());
}
/*
* It was tested native java methods to detect mimetypes, such as:
* - URLConnection.guessContentTypeFromStream(InputStream is);
* - Files.probeContentType(Path path);
* - FileNameMap fileNameMap.getContentTypeFor(String file.getName());
* - MimetypesFileTypeMap fileTypeMap.getContentType(File file);
* But none of them was as successful as the linux based command
*/
public static String detectMimeType(File pres) {
String mimeType = "";
if (pres != null && pres.isFile()){
try {
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command("bash", "-c", "file -b --mime-type " + pres.getAbsolutePath());
Process process = processBuilder.start();
StringBuilder output = new StringBuilder();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
output.append(line + "\n");
}
int exitVal = process.waitFor();
if (exitVal == 0) {
mimeType = output.toString().trim();
} else {
log.error("Error while executing command {} for file {}, error: {}",
process.toString(), pres.getAbsolutePath(), process.getErrorStream());
}
} catch (IOException e) {
log.error("Could not read file [{}]", pres.getAbsolutePath(), e.getMessage());
} catch (InterruptedException e) {
log.error("Flow interrupted for file [{}]", pres.getAbsolutePath(), e.getMessage());
}
}
return mimeType;
}
public static Boolean isPresentationMimeTypeValid(File pres, String fileExtension) {
String mimeType = detectMimeType(pres);
if(mimeType == null || mimeType == "") return false;
if(!mimeTypeUtils.getValidMimeTypes().contains(mimeType)) return false;
if(!mimeTypeUtils.extensionMatchMimeType(mimeType, fileExtension)) {
log.error("File with extension [{}] doesn't match with mimeType [{}].", fileExtension, mimeType);
return false;
}
return true;
}
}

View File

@ -21,7 +21,10 @@
* @version $Id: $
*/
package org.bigbluebutton.presentation;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.TimeoutException;
public interface SvgImageCreator {
public boolean createSvgImage(UploadedPresentation pres, int page);
public boolean createSvgImage(UploadedPresentation pres, int page) throws TimeoutException;
}

View File

@ -19,9 +19,14 @@
package org.bigbluebutton.presentation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.file.Path;
public final class Util {
private static Logger log = LoggerFactory.getLogger(Util.class);
public static void deleteDirectory(File directory) {
/**
@ -40,4 +45,20 @@ public final class Util {
// Now that the directory is empty. Delete it.
directory.delete();
}
public static void deleteDirectoryFromFileHandlingErrors(File presentationFile) {
if ( presentationFile != null ){
Path presDir = presentationFile.toPath().getParent();
try {
File presFileDir = new File(presDir.toString());
if (presFileDir.exists()) {
deleteDirectory(presFileDir);
}
} catch (Exception ex) {
log.error("Error while trying to delete directory {}", presDir.toString(), ex);
}
}
}
}

View File

@ -1,110 +0,0 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2015 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.presentation.handlers;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* The default command output the anlayse looks like the following: </br>
* 20 DEBUG Using</br>
* 60 VERBOSE Updating font</br>
* 80 VERBOSE Drawing
*
*/
public class Pdf2SwfPageConverterHandler extends AbstractCommandHandler {
private static Logger log = LoggerFactory
.getLogger(Pdf2SwfPageConverterHandler.class);
private static final String PLACEMENT_OUTPUT = "DEBUG Using";
private static final String TEXT_TAG_OUTPUT = "VERBOSE Updating";
private static final String IMAGE_TAG_OUTPUT = "VERBOSE Drawing";
private static final String DIGITS_AND_WHITESPACES = "\\d+\\s";
private static final String PLACEMENT_PATTERN = DIGITS_AND_WHITESPACES + PLACEMENT_OUTPUT;
private static final String TEXT_TAG_PATTERN = DIGITS_AND_WHITESPACES + TEXT_TAG_OUTPUT;
private static final String IMAGE_TAG_PATTERN = DIGITS_AND_WHITESPACES + IMAGE_TAG_OUTPUT;
/**
*
* @return The number of PlaceObject2 tags in the generated SWF
*/
public long numberOfPlacements() {
if (stdoutContains(PLACEMENT_OUTPUT)) {
try {
String out = stdoutBuilder.toString();
Pattern r = Pattern.compile(PLACEMENT_PATTERN);
Matcher m = r.matcher(out);
m.find();
return Integer
.parseInt(m.group(0).replace(PLACEMENT_OUTPUT, "").trim());
} catch (Exception e) {
log.error("Exception counting the number of placements", e);
return 0;
}
}
return 0;
}
/**
*
* @return The number of text tags in the generated SWF.
*/
public long numberOfTextTags() {
if (stdoutContains(TEXT_TAG_OUTPUT)) {
try {
String out = stdoutBuilder.toString();
Pattern r = Pattern.compile(TEXT_TAG_PATTERN);
Matcher m = r.matcher(out);
m.find();
return Integer.parseInt(m.group(0).replace(TEXT_TAG_OUTPUT, "").trim());
} catch (Exception e) {
log.error("Exception counting the number of text tags", e);
return 0;
}
}
return 0;
}
/**
*
* @return The number of image tags in the generated SWF.
*/
public long numberOfImageTags() {
if (stdoutContains(IMAGE_TAG_OUTPUT)) {
try {
String out = stdoutBuilder.toString();
Pattern r = Pattern.compile(IMAGE_TAG_PATTERN);
Matcher m = r.matcher(out);
m.find();
return Integer
.parseInt(m.group(0).replace(IMAGE_TAG_OUTPUT, "").trim());
} catch (Exception e) {
log.error("Exception counting the number of iamge tags", e);
return 0;
}
}
return 0;
}
}

View File

@ -1,27 +0,0 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2015 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.presentation.handlers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Png2SwfPageConverterHandler extends AbstractCommandHandler {
private static Logger log = LoggerFactory.getLogger(Png2SwfPageConverterHandler.class);
}

View File

@ -0,0 +1,167 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.presentation.imp;
import java.awt.image.BufferedImage;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeoutException;
import org.bigbluebutton.presentation.ImageResizer;
import org.bigbluebutton.presentation.PngCreator;
import org.bigbluebutton.presentation.SvgImageCreator;
import org.bigbluebutton.presentation.TextFileCreator;
import org.bigbluebutton.presentation.ThumbnailCreator;
import org.bigbluebutton.presentation.UploadedPresentation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.imageio.ImageIO;
public class ImageSlidesGenerationService {
private static Logger log = LoggerFactory.getLogger(ImageSlidesGenerationService.class);
private ExecutorService executor;
private SlidesGenerationProgressNotifier notifier;
private SvgImageCreator svgImageCreator;
private ThumbnailCreator thumbnailCreator;
private TextFileCreator textFileCreator;
private PngCreator pngCreator;
private ImageResizer imageResizer;
private long maxImageWidth = 2048;
private long maxImageHeight = 1536;
private long MAX_CONVERSION_TIME = 5*60*1000L;
private boolean svgImagesRequired=true;
private boolean generatePngs;
public ImageSlidesGenerationService() {
int numThreads = Runtime.getRuntime().availableProcessors();
executor = Executors.newFixedThreadPool(numThreads);
}
public void generateSlides(UploadedPresentation pres) {
for (int page = 1; page <= pres.getNumberOfPages(); page++) {
/* adding accessibility */
createTextFiles(pres, page);
createThumbnails(pres, page);
if (svgImagesRequired) {
try {
createSvgImages(pres, page);
} catch (TimeoutException e) {
log.error("Slide {} was not converted due to TimeoutException, ending process.", page, e);
notifier.sendUploadFileTimedout(pres, page);
break;
}
}
if (generatePngs) {
createPngImages(pres, page);
}
notifier.sendConversionUpdateMessage(page, pres, page);
}
System.out.println("****** Conversion complete for " + pres.getName());
notifier.sendConversionCompletedMessage(pres);
}
private void createTextFiles(UploadedPresentation pres, int page) {
log.debug("Creating textfiles for accessibility.");
notifier.sendCreatingTextFilesUpdateMessage(pres);
textFileCreator.createTextFile(pres, page);
}
private void createThumbnails(UploadedPresentation pres, int page) {
log.debug("Creating thumbnails.");
notifier.sendCreatingThumbnailsUpdateMessage(pres);
thumbnailCreator.createThumbnail(pres, page, pres.getUploadedFile());
}
private void createSvgImages(UploadedPresentation pres, int page) throws TimeoutException{
log.debug("Creating SVG images.");
try {
BufferedImage bimg = ImageIO.read(pres.getUploadedFile());
if(bimg.getWidth() > maxImageWidth || bimg.getHeight() > maxImageHeight) {
log.info("The image exceeds max dimension allowed, it will be resized.");
resizeImage(pres, maxImageWidth + "x" + maxImageHeight);
}
} catch (Exception e) {
log.error("Exception while resizing image {}", pres.getName(), e);
}
notifier.sendCreatingSvgImagesUpdateMessage(pres);
svgImageCreator.createSvgImage(pres, page);
}
private void createPngImages(UploadedPresentation pres, int page) {
pngCreator.createPng(pres, page, pres.getUploadedFile());
}
private void resizeImage(UploadedPresentation pres, String ratio) {
imageResizer.resize(pres, ratio);
}
public void setThumbnailCreator(ThumbnailCreator thumbnailCreator) {
this.thumbnailCreator = thumbnailCreator;
}
public void setTextFileCreator(TextFileCreator textFileCreator) {
this.textFileCreator = textFileCreator;
}
public void setPngCreator(PngCreator pngCreator) {
this.pngCreator = pngCreator;
}
public void setSvgImageCreator(SvgImageCreator svgImageCreator) {
this.svgImageCreator = svgImageCreator;
}
public void setGeneratePngs(boolean generatePngs) {
this.generatePngs = generatePngs;
}
public void setSvgImagesRequired(boolean svg) {
this.svgImagesRequired = svg;
}
public void setMaxConversionTime(int minutes) {
MAX_CONVERSION_TIME = minutes * 60 * 1000L;
}
public void setSlidesGenerationProgressNotifier(SlidesGenerationProgressNotifier notifier) {
this.notifier = notifier;
}
public void setImageResizer(ImageResizer imageResizer) {
this.imageResizer = imageResizer;
}
public void setMaxImageWidth(long maxImageWidth) {
this.maxImageWidth = maxImageWidth;
}
public void setMaxImageHeight(long maxImageHeight) {
this.maxImageHeight = maxImageHeight;
}
}

View File

@ -1,258 +0,0 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.presentation.imp;
import java.text.DecimalFormat;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.bigbluebutton.presentation.FileTypeConstants;
import org.bigbluebutton.presentation.ImageResizer;
import org.bigbluebutton.presentation.ImageToSwfSlide;
import org.bigbluebutton.presentation.PageConverter;
import org.bigbluebutton.presentation.PngCreator;
import org.bigbluebutton.presentation.SvgImageCreator;
import org.bigbluebutton.presentation.TextFileCreator;
import org.bigbluebutton.presentation.ThumbnailCreator;
import org.bigbluebutton.presentation.UploadedPresentation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ImageToSwfSlidesGenerationService {
private static Logger log = LoggerFactory.getLogger(ImageToSwfSlidesGenerationService.class);
private ExecutorService executor;
private CompletionService<ImageToSwfSlide> completionService;
private SwfSlidesGenerationProgressNotifier notifier;
private PageConverter jpgToSwfConverter;
private PageConverter pngToSwfConverter;
private SvgImageCreator svgImageCreator;
private ThumbnailCreator thumbnailCreator;
private TextFileCreator textFileCreator;
private PngCreator pngCreator;
private ImageResizer imageResizer;
private Long maxImageSize;
private long MAX_CONVERSION_TIME = 5*60*1000L;
private String BLANK_SLIDE;
private boolean swfSlidesRequired;
private boolean svgImagesRequired;
private boolean generatePngs;
public ImageToSwfSlidesGenerationService() {
int numThreads = Runtime.getRuntime().availableProcessors();
executor = Executors.newFixedThreadPool(numThreads);
completionService = new ExecutorCompletionService<ImageToSwfSlide>(executor);
}
public void generateSlides(UploadedPresentation pres) {
for (int page = 1; page <= pres.getNumberOfPages(); page++) {
if (swfSlidesRequired) {
if (pres.getNumberOfPages() > 0) {
PageConverter pageConverter = determinePageConverter(pres);
convertImageToSwf(pres, pageConverter);
}
}
/* adding accessibility */
createTextFiles(pres, page);
createThumbnails(pres, page);
if (svgImagesRequired) {
createSvgImages(pres, page);
}
if (generatePngs) {
createPngImages(pres, page);
}
notifier.sendConversionUpdateMessage(page, pres, page);
}
System.out.println("****** Conversion complete for " + pres.getName());
notifier.sendConversionCompletedMessage(pres);
}
private PageConverter determinePageConverter(UploadedPresentation pres) {
String fileType = pres.getFileType().toUpperCase();
if ((FileTypeConstants.JPEG.equalsIgnoreCase(fileType)) || (FileTypeConstants.JPG.equalsIgnoreCase(fileType))) {
return jpgToSwfConverter;
}
return pngToSwfConverter;
}
private void createTextFiles(UploadedPresentation pres, int page) {
log.debug("Creating textfiles for accessibility.");
notifier.sendCreatingTextFilesUpdateMessage(pres);
textFileCreator.createTextFile(pres, page);
}
private void createThumbnails(UploadedPresentation pres, int page) {
log.debug("Creating thumbnails.");
notifier.sendCreatingThumbnailsUpdateMessage(pres);
thumbnailCreator.createThumbnail(pres, page, pres.getUploadedFile());
}
private void createSvgImages(UploadedPresentation pres, int page) {
log.debug("Creating SVG images.");
notifier.sendCreatingSvgImagesUpdateMessage(pres);
svgImageCreator.createSvgImage(pres, page);
}
private void createPngImages(UploadedPresentation pres, int page) {
pngCreator.createPng(pres, page, pres.getUploadedFile());
}
private void convertImageToSwf(UploadedPresentation pres, PageConverter pageConverter) {
int numPages = pres.getNumberOfPages();
// A better implementation is described at the link below
// https://stackoverflow.com/questions/4513648/how-to-estimate-the-size-of-jpeg-image-which-will-be-scaled-down
if (pres.getUploadedFile().length() > maxImageSize) {
DecimalFormat percentFormat= new DecimalFormat("#.##%");
// Resize the image and overwrite it
resizeImage(pres, percentFormat
.format(Double.valueOf(maxImageSize) / Double.valueOf(pres.getUploadedFile().length())));
}
ImageToSwfSlide[] slides = setupSlides(pres, numPages, pageConverter);
generateSlides(slides);
handleSlideGenerationResult(pres, slides);
}
private void resizeImage(UploadedPresentation pres, String ratio) {
imageResizer.resize(pres, ratio);
}
private void handleSlideGenerationResult(UploadedPresentation pres, ImageToSwfSlide[] slides) {
long endTime = System.currentTimeMillis() + MAX_CONVERSION_TIME;
for (int t = 0; t < slides.length; t++) {
Future<ImageToSwfSlide> future = null;
ImageToSwfSlide slide = null;
try {
long timeLeft = endTime - System.currentTimeMillis();
future = completionService.take();
slide = future.get(timeLeft, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
log.error("InterruptedException while creating slide {}", pres.getName(), e);
} catch (ExecutionException e) {
log.error("ExecutionException while creating slide {}", pres.getName(), e);
} catch (TimeoutException e) {
log.error("TimeoutException while converting {}", pres.getName(), e);
} finally {
if ((slide != null) && (! slide.isDone())){
log.warn("Creating blank slide for {}", slide.getPageNumber());
future.cancel(true);
slide.generateBlankSlide();
}
}
}
}
private ImageToSwfSlide[] setupSlides(UploadedPresentation pres, int numPages, PageConverter pageConverter) {
ImageToSwfSlide[] slides = new ImageToSwfSlide[numPages];
for (int page = 1; page <= numPages; page++) {
ImageToSwfSlide slide = new ImageToSwfSlide(pres, page);
slide.setBlankSlide(BLANK_SLIDE);
slide.setPageConverter(pageConverter);
// Array index is zero-based
slides[page-1] = slide;
}
return slides;
}
private void generateSlides(ImageToSwfSlide[] slides) {
for (int i = 0; i < slides.length; i++) {
final ImageToSwfSlide slide = slides[i];
completionService.submit(new Callable<ImageToSwfSlide>() {
public ImageToSwfSlide call() {
return slide.createSlide();
}
});
}
}
public void setJpgPageConverter(PageConverter converter) {
this.jpgToSwfConverter = converter;
}
public void setPngPageConverter(PageConverter converter) {
this.pngToSwfConverter = converter;
}
public void setBlankSlide(String blankSlide) {
this.BLANK_SLIDE = blankSlide;
}
public void setThumbnailCreator(ThumbnailCreator thumbnailCreator) {
this.thumbnailCreator = thumbnailCreator;
}
public void setTextFileCreator(TextFileCreator textFileCreator) {
this.textFileCreator = textFileCreator;
}
public void setPngCreator(PngCreator pngCreator) {
this.pngCreator = pngCreator;
}
public void setSvgImageCreator(SvgImageCreator svgImageCreator) {
this.svgImageCreator = svgImageCreator;
}
public void setGeneratePngs(boolean generatePngs) {
this.generatePngs = generatePngs;
}
public void setSwfSlidesRequired(boolean swf) {
this.swfSlidesRequired = swf;
}
public void setSvgImagesRequired(boolean svg) {
this.svgImagesRequired = svg;
}
public void setMaxConversionTime(int minutes) {
MAX_CONVERSION_TIME = minutes * 60 * 1000L;
}
public void setSwfSlidesGenerationProgressNotifier(SwfSlidesGenerationProgressNotifier notifier) {
this.notifier = notifier;
}
public void setImageResizer(ImageResizer imageResizer) {
this.imageResizer = imageResizer;
}
public void setMaxImageSize(Long maxImageSize) {
this.maxImageSize = maxImageSize;
}
}

View File

@ -1,66 +0,0 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.presentation.imp;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import org.bigbluebutton.presentation.PageConverter;
import org.bigbluebutton.presentation.UploadedPresentation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
public class Jpeg2SwfPageConverter implements PageConverter {
private static Logger log = LoggerFactory.getLogger(Jpeg2SwfPageConverter.class);
private String SWFTOOLS_DIR;
public boolean convert(File presentationFile, File output, int page, UploadedPresentation pres){
String COMMAND = SWFTOOLS_DIR + File.separatorChar + "jpeg2swf -o " + output.getAbsolutePath() + " " + presentationFile.getAbsolutePath();
boolean done = new ExternalProcessExecutor().exec(COMMAND, 10000);
if (done && output.exists()) {
return true;
} else {
Map<String, Object> logData = new HashMap<>();
logData.put("meetingId", pres.getMeetingId());
logData.put("presId", pres.getId());
logData.put("filename", pres.getName());
logData.put("logCode", "jpg_to_swf_conversion_failed");
logData.put("message", "Failed to convert: " + output.getAbsolutePath() + " does not exist.");
Gson gson = new Gson();
String logStr = gson.toJson(logData);
log.warn(" --analytics-- data={}", logStr);
return false;
}
}
public void setSwfToolsDir(String dir) {
SWFTOOLS_DIR = dir;
}
}

View File

@ -35,6 +35,8 @@ import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import static org.bigbluebutton.presentation.Util.deleteDirectoryFromFileHandlingErrors;
public abstract class Office2PdfPageConverter {
private static Logger log = LoggerFactory.getLogger(Office2PdfPageConverter.class);
@ -95,6 +97,7 @@ public abstract class Office2PdfPageConverter {
return false;
}
} catch (Exception e) {
deleteDirectoryFromFileHandlingErrors(presentationFile);
Map<String, Object> logData = new HashMap<>();
logData.put("meetingId", pres.getMeetingId());
logData.put("presId", pres.getId());

View File

@ -3,56 +3,46 @@ package org.bigbluebutton.presentation.imp;
import org.bigbluebutton.presentation.*;
import java.io.File;
import java.util.concurrent.TimeoutException;
public class PageToConvert {
private UploadedPresentation pres;
private int page;
private boolean swfSlidesRequired;
private boolean svgImagesRequired;
private boolean svgImagesRequired=true;
private boolean generatePngs;
private PageExtractor pageExtractor;
private String BLANK_SLIDE;
private int MAX_SWF_FILE_SIZE;
private TextFileCreator textFileCreator;
private SvgImageCreator svgImageCreator;
private ThumbnailCreator thumbnailCreator;
private PngCreator pngCreator;
private PageConverter pdfToSwfConverter;
private SwfSlidesGenerationProgressNotifier notifier;
private SlidesGenerationProgressNotifier notifier;
private File pageFile;
private String messageErrorInConversion;
public PageToConvert(UploadedPresentation pres,
int page,
File pageFile,
boolean swfSlidesRequired,
boolean svgImagesRequired,
boolean generatePngs,
TextFileCreator textFileCreator,
SvgImageCreator svgImageCreator,
ThumbnailCreator thumbnailCreator,
PngCreator pngCreator,
PageConverter pdfToSwfConverter,
SwfSlidesGenerationProgressNotifier notifier,
String blankSlide,
int maxSwfFileSize) {
SlidesGenerationProgressNotifier notifier) {
this.pres = pres;
this.page = page;
this.pageFile = pageFile;
this.swfSlidesRequired = swfSlidesRequired;
this.svgImagesRequired = svgImagesRequired;
this.generatePngs = generatePngs;
this.textFileCreator = textFileCreator;
this.svgImageCreator = svgImageCreator;
this.thumbnailCreator = thumbnailCreator;
this.pngCreator = pngCreator;
this.pdfToSwfConverter = pdfToSwfConverter;
this.notifier = notifier;
this.BLANK_SLIDE = blankSlide;
this.MAX_SWF_FILE_SIZE = maxSwfFileSize;
}
public File getPageFile() {
@ -71,13 +61,16 @@ public class PageToConvert {
return pres.getMeetingId();
}
public PageToConvert convert() {
// Only create SWF files if the configuration requires it
if (swfSlidesRequired) {
convertPdfToSwf(pres, page, pageFile);
public String getMessageErrorInConversion() {
return messageErrorInConversion;
}
public void setMessageErrorInConversion(String messageErrorInConversion) {
this.messageErrorInConversion = messageErrorInConversion;
}
public PageToConvert convert() {
/* adding accessibility */
createThumbnails(pres, page, pageFile);
@ -85,7 +78,11 @@ public class PageToConvert {
// only create SVG images if the configuration requires it
if (svgImagesRequired) {
try{
createSvgImages(pres, page);
} catch (TimeoutException e) {
messageErrorInConversion = e.getMessage();
}
}
// only create PNG images if the configuration requires it
@ -106,7 +103,7 @@ public class PageToConvert {
textFileCreator.createTextFile(pres, page);
}
private void createSvgImages(UploadedPresentation pres, int page) {
private void createSvgImages(UploadedPresentation pres, int page) throws TimeoutException {
//notifier.sendCreatingSvgImagesUpdateMessage(pres);
svgImageCreator.createSvgImage(pres, page);
}
@ -115,25 +112,4 @@ public class PageToConvert {
pngCreator.createPng(pres, page, pageFile);
}
private void convertPdfToSwf(UploadedPresentation pres, int page, File pageFile) {
PdfToSwfSlide slide = setupSlide(pres, page, pageFile);
generateSlides(pres, slide);
}
private void generateSlides(UploadedPresentation pres, PdfToSwfSlide slide) {
slide.createSlide();
if (!slide.isDone()) {
slide.generateBlankSlide();
}
}
private PdfToSwfSlide setupSlide(UploadedPresentation pres, int page, File pageFile) {
PdfToSwfSlide slide = new PdfToSwfSlide(pres, page, pageFile);
slide.setBlankSlide(BLANK_SLIDE);
slide.setMaxSwfFileSize(MAX_SWF_FILE_SIZE);
slide.setPageConverter(pdfToSwfConverter);
return slide;
}
}

View File

@ -1,239 +0,0 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.presentation.imp;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.FilenameUtils;
import org.bigbluebutton.presentation.PageConverter;
import org.bigbluebutton.presentation.UploadedPresentation;
import org.bigbluebutton.presentation.handlers.Pdf2PngPageConverterHandler;
import org.bigbluebutton.presentation.handlers.Pdf2SwfPageConverterHandler;
import org.bigbluebutton.presentation.handlers.Png2SwfPageConverterHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.zaxxer.nuprocess.NuProcess;
import com.zaxxer.nuprocess.NuProcessBuilder;
public class Pdf2SwfPageConverter implements PageConverter {
private static Logger log = LoggerFactory.getLogger(Pdf2SwfPageConverter.class);
private String SWFTOOLS_DIR;
private String fontsDir;
private long placementsThreshold;
private long defineTextThreshold;
private long imageTagThreshold;
private String convTimeout = "7s";
private int WAIT_FOR_SEC = 7;
public boolean convert(File presentation, File output, int page, UploadedPresentation pres) {
long convertStart = System.currentTimeMillis();
String source = presentation.getAbsolutePath();
String dest = output.getAbsolutePath();
String AVM2SWF = "-T9";
// Building the command line wrapped in shell to be able to use shell
// feature like the pipe
NuProcessBuilder pb = new NuProcessBuilder(Arrays.asList("timeout",
convTimeout, "/bin/sh", "-c",
SWFTOOLS_DIR + File.separatorChar + "pdf2swf" + " -vv " + AVM2SWF + " -F "
+ fontsDir + " " + source + " -o "
+ dest
+ " | egrep 'shape id|Updating font|Drawing' | sed 's/ / /g' | cut -d' ' -f 1-3 | sort | uniq -cw 15"));
Pdf2SwfPageConverterHandler pHandler = new Pdf2SwfPageConverterHandler();
pb.setProcessListener(pHandler);
long pdf2SwfStart = System.currentTimeMillis();
NuProcess process = pb.start();
try {
process.waitFor(WAIT_FOR_SEC, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.error("InterruptedException while creating SWF {}", pres.getName(), e);
}
long pdf2SwfEnd = System.currentTimeMillis();
log.debug("Pdf2Swf conversion duration: {} sec", (pdf2SwfEnd - pdf2SwfStart) / 1000);
boolean timedOut = pdf2SwfEnd
- pdf2SwfStart >= Integer.parseInt(convTimeout.replaceFirst("s", ""))
* 1000;
boolean twiceTotalObjects = pHandler.numberOfPlacements()
+ pHandler.numberOfTextTags()
+ pHandler.numberOfImageTags() >= (placementsThreshold
+ defineTextThreshold + imageTagThreshold) * 2;
File destFile = new File(dest);
if (pHandler.isCommandSuccessful() && destFile.exists()
&& pHandler.numberOfPlacements() < placementsThreshold
&& pHandler.numberOfTextTags() < defineTextThreshold
&& pHandler.numberOfImageTags() < imageTagThreshold) {
return true;
} else {
// We need t delete the destination file as we are starting a new
// conversion process
if (destFile.exists()) {
destFile.delete();
}
Map<String, Object> logData = new HashMap<>();
logData.put("meetingId", pres.getMeetingId());
logData.put("presId", pres.getId());
logData.put("filename", pres.getName());
logData.put("page", page);
logData.put("convertSuccess", pHandler.isCommandSuccessful());
logData.put("fileExists", destFile.exists());
logData.put("numObjectTags", pHandler.numberOfPlacements());
logData.put("numTextTags", pHandler.numberOfTextTags());
logData.put("numImageTags", pHandler.numberOfImageTags());
logData.put("logCode", "problem_with_generated_swf");
Gson gson = new Gson();
String logStr = gson.toJson(logData);
log.warn(" --analytics-- data={}", logStr);
File tempPng = null;
String basePresentationame = UUID.randomUUID().toString();
try {
tempPng = File.createTempFile(basePresentationame + "-" + page, ".png");
} catch (IOException ioException) {
// We should never fall into this if the server is correctly configured
logData = new HashMap<>();
logData.put("meetingId", pres.getMeetingId());
logData.put("presId", pres.getId());
logData.put("filename", pres.getName());
logData.put("logCode", "failed_to_create_temp_file");
logData.put("message", "Unable to create temporary files for pdf to swf.");
gson = new Gson();
logStr = gson.toJson(logData);
log.error(" --analytics-- data={}", logStr, ioException);
}
// long pdfStart = System.currentTimeMillis();
// Step 1: Convert a PDF page to PNG using a raw pdftocairo
NuProcessBuilder pbPng = new NuProcessBuilder(
Arrays.asList("timeout", convTimeout, "pdftocairo", "-png",
"-singlefile", "-r", timedOut || twiceTotalObjects ? "72" : "150",
presentation.getAbsolutePath(), tempPng.getAbsolutePath()
.substring(0, tempPng.getAbsolutePath().lastIndexOf('.'))));
Pdf2PngPageConverterHandler pbPngHandler = new Pdf2PngPageConverterHandler();
pbPng.setProcessListener(pbPngHandler);
NuProcess processPng = pbPng.start();
try {
processPng.waitFor(WAIT_FOR_SEC, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.error("InterruptedException while creating temporary PNG {}", pres.getName(), e);
}
//long pdfEnd = System.currentTimeMillis();
//log.debug("pdftocairo conversion duration: {} sec", (pdfEnd - pdfStart) / 1000);
// long png2swfStart = System.currentTimeMillis();
// Step 2: Convert a PNG image to SWF
// We need to update the file path as pdftocairo adds "-page.png"
source = tempPng.getAbsolutePath();
NuProcessBuilder pbSwf = new NuProcessBuilder(
Arrays.asList("timeout", convTimeout,
SWFTOOLS_DIR + File.separatorChar + "png2swf", "-o", dest, source));
Png2SwfPageConverterHandler pSwfHandler = new Png2SwfPageConverterHandler();
pbSwf.setProcessListener(pSwfHandler);
NuProcess processSwf = pbSwf.start();
try {
processSwf.waitFor(WAIT_FOR_SEC, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.error("InterruptedException while creating SWF {}", pres.getName(), e);
}
//long png2swfEnd = System.currentTimeMillis();
//log.debug("SwfTools conversion duration: {} sec", (png2swfEnd - png2swfStart) / 1000);
// Delete the temporary PNG and PDF files after finishing the image
// conversion
tempPng.delete();
boolean doneSwf = pSwfHandler.isCommandSuccessful();
long convertEnd = System.currentTimeMillis();
logData = new HashMap<>();
logData.put("meetingId", pres.getMeetingId());
logData.put("presId", pres.getId());
logData.put("filename", pres.getName());
logData.put("page", page);
logData.put("conversionTime(sec)", (convertEnd - convertStart) / 1000);
logData.put("logCode", "conversion_took_too_long");
logData.put("message", "PDF to SWF conversion took a long time.");
logStr = gson.toJson(logData);
log.info(" --analytics-- data={}", logStr);
if (doneSwf && destFile.exists()) {
return true;
} else {
logData = new HashMap<>();
logData.put("meetingId", pres.getMeetingId());
logData.put("presId", pres.getId());
logData.put("filename", pres.getName());
logData.put("page", page);
logData.put("conversionTime(sec)", (convertEnd - convertStart) / 1000);
logData.put("logCode", "pdf2swf_conversion_failed");
logData.put("message", "Failed to convert: " + destFile + " does not exist.");
logStr = gson.toJson(logData);
log.warn(" --analytics-- data={}", logStr);
return false;
}
}
}
public void setSwfToolsDir(String dir) {
SWFTOOLS_DIR = dir;
}
public void setFontsDir(String dir) {
fontsDir = dir;
}
public void setPlacementsThreshold(long threshold) {
placementsThreshold = threshold;
}
public void setDefineTextThreshold(long threshold) {
defineTextThreshold = threshold;
}
public void setImageTagThreshold(long threshold) {
imageTagThreshold = threshold;
}
}

View File

@ -26,8 +26,8 @@ import org.bigbluebutton.presentation.messages.PageConvertProgressMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PdfToSwfSlidesGenerationService {
private static Logger log = LoggerFactory.getLogger(PdfToSwfSlidesGenerationService.class);
public class PdfSlidesGenerationService {
private static Logger log = LoggerFactory.getLogger(PdfSlidesGenerationService.class);
private ExecutorService executor;
@ -35,7 +35,7 @@ public class PdfToSwfSlidesGenerationService {
private PresentationConversionCompletionService presentationConversionCompletionService;
public PdfToSwfSlidesGenerationService(int numConversionThreads) {
public PdfSlidesGenerationService(int numConversionThreads) {
executor = Executors.newFixedThreadPool(numConversionThreads);
}

View File

@ -1,64 +0,0 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.presentation.imp;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import org.bigbluebutton.presentation.PageConverter;
import org.bigbluebutton.presentation.UploadedPresentation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
public class Png2SwfPageConverter implements PageConverter {
private static Logger log = LoggerFactory.getLogger(Png2SwfPageConverter.class);
private String SWFTOOLS_DIR;
public boolean convert(File presentationFile, File output, int page, UploadedPresentation pres){
String COMMAND = SWFTOOLS_DIR + File.separatorChar + "png2swf -o " + output.getAbsolutePath() + " " + presentationFile.getAbsolutePath();
boolean done = new ExternalProcessExecutor().exec(COMMAND, 10000);
if (done && output.exists()) {
return true;
} else {
Map<String, Object> logData = new HashMap<String, Object>();
logData.put("meetingId", pres.getMeetingId());
logData.put("presId", pres.getId());
logData.put("filename", pres.getName());
logData.put("logCode", "png_to_swf_failed");
logData.put("message", "Failed to convert PNG doc to SWF.");
Gson gson = new Gson();
String logStr = gson.toJson(logData);
log.warn(" --analytics-- data={}", logStr);
return false;
}
}
public void setSwfToolsDir(String dir) {
SWFTOOLS_DIR = dir;
}
}

View File

@ -14,7 +14,7 @@ import java.util.concurrent.*;
public class PresentationConversionCompletionService {
private static Logger log = LoggerFactory.getLogger(PresentationConversionCompletionService.class);
private SwfSlidesGenerationProgressNotifier notifier;
private SlidesGenerationProgressNotifier notifier;
private ExecutorService executor;
private volatile boolean processProgress = false;
@ -105,7 +105,7 @@ public class PresentationConversionCompletionService {
processProgress = false;
}
public void setSwfSlidesGenerationProgressNotifier(SwfSlidesGenerationProgressNotifier notifier) {
public void setSlidesGenerationProgressNotifier(SlidesGenerationProgressNotifier notifier) {
this.notifier = notifier;
}
}

View File

@ -12,7 +12,9 @@ import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
@ -22,13 +24,10 @@ import java.util.concurrent.LinkedBlockingQueue;
public class PresentationFileProcessor {
private static Logger log = LoggerFactory.getLogger(PresentationFileProcessor.class);
private boolean swfSlidesRequired;
private boolean svgImagesRequired;
private boolean svgImagesRequired=true;
private boolean generatePngs;
private PageExtractor pageExtractor;
private String BLANK_SLIDE;
private int MAX_SWF_FILE_SIZE;
private long bigPdfSize;
private long maxBigPdfPageSize;
@ -38,12 +37,11 @@ public class PresentationFileProcessor {
private SvgImageCreator svgImageCreator;
private ThumbnailCreator thumbnailCreator;
private PngCreator pngCreator;
private PageConverter pdfToSwfConverter;
private SwfSlidesGenerationProgressNotifier notifier;
private SlidesGenerationProgressNotifier notifier;
private PageCounterService counterService;
private PresentationConversionCompletionService presentationConversionCompletionService;
private ImageToSwfSlidesGenerationService imageToSwfSlidesGenerationService;
private PdfToSwfSlidesGenerationService pdfToSwfSlidesGenerationService;
private ImageSlidesGenerationService imageSlidesGenerationService;
private PdfSlidesGenerationService pdfSlidesGenerationService;
private ExecutorService executor;
private volatile boolean processPresentation = false;
@ -86,11 +84,12 @@ public class PresentationFileProcessor {
} else if (SupportedFileTypes.isImageFile(pres.getFileType())) {
pres.setNumberOfPages(1); // There should be only one image to convert.
sendDocPageConversionStartedProgress(pres);
imageToSwfSlidesGenerationService.generateSlides(pres);
imageSlidesGenerationService.generateSlides(pres);
}
}
private void extractIntoPages(UploadedPresentation pres) {
List<PageToConvert> listOfPagesConverted = new ArrayList<>();
for (int page = 1; page <= pres.getNumberOfPages(); page++) {
String presDir = pres.getUploadedFile().getParent();
File pageFile = new File(presDir + "/page" + "-" + page + ".pdf");
@ -109,20 +108,27 @@ public class PresentationFileProcessor {
pres,
page,
pageFile,
swfSlidesRequired,
svgImagesRequired,
generatePngs,
textFileCreator,
svgImageCreator,
thumbnailCreator,
pngCreator,
pdfToSwfConverter,
notifier,
BLANK_SLIDE,
MAX_SWF_FILE_SIZE
notifier
);
pdfToSwfSlidesGenerationService.process(pageToConvert);
pdfSlidesGenerationService.process(pageToConvert);
listOfPagesConverted.add(pageToConvert);
PageToConvert timeoutErrorMessage =
listOfPagesConverted.stream().filter(item -> {
return item.getMessageErrorInConversion() != null;
}).findAny().orElse(null);
if (timeoutErrorMessage != null) {
log.error(timeoutErrorMessage.getMessageErrorInConversion());
notifier.sendUploadFileTimedout(pres, timeoutErrorMessage.getPageNumber());
break;
}
}
}
@ -267,7 +273,7 @@ public class PresentationFileProcessor {
processPresentation = false;
}
public void setSwfSlidesGenerationProgressNotifier(SwfSlidesGenerationProgressNotifier notifier) {
public void setSlidesGenerationProgressNotifier(SlidesGenerationProgressNotifier notifier) {
this.notifier = notifier;
}
@ -279,26 +285,10 @@ public class PresentationFileProcessor {
this.pageExtractor = extractor;
}
public void setPageConverter(PageConverter converter) {
this.pdfToSwfConverter = converter;
}
public void setBlankSlide(String blankSlide) {
this.BLANK_SLIDE = blankSlide;
}
public void setMaxSwfFileSize(int size) {
this.MAX_SWF_FILE_SIZE = size;
}
public void setGeneratePngs(boolean generatePngs) {
this.generatePngs = generatePngs;
}
public void setSwfSlidesRequired(boolean swfSlidesRequired) {
this.swfSlidesRequired = swfSlidesRequired;
}
public void setBigPdfSize(long bigPdfSize) {
this.bigPdfSize = bigPdfSize;
}
@ -331,15 +321,15 @@ public class PresentationFileProcessor {
MAX_CONVERSION_TIME = minutes * 60 * 1000L * 1000L * 1000L;
}
public void setImageToSwfSlidesGenerationService(ImageToSwfSlidesGenerationService s) {
imageToSwfSlidesGenerationService = s;
public void setImageSlidesGenerationService(ImageSlidesGenerationService s) {
imageSlidesGenerationService = s;
}
public void setPresentationConversionCompletionService(PresentationConversionCompletionService s) {
this.presentationConversionCompletionService = s;
}
public void setPdfToSwfSlidesGenerationService(PdfToSwfSlidesGenerationService s) {
this.pdfToSwfSlidesGenerationService = s;
public void setPdfSlidesGenerationService(PdfSlidesGenerationService s) {
this.pdfSlidesGenerationService = s;
}
}

View File

@ -27,11 +27,11 @@ import org.bigbluebutton.presentation.messages.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SwfSlidesGenerationProgressNotifier {
private static Logger log = LoggerFactory.getLogger(SwfSlidesGenerationProgressNotifier.class);
public class SlidesGenerationProgressNotifier {
private static Logger log = LoggerFactory.getLogger(SlidesGenerationProgressNotifier.class);
private IBbbWebApiGWApp messagingService;
private int maxNumberOfAttempts = 3;
private GeneratedSlidesInfoHelper generatedSlidesInfoHelper;
@ -50,6 +50,29 @@ public class SwfSlidesGenerationProgressNotifier {
maxUploadFileSize);
messagingService.sendDocConversionMsg(progress);
}
public void sendInvalidMimeTypeMessage(UploadedPresentation pres, String fileMime, String fileExtension) {
DocInvalidMimeType invalidMimeType = new DocInvalidMimeType(
pres.getPodId(),
pres.getMeetingId(),
pres.getId(),
pres.getTemporaryPresentationId(),
pres.getName(),
pres.getAuthzToken(),
"IVALID_MIME_TYPE",
fileMime,
fileExtension
);
messagingService.sendDocConversionMsg(invalidMimeType);
}
public void sendUploadFileTimedout(UploadedPresentation pres, int page) {
UploadFileTimedoutMessage errorMessage = new UploadFileTimedoutMessage(
pres.getPodId(),
pres.getMeetingId(),
pres.getName(),
ConversionMessageConstants.CONVERSION_TIMEOUT_KEY,
page, pres.getTemporaryPresentationId(), pres.getId(), maxNumberOfAttempts);
messagingService.sendDocConversionMsg(errorMessage);
}
public void sendConversionUpdateMessage(int slidesCompleted, UploadedPresentation pres, int pageGenerated) {
DocPageGeneratedProgress progress = new DocPageGeneratedProgress(pres.getPodId(),
@ -101,6 +124,10 @@ public class SwfSlidesGenerationProgressNotifier {
messagingService = m;
}
public void setMaxNumberOfAttempts(int maxNumberOfAttempts) {
this.maxNumberOfAttempts = maxNumberOfAttempts;
}
public void setGeneratedSlidesInfoHelper(GeneratedSlidesInfoHelper helper) {
generatedSlidesInfoHelper = helper;
}

View File

@ -7,6 +7,7 @@ import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.apache.commons.io.FileUtils;
import org.bigbluebutton.presentation.SupportedFileTypes;
@ -27,17 +28,19 @@ import com.zaxxer.nuprocess.NuProcessBuilder;
public class SvgImageCreatorImp implements SvgImageCreator {
private static Logger log = LoggerFactory.getLogger(SvgImageCreatorImp.class);
private SwfSlidesGenerationProgressNotifier notifier;
private SlidesGenerationProgressNotifier notifier;
private long imageTagThreshold;
private long pathsThreshold;
private int convPdfToSvgTimeout = 60;
private int pdfFontsTimeout = 3;
private int svgResolutionPpi = 300;
private boolean forceRasterizeSlides = false;
private int pngWidthRasterizedSlides = 2048;
private String BLANK_SVG;
private int maxNumberOfAttempts = 3;
@Override
public boolean createSvgImage(UploadedPresentation pres, int page) {
public boolean createSvgImage(UploadedPresentation pres, int page) throws TimeoutException{
boolean success = false;
File svgImagesPresentationDir = determineSvgImagesDirectory(pres.getUploadedFile());
if (!svgImagesPresentationDir.exists())
@ -45,7 +48,7 @@ public class SvgImageCreatorImp implements SvgImageCreator {
try {
success = generateSvgImage(svgImagesPresentationDir, pres, page);
} catch (Exception e) {
} catch (InterruptedException e) {
log.error("Interrupted Exception while generating images {}", pres.getName(), e);
success = false;
}
@ -53,10 +56,30 @@ public class SvgImageCreatorImp implements SvgImageCreator {
return success;
}
private PdfFontType3DetectorHandler createDetectFontType3tHandler(boolean done, int page, String source, UploadedPresentation pres) {
//Detect if PDF contains text with font Type 3
//Pdftocairo has problem to convert Pdf to Svg when text contains font Type 3
//Case detects type 3, rasterize will be forced to avoid the problem
NuProcessBuilder detectFontType3Process = this.createDetectFontType3Process(source, page);
PdfFontType3DetectorHandler detectFontType3tHandler = new PdfFontType3DetectorHandler();
detectFontType3Process.setProcessListener(detectFontType3tHandler);
NuProcess processDetectFontType3 = detectFontType3Process.start();
try {
processDetectFontType3.waitFor(pdfFontsTimeout + 1, TimeUnit.SECONDS);
done = true;
} catch (InterruptedException e) {
done = false;
log.error("InterruptedException while verifing font type 3 on {} page {}: {}", pres.getName(), page, e);
}
return detectFontType3tHandler;
}
private boolean generateSvgImage(File imagePresentationDir, UploadedPresentation pres, int page)
throws InterruptedException {
throws InterruptedException, TimeoutException {
String source = pres.getUploadedFile().getAbsolutePath();
String dest;
int countOfTimeOut = 0;
int numSlides = 1;
boolean done = false;
@ -95,25 +118,19 @@ public class SvgImageCreatorImp implements SvgImageCreator {
// Continue image processing
long startConv = System.currentTimeMillis();
PdfFontType3DetectorHandler detectFontType3tHandler = this.createDetectFontType3tHandler(done, page, source, pres);
//Detect if PDF contains text with font Type 3
//Pdftocairo has problem to convert Pdf to Svg when text contains font Type 3
//Case detects type 3, rasterize will be forced to avoid the problem
NuProcessBuilder detectFontType3Process = this.createDetectFontType3Process(source,page);
PdfFontType3DetectorHandler detectFontType3tHandler = new PdfFontType3DetectorHandler();
detectFontType3Process.setProcessListener(detectFontType3tHandler);
NuProcess processDetectFontType3 = detectFontType3Process.start();
try {
processDetectFontType3.waitFor(convPdfToSvgTimeout + 1, TimeUnit.SECONDS);
done = true;
} catch (InterruptedException e) {
done = false;
log.error("InterruptedException while verifing font type 3 on {} page {}: {}", pres.getName(), page, e);
while (detectFontType3tHandler.isCommandTimeout()) {
// Took the first process of the function out of the count because it already happened above
if (countOfTimeOut >= maxNumberOfAttempts - 1) {
log.error("Command execution (detectFontType3) exceeded the {} secs timeout within {} attempts for {} page {}.", pdfFontsTimeout, maxNumberOfAttempts, pres.getName(), page);
throw new TimeoutException("(Timeout error) The slide " + page +
" could not be processed within "
+ convPdfToSvgTimeout +
" seconds.");
}
if(detectFontType3tHandler.isCommandTimeout()) {
log.error("Command execution (detectFontType3) exceeded the {} secs timeout for {} page {}.", convPdfToSvgTimeout, pres.getName(), page);
detectFontType3tHandler = this.createDetectFontType3tHandler(done, page, source, pres);
countOfTimeOut += 1;
}
if(detectFontType3tHandler.hasFontType3()) {
@ -316,7 +333,7 @@ public class SvgImageCreatorImp implements SvgImageCreator {
rawCommand += " | grep -m 1 'Type 3'";
rawCommand += " | wc -l";
return new NuProcessBuilder(Arrays.asList("timeout", convPdfToSvgTimeout + "s", "/bin/sh", "-c", rawCommand));
return new NuProcessBuilder(Arrays.asList("timeout", pdfFontsTimeout + "s", "/bin/sh", "-c", rawCommand));
}
private File determineSvgImagesDirectory(File presentationFile) {
@ -349,6 +366,12 @@ public class SvgImageCreatorImp implements SvgImageCreator {
public void setBlankSvg(String blankSvg) {
BLANK_SVG = blankSvg;
}
public void setMaxNumberOfAttempts(int maxNumberOfAttempts) {
this.maxNumberOfAttempts = maxNumberOfAttempts;
}
public void setPdfFontsTimeout(int pdfFontsTimeout) {
this.pdfFontsTimeout = pdfFontsTimeout;
}
public void setImageTagThreshold(long threshold) {
imageTagThreshold = threshold;
@ -358,8 +381,8 @@ public class SvgImageCreatorImp implements SvgImageCreator {
pathsThreshold = threshold;
}
public void setSwfSlidesGenerationProgressNotifier(
SwfSlidesGenerationProgressNotifier notifier) {
public void setSlidesGenerationProgressNotifier(
SlidesGenerationProgressNotifier notifier) {
this.notifier = notifier;
}

View File

@ -0,0 +1,34 @@
package org.bigbluebutton.presentation.messages;
public class DocInvalidMimeType implements IDocConversionMsg{
public final String podId;
public final String meetingId;
public final String presId;
public final String temporaryPresentationId;
public final String filename;
public final String authzToken;
public final String messageKey;
public final String fileMime;
public final String fileExtension;
public DocInvalidMimeType( String podId,
String meetingId,
String presId,
String temporaryPresentationId,
String filename,
String authzToken,
String messageKey,
String fileMime,
String fileExtension) {
this.podId = podId;
this.meetingId = meetingId;
this.presId = presId;
this.temporaryPresentationId = temporaryPresentationId;
this.filename = filename;
this.authzToken = authzToken;
this.messageKey = messageKey;
this.fileMime = fileMime;
this.fileExtension = fileExtension;
}
}

View File

@ -0,0 +1,30 @@
package org.bigbluebutton.presentation.messages;
public class UploadFileTimedoutMessage implements IDocConversionMsg {
public final String podId;
public final String meetingId;
public final String filename;
public final int page;
public final String messageKey;
public final String temporaryPresentationId;
public final String presentationId;
public final int maxNumberOfAttempts;
public UploadFileTimedoutMessage(String podId,
String meetingId,
String filename,
String messageKey,
int page,
String temporaryPresentationId,
String presentationId,
int maxNumberOfAttempts) {
this.podId = podId;
this.meetingId = meetingId;
this.temporaryPresentationId = temporaryPresentationId;
this.filename = filename;
this.messageKey = messageKey;
this.page = page;
this.presentationId = presentationId;
this.maxNumberOfAttempts = maxNumberOfAttempts;
}
}

View File

@ -3,7 +3,6 @@ package org.bigbluebutton.api2
import scala.collection.JavaConverters._
import akka.actor.ActorSystem
import akka.event.Logging
import java.util
import org.bigbluebutton.api.domain.{ BreakoutRoomsParams, Group, LockSettingsParams }
import org.bigbluebutton.api.messaging.converters.messages._
import org.bigbluebutton.api2.bus._
@ -131,7 +130,9 @@ class BbbWebApiGWApp(
freeJoin: java.lang.Boolean,
metadata: java.util.Map[String, String], guestPolicy: String, authenticatedGuest: java.lang.Boolean, meetingLayout: String,
welcomeMsgTemplate: String, welcomeMsg: String, modOnlyMessage: String,
dialNumber: String, maxUsers: java.lang.Integer,
dialNumber: String,
maxUsers: java.lang.Integer,
maxUserConcurrentAccesses: java.lang.Integer,
meetingExpireIfNoUserJoinedInMinutes: java.lang.Integer,
meetingExpireWhenLastUserLeftInMinutes: java.lang.Integer,
userInactivityInspectTimerInMinutes: java.lang.Integer,
@ -189,17 +190,23 @@ class BbbWebApiGWApp(
freeJoin = freeJoin.booleanValue(),
breakoutRooms = Vector(),
record = breakoutParams.record.booleanValue(),
privateChatEnabled = breakoutParams.privateChatEnabled.booleanValue()
privateChatEnabled = breakoutParams.privateChatEnabled.booleanValue(),
captureNotes = breakoutParams.captureNotes.booleanValue(),
captureSlides = breakoutParams.captureSlides.booleanValue(),
)
val welcomeProp = WelcomeProp(welcomeMsgTemplate = welcomeMsgTemplate, welcomeMsg = welcomeMsg,
modOnlyMessage = modOnlyMessage)
val voiceProp = VoiceProp(telVoice = voiceBridge, voiceConf = voiceBridge, dialNumber = dialNumber, muteOnStart = muteOnStart.booleanValue())
val usersProp = UsersProp(maxUsers = maxUsers.intValue(), webcamsOnlyForModerator = webcamsOnlyForModerator.booleanValue(),
val usersProp = UsersProp(
maxUsers = maxUsers.intValue(),
maxUserConcurrentAccesses = maxUserConcurrentAccesses,
webcamsOnlyForModerator = webcamsOnlyForModerator.booleanValue(),
userCameraCap = userCameraCap.intValue(),
guestPolicy = guestPolicy, meetingLayout = meetingLayout, allowModsToUnmuteUsers = allowModsToUnmuteUsers.booleanValue(),
allowModsToEjectCameras = allowModsToEjectCameras.booleanValue(),
authenticatedGuest = authenticatedGuest.booleanValue())
authenticatedGuest = authenticatedGuest.booleanValue()
)
val metadataProp = MetadataProp(mapAsScalaMap(metadata).toMap)
val lockSettingsProps = LockSettingsProps(
@ -261,11 +268,6 @@ class BbbWebApiGWApp(
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))
}
def ejectDuplicateUser(meetingId: String, intUserId: String, name: String, extUserId: String): Unit = {
val event = MsgBuilder.buildEjectDuplicateUserRequestToAkkaApps(meetingId, intUserId, name, extUserId)
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))
}
def guestWaitingLeft(meetingId: String, intUserId: String): Unit = {
val event = MsgBuilder.buildGuestWaitingLeftMsg(meetingId, intUserId)
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))
@ -341,6 +343,12 @@ class BbbWebApiGWApp(
} else if (msg.isInstanceOf[UploadFileTooLargeMessage]) {
val event = MsgBuilder.buildPresentationUploadedFileTooLargeErrorSysMsg(msg.asInstanceOf[UploadFileTooLargeMessage])
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))
} else if (msg.isInstanceOf[UploadFileTimedoutMessage]) {
val event = MsgBuilder.buildPresentationUploadedFileTimedoutErrorSysMsg(msg.asInstanceOf[UploadFileTimedoutMessage])
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))
} else if (msg.isInstanceOf[DocInvalidMimeType]) {
val event = MsgBuilder.buildPresentationHasInvalidMimeType(msg.asInstanceOf[DocInvalidMimeType])
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))
}
}

View File

@ -34,16 +34,6 @@ object MsgBuilder {
BbbCommonEnvCoreMsg(envelope, req)
}
def buildEjectDuplicateUserRequestToAkkaApps(meetingId: String, intUserId: String, name: String, extUserId: String): BbbCommonEnvCoreMsg = {
val routing = collection.immutable.HashMap("sender" -> "bbb-web")
val envelope = BbbCoreEnvelope(EjectDuplicateUserReqMsg.NAME, routing)
val header = BbbCoreHeaderWithMeetingId(EjectDuplicateUserReqMsg.NAME, meetingId)
val body = EjectDuplicateUserReqMsgBody(meetingId = meetingId, intUserId = intUserId,
name = name, extUserId = extUserId)
val req = EjectDuplicateUserReqMsg(header, body)
BbbCommonEnvCoreMsg(envelope, req)
}
def buildRegisterUserRequestToAkkaApps(msg: RegisterUser): BbbCommonEnvCoreMsg = {
val routing = collection.immutable.HashMap("sender" -> "bbb-web")
val envelope = BbbCoreEnvelope(RegisterUserReqMsg.NAME, routing)
@ -78,13 +68,12 @@ object MsgBuilder {
val id = presId + "/" + page
val current = if (page == 1) true else false
val thumbUrl = presBaseUrl + "/thumbnail/" + page
val swfUrl = presBaseUrl + "/slide/" + page
val txtUrl = presBaseUrl + "/textfiles/" + page
val svgUrl = presBaseUrl + "/svg/" + page
val pngUrl = presBaseUrl + "/png/" + page
val urls = Map("swf" -> swfUrl, "thumb" -> thumbUrl, "text" -> txtUrl, "svg" -> svgUrl, "png" -> pngUrl)
val urls = Map("thumb" -> thumbUrl, "text" -> txtUrl, "svg" -> svgUrl, "png" -> pngUrl)
PresentationPageConvertedVO(
id = id,
@ -174,14 +163,12 @@ object MsgBuilder {
val num = i
val current = if (i == 1) true else false
val thumbnail = presBaseUrl + "/thumbnail/" + i
val swfUri = presBaseUrl + "/slide/" + i
val txtUri = presBaseUrl + "/textfiles/" + i
val svgUri = presBaseUrl + "/svg/" + i
val p = PageVO(id = id, num = num, thumbUri = thumbnail, swfUri = swfUri,
txtUri = txtUri, svgUri = svgUri,
current = current)
val p = PageVO(id = id, num = num, thumbUri = thumbnail,
txtUri = txtUri, svgUri = svgUri, current = current)
pages += p.id -> p
}
@ -298,4 +285,31 @@ object MsgBuilder {
BbbCommonEnvCoreMsg(envelope, req)
}
def buildPresentationHasInvalidMimeType(msg: DocInvalidMimeType): BbbCommonEnvCoreMsg = {
val routing = collection.immutable.HashMap("sender" -> "bbb-web")
val envelope = BbbCoreEnvelope(PresentationHasInvalidMimeTypeErrorSysPubMsg.NAME, routing)
val header = BbbClientMsgHeader(PresentationHasInvalidMimeTypeErrorSysPubMsg.NAME, msg.meetingId, "not-used")
val body = PresentationHasInvalidMimeTypeErrorSysPubMsgBody(podId = msg.podId, presentationName = msg.filename,
temporaryPresentationId = msg.temporaryPresentationId, presentationId = msg.presId, meetingId = msg.meetingId,
messageKey = msg.messageKey, fileMime = msg.fileMime, fileExtension = msg.fileExtension)
val req = PresentationHasInvalidMimeTypeErrorSysPubMsg(header, body)
BbbCommonEnvCoreMsg(envelope, req)
}
def buildPresentationUploadedFileTimedoutErrorSysMsg(msg: UploadFileTimedoutMessage): BbbCommonEnvCoreMsg = {
val routing = collection.immutable.HashMap("sender" -> "bbb-web")
val envelope = BbbCoreEnvelope(PresentationUploadedFileTimeoutErrorSysPubMsg.NAME, routing)
val header = BbbClientMsgHeader(PresentationUploadedFileTimeoutErrorSysPubMsg.NAME, msg.meetingId, "not-used")
val body = PresentationUploadedFileTimeoutErrorSysPubMsgBody(podId = msg.podId, presentationName = msg.filename,
page = msg.page, meetingId = msg.meetingId, messageKey = msg.messageKey,
temporaryPresentationId = msg.temporaryPresentationId, presentationId = msg.presentationId,
maxNumberOfAttempts = msg.maxNumberOfAttempts)
val req = PresentationUploadedFileTimeoutErrorSysPubMsg(header, body)
BbbCommonEnvCoreMsg(envelope, req)
}
}

View File

@ -12,7 +12,7 @@ case class CreateBreakoutRoomMsg(meetingId: String, parentMeetingId: String,
name: String, sequence: Integer, freeJoin: Boolean, dialNumber: String,
voiceConfId: String, viewerPassword: String, moderatorPassword: String, duration: Int,
sourcePresentationId: String, sourcePresentationSlide: Int,
record: Boolean) extends ApiMsg
record: Boolean, captureNotes: Boolean, captureSlides: Boolean) extends ApiMsg
case class AddUserSession(token: String, session: UserSession)
case class RegisterUser(meetingId: String, intUserId: String, name: String, role: String,

View File

@ -102,7 +102,9 @@ class OldMeetingMsgHdlrActor(val olgMsgGW: OldMessageReceivedGW)
msg.body.room.sourcePresentationId,
msg.body.room.sourcePresentationSlide,
msg.body.room.record,
msg.body.room.privateChatEnabled
msg.body.room.privateChatEnabled,
msg.body.room.captureNotes,
msg.body.room.captureSlides,
))
}

View File

@ -19,7 +19,7 @@
<hasBeenForciblyEnded>${meeting.isForciblyEnded()?c}</hasBeenForciblyEnded>
<startTime>${meeting.getStartTime()?c}</startTime>
<endTime>${meeting.getEndTime()?c}</endTime>
<participantCount>${meeting.getNumUsers()}</participantCount>
<participantCount>${meeting.getNumUsersOnline()}</participantCount>
<listenerCount>${meeting.getNumListenOnly()}</listenerCount>
<voiceParticipantCount>${meeting.getNumVoiceJoined()}</voiceParticipantCount>
<videoCount>${meeting.getNumVideos()}</videoCount>

View File

@ -24,7 +24,7 @@
<hasBeenForciblyEnded>${meeting.isForciblyEnded()?c}</hasBeenForciblyEnded>
<startTime>${meeting.getStartTime()?c}</startTime>
<endTime>${meeting.getEndTime()?c}</endTime>
<participantCount>${meeting.getNumUsers()}</participantCount>
<participantCount>${meeting.getNumUsersOnline()}</participantCount>
<listenerCount>${meeting.getNumListenOnly()}</listenerCount>
<voiceParticipantCount>${meeting.getNumVoiceJoined()}</voiceParticipantCount>
<videoCount>${meeting.getNumVideos()}</videoCount>

View File

@ -1,6 +1,7 @@
{
"log": {
"level": "info"
"level": "info",
"msgName": "PresAnnStatusMsg"
},
"shared": {
"presDir": "/var/bigbluebutton",
@ -25,6 +26,7 @@
"msgName": "NewPresAnnFileAvailableMsg"
},
"bbbWebAPI": "http://127.0.0.1:8090",
"bbbPadsAPI": "http://127.0.0.1:9002",
"redis": {
"host": "127.0.0.1",
"port": 6379,

View File

@ -0,0 +1,32 @@
const {Worker} = require('worker_threads');
const path = require('path');
const WorkerTypes = Object.freeze({
Collector: 'collector',
Process: 'process',
Notifier: 'notifier',
});
const kickOffWorker = (workerType, workerData) => {
return new Promise((resolve, reject) => {
const workerPath = path.join(__dirname, '..', '..', 'workers', `${workerType}.js`);
const worker = new Worker(workerPath, {workerData});
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`Worker '${workerType}' stopped with exit code ${code}`));
}
});
});
};
module.exports = class WorkerStarter {
constructor(workerData) {
this.workerData = workerData;
}
collect = () => kickOffWorker(WorkerTypes.Collector, this.workerData);
process = () => kickOffWorker(WorkerTypes.Process, this.workerData);
notify = () => kickOffWorker(WorkerTypes.Notifier, this.workerData);
};

View File

@ -1,28 +1,14 @@
const Logger = require('./lib/utils/logger');
const WorkerStarter = require('./lib/utils/worker-starter');
const config = require('./config');
const fs = require('fs');
const redis = require('redis');
const {commandOptions} = require('redis');
const {Worker} = require('worker_threads');
const path = require('path');
const logger = new Logger('presAnn Master');
logger.info('Running bbb-export-annotations');
const kickOffCollectorWorker = (jobId) => {
return new Promise((resolve, reject) => {
const collectorPath = path.join(__dirname, 'workers', 'collector.js');
const worker = new Worker(collectorPath, {workerData: jobId});
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`Collector Worker stopped with exit code ${code}`));
}
});
});
};
(async () => {
const client = redis.createClient({
host: config.redis.host,
@ -49,9 +35,10 @@ const kickOffCollectorWorker = (jobId) => {
logger.info('Received job', job.element);
const exportJob = JSON.parse(job.element);
const jobId = exportJob.jobId;
// Create folder in dropbox
const dropbox = path.join(config.shared.presAnnDropboxDir, exportJob.jobId);
const dropbox = path.join(config.shared.presAnnDropboxDir, jobId);
fs.mkdirSync(dropbox, {recursive: true});
// Drop job into dropbox as JSON
@ -61,8 +48,8 @@ const kickOffCollectorWorker = (jobId) => {
}
});
kickOffCollectorWorker(exportJob.jobId);
const collectorWorker = new WorkerStarter({jobId});
collectorWorker.collect();
waitForJobs();
}

View File

@ -1,37 +1,28 @@
const Logger = require('../lib/utils/logger');
const axios = require('axios').default;
const config = require('../config');
const fs = require('fs');
const redis = require('redis');
const {Worker, workerData} = require('worker_threads');
const path = require('path');
const cp = require('child_process');
const fs = require('fs');
const path = require('path');
const redis = require('redis');
const sanitize = require('sanitize-filename');
const stream = require('stream');
const WorkerStarter = require('../lib/utils/worker-starter');
const {workerData} = require('worker_threads');
const {promisify} = require('util');
const jobId = workerData;
const jobId = workerData.jobId;
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(`Process Worker stopped with exit code ${code}`));
}
});
});
};
logger.info(`Collecting job ${jobId}`);
const dropbox = path.join(config.shared.presAnnDropboxDir, jobId);
// Takes the Job from the dropbox
const job = fs.readFileSync(path.join(dropbox, 'job'));
const exportJob = JSON.parse(job);
const jobType = exportJob.jobType;
// Collect the annotations from Redis
(async () => {
async function collectAnnotationsFromRedis() {
const client = redis.createClient({
host: config.redis.host,
port: config.redis.port,
@ -42,13 +33,11 @@ const exportJob = JSON.parse(job);
await client.connect();
const presAnn = await client.hGetAll(exportJob.jobId);
const presAnn = await client.hGetAll(jobId);
// Remove annotations from Redis
await client.del(jobId);
client.disconnect();
const annotations = JSON.stringify(presAnn);
const whiteboard = JSON.parse(annotations);
@ -65,6 +54,31 @@ const exportJob = JSON.parse(job);
const presFile = path.join(exportJob.presLocation, exportJob.presId);
const pdfFile = `${presFile}.pdf`;
// Message to display conversion progress toast
const statusUpdate = {
envelope: {
name: config.log.msgName,
routing: {
sender: exportJob.module,
},
timestamp: (new Date()).getTime(),
},
core: {
header: {
name: config.log.msgName,
meetingId: exportJob.parentMeetingId,
userId: '',
},
body: {
presId: exportJob.presId,
pageNumber: 1,
totalPages: pages.length,
status: 'COLLECTING',
error: false,
},
},
};
if (fs.existsSync(pdfFile)) {
for (const p of pages) {
const pageNumber = p.page;
@ -86,17 +100,92 @@ const exportJob = JSON.parse(job);
pdfFile, outputFile,
];
try {
cp.spawnSync(config.shared.pdftocairo, extract_png_from_pdf, {shell: false});
} catch (error) {
const error_reason = `PDFtoCairo failed extracting slide ${pageNumber}`;
logger.error(`${error_reason} in job ${jobId}: ${error.message}`);
statusUpdate.core.body.status = error_reason;
statusUpdate.core.body.error = true;
}
statusUpdate.core.body.pageNumber = pageNumber;
statusUpdate.envelope.timestamp = (new Date()).getTime();
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate));
statusUpdate.core.body.error = false;
}
// If PNG file already available
} else if (fs.existsSync(`${presFile}.png`)) {
fs.copyFileSync(`${presFile}.png`, path.join(dropbox, 'slide1.png'));
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate));
// If JPEG file available
} else if (fs.existsSync(`${presFile}.jpeg`)) {
fs.copyFileSync(`${presFile}.jpeg`, path.join(dropbox, 'slide1.jpeg'));
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate));
} else {
return logger.error(`Could not find presentation file ${exportJob.jobId}`);
statusUpdate.core.body.error = true;
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate));
client.disconnect();
return logger.error(`Presentation file missing for job ${exportJob.jobId}`);
}
kickOffProcessWorker(exportJob.jobId);
})();
client.disconnect();
const process = new WorkerStarter({jobId, statusUpdate});
process.process();
}
async function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
/** Export shared notes via bbb-pads in the desired format
* @param {Integer} retries - Number of retries to get the shared notes
*/
async function collectSharedNotes(retries = 3) {
/** One of the following formats is supported:
etherpad / html / pdf / txt / doc / odf */
const padId = exportJob.presId;
const notesFormat = 'pdf';
const filename = `${sanitize(exportJob.filename.replace(/\s/g, '_'))}.${notesFormat}`;
const notes_endpoint = `${config.bbbPadsAPI}/p/${padId}/export/${notesFormat}`;
const filePath = path.join(dropbox, filename);
const finishedDownload = promisify(stream.finished);
const writer = fs.createWriteStream(filePath);
try {
const response = await axios({
method: 'GET',
url: notes_endpoint,
responseType: 'stream',
});
response.data.pipe(writer);
await finishedDownload(writer);
} catch (err) {
if (retries > 0 && err?.response?.status == 429) {
// Wait for the bbb-pads API to be available due to rate limiting
const backoff = err.response.headers['retry-after'] * 1000;
logger.info(`Retrying ${jobId} in ${backoff}ms...`);
await sleep(backoff);
return collectSharedNotes(retries - 1);
} else {
logger.error(`Could not download notes in job ${jobId}`);
return;
}
}
const notifier = new WorkerStarter({jobType, jobId, filename});
notifier.notify();
}
switch (jobType) {
case 'PresentationWithAnnotationExportJob': return collectAnnotationsFromRedis();
case 'PresentationWithAnnotationDownloadJob': return collectAnnotationsFromRedis();
case 'PadCaptureJob': return collectSharedNotes();
default: return logger.error(`Unknown job type ${jobType}`);
}

View File

@ -7,8 +7,7 @@ const axios = require('axios').default;
const path = require('path');
const {workerData} = require('worker_threads');
const [jobType, jobId, filename_with_extension] = workerData;
const [jobType, jobId, filename] = [workerData.jobType, workerData.jobId, workerData.filename];
const logger = new Logger('presAnn Notifier Worker');
@ -30,7 +29,7 @@ async function notifyMeetingActor() {
const link = path.join(`${path.sep}bigbluebutton`, 'presentation',
exportJob.parentMeetingId, exportJob.parentMeetingId,
exportJob.presId, 'pdf', jobId, filename_with_extension);
exportJob.presId, 'pdf', jobId, filename);
const notification = {
envelope: {
@ -59,27 +58,35 @@ async function notifyMeetingActor() {
client.disconnect();
}
/** Upload PDF to a BBB room */
async function upload() {
/** Upload PDF to a BBB room
* @param {String} filePath - Absolute path to the file, including the extension
*/
async function upload(filePath) {
const callbackUrl = `${config.bbbWebAPI}/bigbluebutton/presentation/${exportJob.presentationUploadToken}/upload`;
const formData = new FormData();
const file = `${exportJob.presLocation}/pdfs/${jobId}/${filename_with_extension}`;
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(file));
formData.append('fileUpload', fs.createReadStream(filePath));
try {
const res = await axios.post(callbackUrl, formData,
{headers: formData.getHeaders()});
logger.info(`Upload of job ${exportJob.jobId} returned ${res.data}`);
} catch (error) {
return logger.error(`Could not upload job ${exportJob.jobId}: ${error}`);
}
}
if (jobType == 'PresentationWithAnnotationDownloadJob') {
notifyMeetingActor();
} else if (jobType == 'PresentationWithAnnotationExportJob') {
upload();
const filePath = `${exportJob.presLocation}/pdfs/${jobId}/${filename}`;
upload(filePath);
} else if (jobType == 'PadCaptureJob') {
const filePath = `${dropbox}/${filename}`;
upload(filePath);
} else {
logger.error(`Notifier received unknown job type ${jobType}`);
}

View File

@ -3,31 +3,21 @@ const config = require('../config');
const fs = require('fs');
const {create} = require('xmlbuilder2', {encoding: 'utf-8'});
const cp = require('child_process');
const {Worker, workerData} = require('worker_threads');
const WorkerStarter = require('../lib/utils/worker-starter');
const {workerData} = require('worker_threads');
const path = require('path');
const sanitize = require('sanitize-filename');
const {getStroke, getStrokePoints} = require('perfect-freehand');
const {getStrokePoints, getStrokeOutlinePoints} = require('perfect-freehand');
const probe = require('probe-image-size');
const redis = require('redis');
const jobId = workerData;
const [jobId, statusUpdate] = [workerData.jobId, workerData.statusUpdate];
const logger = new Logger('presAnn Process Worker');
logger.info('Processing PDF for job ' + jobId);
statusUpdate.core.body.status = 'PROCESSING';
const kickOffNotifierWorker = (jobType, filename) => {
return new Promise((resolve, reject) => {
const notifierPath = './workers/notifier.js';
const worker = new Worker(notifierPath,
{workerData: [jobType, jobId, filename]});
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`Notifier Worker stopped with exit code ${code}`));
}
});
});
};
const dropbox = path.join(config.shared.presAnnDropboxDir, jobId);
// General utilities for rendering SVGs resembling Tldraw as much as possible
function align_to_pango(alignment) {
@ -164,7 +154,14 @@ function render_textbox(textColor, font, fontSize, textAlign, text, id, textBoxW
path.join(dropbox, `text${id}.png`),
]);
try {
cp.spawnSync(config.shared.imagemagick, commands, {shell: false});
} catch (error) {
const error_reason = 'ImageMagick failed to render textbox';
logger.error(`${error_reason} in job ${jobId}: ${error.message}`);
statusUpdate.core.body.status = error_reason;
statusUpdate.core.body.error = true;
}
}
function get_gap(dash, size) {
@ -193,17 +190,17 @@ function get_gap(dash, size) {
function get_stroke_width(dash, size) {
switch (size) {
case 'small': if (dash === 'draw') {
return 1;
return 2;
} else {
return 4;
}
case 'medium': if (dash === 'draw') {
return 1.75;
return 3.5;
} else {
return 6.25;
}
case 'large': if (dash === 'draw') {
return 2.5;
return 5;
} else {
return 8.5;
}
@ -236,61 +233,26 @@ function text_size_to_px(size, scale = 1, isStickyNote = false) {
}
}
// Methods based on tldraw's utilities
function getPath(annotationPoints) {
// Gets inner path of a stroke outline
// For solid, dashed, and dotted types
const stroke = getStrokePoints(annotationPoints)
.map((strokePoint) => strokePoint.point);
let [max_x, max_y] = [0, 0];
const inner_path = stroke.reduce(
/**
* Turns an array of points into a path of quadradic curves.
* @param {Array} annotationPoints
* @param {Boolean} closed - whether the path end and start should be connected (default)
* @return {Array} - an SVG quadratic curve path
*/
function getSvgPath(annotationPoints, closed = true) {
const svgPath = annotationPoints.reduce(
(acc, [x0, y0], i, arr) => {
if (!arr[i + 1]) return acc;
const [x1, y1] = arr[i + 1];
if (x1 >= max_x) {
max_x = x1;
}
if (y1 >= max_y) {
max_y = y1;
}
acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2);
acc.push(x0.toFixed(2), y0.toFixed(2), ((x0 + x1) / 2).toFixed(2), ((y0 + y1) / 2).toFixed(2));
return acc;
},
['M', ...stroke[0], 'Q'],
['M', ...annotationPoints[0], 'Q'],
);
return [inner_path, max_x, max_y];
}
function getOutlinePath(annotationPoints) {
// Gets outline of a hand-drawn input, with pressure
const stroke = getStroke(annotationPoints, {
simulatePressure: true,
size: 8,
});
let [max_x, max_y] = [0, 0];
const outline_path = stroke.reduce(
(acc, [x0, y0], i, arr) => {
const [x1, y1] = arr[(i + 1) % arr.length];
if (x1 >= max_x) {
max_x = x1;
}
if (y1 >= max_y) {
max_y = y1;
}
acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2);
return acc;
},
['M', ...stroke[0], 'Q'],
);
outline_path.push('Z');
return [outline_path, max_x, max_y];
if (closed) svgPath.push('Z');
return svgPath;
}
function circleFromThreePoints(A, B, C) {
@ -471,49 +433,94 @@ function overlay_arrow(svg, annotation) {
}
function overlay_draw(svg, annotation) {
const shapePoints = annotation.points;
const shapePointsLength = shapePoints.length;
if (shapePointsLength < 2) return;
const dash = annotation.style.dash;
const [calculated_path, max_x, max_y] = (dash == 'draw') ? getOutlinePath(annotation.points) : getPath(annotation.points);
const isDashDraw = (dash == 'draw');
if (!calculated_path.length) return;
const shapeColor = color_to_hex(annotation.style.color);
const rotation = rad_to_degree(annotation.rotation);
const thickness = get_stroke_width(dash, annotation.style.size);
const gap = get_gap(dash, annotation.style.size);
const [x, y] = annotation.point;
const stroke_dasharray = determine_dasharray(dash, gap);
const fill = (dash === 'draw') ? shapeColor : 'none';
const shapeColor = color_to_hex(annotation.style.color);
const shapeFillColor = color_to_hex(`fill-${annotation.style.color}`);
const shapeTransform = `translate(${x} ${y}), rotate(${rotation} ${max_x / 2} ${max_y / 2})`;
const fill = isDashDraw ? shapeColor : 'none';
// Fill assuming solid, small pencil used
// when path start- and end points overlap
const shapeIsFilled =
const rotation = rad_to_degree(annotation.rotation);
const [x, y] = annotation.point;
const [width, height] = annotation.size;
const shapeTransform = `translate(${x} ${y}), rotate(${rotation} ${width / 2} ${height / 2})`;
const simulatePressure = {
easing: (t) => Math.sin((t * Math.PI) / 2),
simulatePressure: true,
};
const realPressure = {
easing: (t) => t * t,
simulatePressure: false,
};
const options = {
size: 1 + thickness * 1.5,
thinning: 0.65,
streamline: 0.65,
smoothing: 0.65,
...(shapePoints[1][2] === 0.5 ? simulatePressure : realPressure),
last: annotation.isComplete,
};
const strokePoints = getStrokePoints(shapePoints, options);
// Fill when path start- and end points overlap
const isShapeFilled =
annotation.style.isFilled &&
annotation.points.length > 3 &&
shapePointsLength > 3 &&
Math.round(distance(
annotation.points[0][0],
annotation.points[0][1],
annotation.points[annotation.points.length - 1][0],
annotation.points[annotation.points.length - 1][1],
)) <= 2 * get_stroke_width('solid', 'small');
shapePoints[0][0],
shapePoints[0][1],
shapePoints[shapePointsLength - 1][0],
shapePoints[shapePointsLength - 1][1],
)) <= 2 * thickness;
if (shapeIsFilled) {
if (isShapeFilled) {
const shapeArea = strokePoints.map((strokePoint) => strokePoint.point);
svg.ele('path', {
style: `fill:${shapeFillColor};`,
d: getPath(annotation.points)[0] + 'Z',
d: getSvgPath(shapeArea),
transform: shapeTransform,
}).up();
}
if (isDashDraw) {
const strokeOutlinePoints = getStrokeOutlinePoints(strokePoints, options);
const svgPath = getSvgPath(strokeOutlinePoints);
svg.ele('path', {
style: `stroke:${shapeColor};stroke-width:${thickness};fill:${fill};${stroke_dasharray}`,
d: calculated_path,
style: `fill:${fill};${stroke_dasharray}`,
d: svgPath,
transform: shapeTransform,
});
} else {
const last = shapePoints[shapePointsLength - 1];
// Avoid single dots from not being drawn
if (strokePoints[0].point[0] == last[0] && strokePoints[0].point[1] == last[1]) {
strokePoints.push({point: last});
}
const solidPath = strokePoints.map((strokePoint) => strokePoint.point);
const svgPath = getSvgPath(solidPath, false);
svg.ele('path', {
style: `stroke:${shapeColor};stroke-width:${thickness};fill:${fill};${stroke_dasharray}`,
d: svgPath,
transform: shapeTransform,
});
}
}
function overlay_ellipse(svg, annotation) {
@ -603,7 +610,10 @@ function overlay_shape_label(svg, annotation) {
render_textbox(fontColor, font, fontSize, textAlign, text, id);
const dimensions = probe.sync(fs.readFileSync(path.join(dropbox, `text${id}.png`)));
const shape_label = path.join(dropbox, `text${id}.png`);
if (fs.existsSync(shape_label)) {
const dimensions = probe.sync(fs.readFileSync(shape_label));
const labelWidth = dimensions.width / config.process.textScaleFactor;
const labelHeight = dimensions.height / config.process.textScaleFactor;
@ -616,6 +626,7 @@ function overlay_shape_label(svg, annotation) {
'height': labelHeight,
'xlink:href': `file://${dropbox}/text${id}.png`,
}).up();
}
}
function overlay_sticky(svg, annotation) {
@ -712,7 +723,6 @@ function overlay_text(svg, annotation) {
}
function overlay_annotation(svg, currentAnnotation) {
if (currentAnnotation.childIndex >= 1) {
switch (currentAnnotation.type) {
case 'arrow':
overlay_arrow(svg, currentAnnotation);
@ -738,7 +748,6 @@ function overlay_annotation(svg, currentAnnotation) {
default:
logger.info(`Unknown annotation type ${currentAnnotation.type}.`);
}
}
}
function overlay_annotations(svg, currentSlideAnnotations) {
@ -767,20 +776,29 @@ function overlay_annotations(svg, currentSlideAnnotations) {
}
// Process the presentation pages and annotations into a PDF file
async function process_presentation_annotations() {
const client = redis.createClient({
host: config.redis.host,
port: config.redis.port,
password: config.redis.password,
});
// 1. Get the job
const dropbox = path.join(config.shared.presAnnDropboxDir, jobId);
const job = fs.readFileSync(path.join(dropbox, 'job'));
const exportJob = JSON.parse(job);
await client.connect();
// 2. Get the annotations
const annotations = fs.readFileSync(path.join(dropbox, 'whiteboard'));
const whiteboard = JSON.parse(annotations);
const pages = JSON.parse(whiteboard.pages);
const ghostScriptInput = [];
client.on('error', (err) => logger.info('Redis Client Error', err));
// 3. Convert annotations to SVG
for (const currentSlide of pages) {
// 1. Get the job
const job = fs.readFileSync(path.join(dropbox, 'job'));
const exportJob = JSON.parse(job);
// 2. Get the annotations
const annotations = fs.readFileSync(path.join(dropbox, 'whiteboard'));
const whiteboard = JSON.parse(annotations);
const pages = JSON.parse(whiteboard.pages);
const ghostScriptInput = [];
// 3. Convert annotations to SVG
for (const currentSlide of pages) {
const bgImagePath = path.join(dropbox, `slide${currentSlide.page}`);
const svgBackgroundSlide = path.join(exportJob.presLocation,
'svgs', `slide${currentSlide.page}.svg`);
@ -851,30 +869,53 @@ for (const currentSlide of pages) {
'-o', PDFfile,
];
try {
cp.spawnSync(config.shared.cairosvg, convertAnnotatedSlide, {shell: false});
} catch (error) {
logger.error(`Processing slide ${currentSlide.page} failed for job ${jobId}: ${error.message}`);
statusUpdate.core.body.error = true;
}
statusUpdate.core.body.pageNumber = currentSlide.page;
statusUpdate.envelope.timestamp = (new Date()).getTime();
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate));
ghostScriptInput.push(PDFfile);
}
statusUpdate.core.body.error = false;
}
// Create PDF output directory if it doesn't exist
const outputDir = path.join(exportJob.presLocation, 'pdfs', jobId);
// Create PDF output directory if it doesn't exist
const outputDir = path.join(exportJob.presLocation, 'pdfs', jobId);
if (!fs.existsSync(outputDir)) {
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, {recursive: true});
}
}
const filename_with_extension = `${sanitize(exportJob.filename.replace(/\s/g, '_'))}.pdf`;
const filename_with_extension = `${sanitize(exportJob.filename.replace(/\s/g, '_'))}.pdf`;
const mergePDFs = [
const mergePDFs = [
'-dNOPAUSE',
'-sDEVICE=pdfwrite',
`-sOUTPUTFILE="${path.join(outputDir, filename_with_extension)}"`,
`-dBATCH`].concat(ghostScriptInput);
// Resulting PDF file is stored in the presentation dir
cp.spawnSync(config.shared.ghostscript, mergePDFs, {shell: false});
// Resulting PDF file is stored in the presentation dir
try {
cp.spawnSync(config.shared.ghostscript, mergePDFs, {shell: false});
} catch (error) {
const error_reason = 'GhostScript failed to merge PDFs';
logger.error(`${error_reason} in job ${jobId}: ${error.message}`);
statusUpdate.core.body.status = error_reason;
statusUpdate.core.body.error = true;
await client.publish(config.redis.channels.publish, JSON.stringify(statusUpdate));
}
// Launch Notifier Worker depending on job type
logger.info(`Saved PDF at ${outputDir}/${jobId}/${filename_with_extension}`);
// Launch Notifier Worker depending on job type
logger.info(`Saved PDF at ${outputDir}/${jobId}/${filename_with_extension}`);
kickOffNotifierWorker(exportJob.jobType, filename_with_extension);
const notifier = new WorkerStarter({jobType: exportJob.jobType, jobId, filename: filename_with_extension});
notifier.notify();
await client.disconnect();
}
process_presentation_annotations();

Some files were not shown because too many files have changed in this diff Show More