Merge branch 'v2.6.x-release' into ask-before-leave
This commit is contained in:
commit
55ae884f5f
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -14,8 +14,8 @@ trait EndAllBreakoutRoomsMsgHdlr extends RightsManagementTrait {
|
||||
val outGW: OutMsgRouter
|
||||
|
||||
def handleEndAllBreakoutRoomsMsg(msg: EndAllBreakoutRoomsMsg, state: MeetingState2x): MeetingState2x = {
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
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",
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -84,10 +84,12 @@ trait CreateGroupChatReqMsgHdlr extends SystemConfiguration {
|
||||
BbbCoreEnvelope(name, routing)
|
||||
}
|
||||
|
||||
def makeBody(chatId: String,
|
||||
access: String, correlationId: String,
|
||||
createdBy: GroupChatUser, users: Vector[GroupChatUser],
|
||||
msgs: Vector[GroupChatMsgToUser]): GroupChatCreatedEvtMsgBody = {
|
||||
def makeBody(
|
||||
chatId: String,
|
||||
access: String, correlationId: String,
|
||||
createdBy: GroupChatUser, users: Vector[GroupChatUser],
|
||||
msgs: Vector[GroupChatMsgToUser]
|
||||
): GroupChatCreatedEvtMsgBody = {
|
||||
GroupChatCreatedEvtMsgBody(correlationId, chatId, createdBy,
|
||||
access, users, msgs)
|
||||
}
|
||||
|
@ -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 _ =>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ class PadsApp2x(implicit val context: ActorContext)
|
||||
with PadUpdatedSysMsgHdlr
|
||||
with PadContentSysMsgHdlr
|
||||
with PadPatchSysMsgHdlr
|
||||
with PadUpdatePubMsgHdlr {
|
||||
with PadUpdatePubMsgHdlr
|
||||
with PadPinnedReqMsgHdlr {
|
||||
|
||||
}
|
||||
|
@ -45,27 +45,31 @@ trait RespondToPollReqMsgHdlr {
|
||||
bus.outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
for {
|
||||
(pollId: String, updatedPoll: SimplePollResultOutVO) <- Polls.handleRespondToPollReqMsg(msg.header.userId, msg.body.pollId,
|
||||
msg.body.questionId, msg.body.answerIds, liveMeeting)
|
||||
} yield {
|
||||
broadcastPollUpdatedEvent(msg, pollId, updatedPoll)
|
||||
if (Polls.checkUserResponded(msg.body.pollId, msg.header.userId, liveMeeting.polls) == false) {
|
||||
for {
|
||||
poll <- Polls.getPoll(pollId, liveMeeting.polls)
|
||||
(pollId: String, updatedPoll: SimplePollResultOutVO) <- Polls.handleRespondToPollReqMsg(msg.header.userId, msg.body.pollId,
|
||||
msg.body.questionId, msg.body.answerIds, liveMeeting)
|
||||
} yield {
|
||||
broadcastPollUpdatedEvent(msg, pollId, updatedPoll)
|
||||
for {
|
||||
answerId <- msg.body.answerIds
|
||||
poll <- Polls.getPoll(pollId, liveMeeting.polls)
|
||||
} yield {
|
||||
val answerText = poll.questions(0).answers.get(answerId).key
|
||||
broadcastUserRespondedToPollRecordMsg(msg, pollId, answerId, answerText, poll.isSecret)
|
||||
for {
|
||||
answerId <- msg.body.answerIds
|
||||
} yield {
|
||||
val answerText = poll.questions(0).answers.get(answerId).key
|
||||
broadcastUserRespondedToPollRecordMsg(msg, pollId, answerId, answerText, poll.isSecret)
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
presenter <- Users2x.findPresenter(liveMeeting.users2x)
|
||||
} yield {
|
||||
broadcastUserRespondedToPollRespMsg(msg, pollId, msg.body.answerIds, presenter.intId)
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
presenter <- Users2x.findPresenter(liveMeeting.users2x)
|
||||
} yield {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,17 +34,23 @@ trait RespondToTypedPollReqMsgHdlr {
|
||||
bus.outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
for {
|
||||
(pollId: String, updatedPoll: SimplePollResultOutVO) <- Polls.handleRespondToTypedPollReqMsg(msg.header.userId, msg.body.pollId,
|
||||
msg.body.questionId, msg.body.answer, liveMeeting)
|
||||
} yield {
|
||||
broadcastPollUpdatedEvent(msg, pollId, updatedPoll)
|
||||
|
||||
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 {
|
||||
presenter <- Users2x.findPresenter(liveMeeting.users2x)
|
||||
(pollId: String, updatedPoll: SimplePollResultOutVO) <- Polls.handleRespondToTypedPollReqMsg(msg.header.userId, msg.body.pollId,
|
||||
msg.body.questionId, msg.body.answer, liveMeeting)
|
||||
} yield {
|
||||
broadcastUserRespondedToTypedPollRespMsg(msg, pollId, msg.body.answer, presenter.intId)
|
||||
broadcastPollUpdatedEvent(msg, pollId, updatedPoll)
|
||||
|
||||
for {
|
||||
presenter <- Users2x.findPresenter(liveMeeting.users2x)
|
||||
} yield {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlr extends RightsManagementTrait {
|
||||
outGW.send(notifyEvent)
|
||||
|
||||
LockSettingsUtil.enforceCamLockSettingsForAllUsers(liveMeeting, outGW)
|
||||
|
||||
|
||||
// Dial-in
|
||||
def buildLockMessage(meetingId: String, userId: String, lockedBy: String, locked: Boolean): BbbCommonEnvCoreMsg = {
|
||||
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId)
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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 =>
|
||||
val newState = userJoinMeeting(outGW, msg.body.authToken, msg.body.clientType, liveMeeting, state)
|
||||
|
||||
if (liveMeeting.props.meetingProp.isBreakout) {
|
||||
BreakoutHdlrHelpers.updateParentMeetingWithUsers(liveMeeting, eventBus)
|
||||
// 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
|
||||
|
||||
// fresh user joined (not due to reconnection). Clear (pop) the cached voice user
|
||||
VoiceUsers.recoverVoiceUser(liveMeeting.voiceUsers, msg.body.userId)
|
||||
if (!hasReachedMaxParticipants) {
|
||||
val newState = userJoinMeeting(outGW, msg.body.authToken, msg.body.clientType, liveMeeting, state)
|
||||
|
||||
newState
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -158,7 +158,6 @@ class UsersApp(
|
||||
with SelectRandomViewerReqMsgHdlr
|
||||
with AssignPresenterReqMsgHdlr
|
||||
with ChangeUserPinStateReqMsgHdlr
|
||||
with EjectDuplicateUserReqMsgHdlr
|
||||
with EjectUserFromMeetingCmdMsgHdlr
|
||||
with EjectUserFromMeetingSysMsgHdlr
|
||||
with MuteUserCmdMsgHdlr {
|
||||
|
@ -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,
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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] = {
|
||||
|
@ -14,10 +14,12 @@ case class BreakoutRoom2x(
|
||||
users: Vector[BreakoutUser],
|
||||
voiceUsers: Vector[BreakoutVoiceUser],
|
||||
startedOn: Option[Long],
|
||||
started: Boolean
|
||||
started: Boolean,
|
||||
captureNotes: Boolean,
|
||||
captureSlides: Boolean,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
case class BreakoutUser(id: String, name: String)
|
||||
case class BreakoutVoiceUser(id: String, extId: String, voiceUserId: String)
|
||||
case class BreakoutVoiceUser(id: String, extId: String, voiceUserId: String)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,10 +414,13 @@ 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) => {
|
||||
p.addQuestionResponse(questionID, answer)
|
||||
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 => {
|
||||
|
@ -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 {
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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 =>
|
||||
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
||||
@ -448,9 +448,9 @@ class MeetingActor(
|
||||
case m: UserTalkingInVoiceConfEvtMsg =>
|
||||
updateVoiceUserLastActivity(m.body.voiceUserId)
|
||||
handleUserTalkingInVoiceConfEvtMsg(m)
|
||||
case m: VoiceConfCallStateEvtMsg => handleVoiceConfCallStateEvtMsg(m)
|
||||
case m: VoiceConfCallStateEvtMsg => handleVoiceConfCallStateEvtMsg(m)
|
||||
|
||||
case m: RecordingStartedVoiceConfEvtMsg => handleRecordingStartedVoiceConfEvtMsg(m)
|
||||
case m: RecordingStartedVoiceConfEvtMsg => handleRecordingStartedVoiceConfEvtMsg(m)
|
||||
case m: AudioFloorChangedVoiceConfEvtMsg =>
|
||||
handleAudioFloorChangedVoiceConfEvtMsg(m)
|
||||
audioCaptionsApp2x.handle(m, liveMeeting)
|
||||
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
@ -39,14 +41,15 @@ case class WelcomeProp(welcomeMsgTemplate: String, welcomeMsg: String, modOnlyMe
|
||||
case class VoiceProp(telVoice: String, voiceConf: String, dialNumber: String, muteOnStart: Boolean)
|
||||
|
||||
case class UsersProp(
|
||||
maxUsers: Int,
|
||||
webcamsOnlyForModerator: Boolean,
|
||||
userCameraCap: Int,
|
||||
guestPolicy: String,
|
||||
meetingLayout: String,
|
||||
allowModsToUnmuteUsers: Boolean,
|
||||
allowModsToEjectCameras: Boolean,
|
||||
authenticatedGuest: Boolean
|
||||
maxUsers: Int,
|
||||
maxUserConcurrentAccesses:Int,
|
||||
webcamsOnlyForModerator: Boolean,
|
||||
userCameraCap: Int,
|
||||
guestPolicy: String,
|
||||
meetingLayout: String,
|
||||
allowModsToUnmuteUsers: Boolean,
|
||||
allowModsToEjectCameras: Boolean,
|
||||
authenticatedGuest: Boolean
|
||||
)
|
||||
|
||||
case class MetadataProp(metadata: collection.immutable.Map[String, String])
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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 ------------
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
cs = DigestUtils.sha256Hex(data);
|
||||
log.info("SHA256 {}", cs);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,9 @@ 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 Integer duration = 0;
|
||||
private Boolean captureSlides = false;
|
||||
private Boolean captureNotes = false;
|
||||
private Integer duration = 0;
|
||||
private long createdTime = 0;
|
||||
private long startTime = 0;
|
||||
private long endTime = 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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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 {};
|
||||
}
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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,12 +42,37 @@ 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");
|
||||
createdCheckSum = DigestUtils.sha256Hex(data);
|
||||
log.info("SHA256 {}", createdCheckSum);
|
||||
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)) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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.");
|
||||
|
@ -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);
|
||||
|
@ -21,4 +21,5 @@ package org.bigbluebutton.presentation;
|
||||
|
||||
public interface DocumentConversionService {
|
||||
void processDocument(UploadedPresentation pres);
|
||||
void sendDocConversionFailedOnMimeType(UploadedPresentation pres, String fileMime, String fileExtension);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -19,15 +19,25 @@
|
||||
|
||||
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;
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -19,10 +19,15 @@
|
||||
|
||||
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) {
|
||||
/**
|
||||
* Go through each directory and check if it's not empty.
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
|
@ -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,12 +61,15 @@ public class PageToConvert {
|
||||
return pres.getMeetingId();
|
||||
}
|
||||
|
||||
public PageToConvert convert() {
|
||||
public String getMessageErrorInConversion() {
|
||||
return messageErrorInConversion;
|
||||
}
|
||||
|
||||
// Only create SWF files if the configuration requires it
|
||||
if (swfSlidesRequired) {
|
||||
convertPdfToSwf(pres, page, pageFile);
|
||||
}
|
||||
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) {
|
||||
createSvgImages(pres, page);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -1,62 +1,62 @@
|
||||
/**
|
||||
* 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.imp;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.*;
|
||||
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);
|
||||
|
||||
private ExecutorService executor;
|
||||
|
||||
private BlockingQueue<PageToConvert> messages = new LinkedBlockingQueue<PageToConvert>();
|
||||
|
||||
private PresentationConversionCompletionService presentationConversionCompletionService;
|
||||
|
||||
public PdfToSwfSlidesGenerationService(int numConversionThreads) {
|
||||
executor = Executors.newFixedThreadPool(numConversionThreads);
|
||||
}
|
||||
|
||||
public void process(PageToConvert pageToConvert) {
|
||||
Runnable task = new Runnable() {
|
||||
public void run() {
|
||||
pageToConvert.convert();
|
||||
PageConvertProgressMessage msg = new PageConvertProgressMessage(
|
||||
pageToConvert.getPageNumber(),
|
||||
pageToConvert.getPresId(),
|
||||
pageToConvert.getMeetingId(),
|
||||
new ArrayList<>());
|
||||
presentationConversionCompletionService.handle(msg);
|
||||
pageToConvert.getPageFile().delete();
|
||||
}
|
||||
};
|
||||
|
||||
executor.execute(task);
|
||||
}
|
||||
|
||||
public void setPresentationConversionCompletionService(PresentationConversionCompletionService s) {
|
||||
this.presentationConversionCompletionService = s;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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.imp;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.*;
|
||||
import org.bigbluebutton.presentation.messages.PageConvertProgressMessage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class PdfSlidesGenerationService {
|
||||
private static Logger log = LoggerFactory.getLogger(PdfSlidesGenerationService.class);
|
||||
|
||||
private ExecutorService executor;
|
||||
|
||||
private BlockingQueue<PageToConvert> messages = new LinkedBlockingQueue<PageToConvert>();
|
||||
|
||||
private PresentationConversionCompletionService presentationConversionCompletionService;
|
||||
|
||||
public PdfSlidesGenerationService(int numConversionThreads) {
|
||||
executor = Executors.newFixedThreadPool(numConversionThreads);
|
||||
}
|
||||
|
||||
public void process(PageToConvert pageToConvert) {
|
||||
Runnable task = new Runnable() {
|
||||
public void run() {
|
||||
pageToConvert.convert();
|
||||
PageConvertProgressMessage msg = new PageConvertProgressMessage(
|
||||
pageToConvert.getPageNumber(),
|
||||
pageToConvert.getPresId(),
|
||||
pageToConvert.getMeetingId(),
|
||||
new ArrayList<>());
|
||||
presentationConversionCompletionService.handle(msg);
|
||||
pageToConvert.getPageFile().delete();
|
||||
}
|
||||
};
|
||||
|
||||
executor.execute(task);
|
||||
}
|
||||
|
||||
public void setPresentationConversionCompletionService(PresentationConversionCompletionService s) {
|
||||
this.presentationConversionCompletionService = s;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -1,125 +1,152 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
* <p>
|
||||
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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 org.bigbluebutton.api.messaging.messages.PresentationUploadToken;
|
||||
import org.bigbluebutton.api2.IBbbWebApiGWApp;
|
||||
import org.bigbluebutton.presentation.ConversionMessageConstants;
|
||||
import org.bigbluebutton.presentation.GeneratedSlidesInfoHelper;
|
||||
import org.bigbluebutton.presentation.UploadedPresentation;
|
||||
import org.bigbluebutton.presentation.messages.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class SwfSlidesGenerationProgressNotifier {
|
||||
private static Logger log = LoggerFactory.getLogger(SwfSlidesGenerationProgressNotifier.class);
|
||||
|
||||
private IBbbWebApiGWApp messagingService;
|
||||
|
||||
private GeneratedSlidesInfoHelper generatedSlidesInfoHelper;
|
||||
|
||||
|
||||
public void sendDocConversionProgress(IDocConversionMsg msg) {
|
||||
messagingService.sendDocConversionMsg(msg);
|
||||
}
|
||||
|
||||
public void sendUploadFileTooLargeMessage(PresentationUploadToken pres, int uploadedFileSize, int maxUploadFileSize) {
|
||||
UploadFileTooLargeMessage progress = new UploadFileTooLargeMessage(
|
||||
pres.podId,
|
||||
pres.meetingId,
|
||||
pres.filename,
|
||||
pres.authzToken,
|
||||
ConversionMessageConstants.FILE_TOO_LARGE,
|
||||
uploadedFileSize,
|
||||
maxUploadFileSize);
|
||||
messagingService.sendDocConversionMsg(progress);
|
||||
}
|
||||
|
||||
public void sendConversionUpdateMessage(int slidesCompleted, UploadedPresentation pres, int pageGenerated) {
|
||||
DocPageGeneratedProgress progress = new DocPageGeneratedProgress(pres.getPodId(),
|
||||
pres.getMeetingId(),
|
||||
pres.getId(),
|
||||
pres.getId(),
|
||||
pres.getName(),
|
||||
"notUsedYet",
|
||||
"notUsedYet",
|
||||
pres.isDownloadable(),
|
||||
pres.isRemovable(),
|
||||
ConversionMessageConstants.GENERATED_SLIDE_KEY,
|
||||
pres.getNumberOfPages(),
|
||||
slidesCompleted,
|
||||
generateBasePresUrl(pres),
|
||||
pageGenerated,
|
||||
(pageGenerated == 1));
|
||||
messagingService.sendDocConversionMsg(progress);
|
||||
}
|
||||
|
||||
public void sendCreatingThumbnailsUpdateMessage(UploadedPresentation pres) {
|
||||
DocConversionProgress progress = new DocConversionProgress(pres.getPodId(), pres.getMeetingId(),
|
||||
pres.getId(), pres.getId(),
|
||||
pres.getName(), "notUsedYet", "notUsedYet",
|
||||
pres.isDownloadable(), pres.isRemovable(), ConversionMessageConstants.GENERATING_THUMBNAIL_KEY,
|
||||
pres.getTemporaryPresentationId());
|
||||
messagingService.sendDocConversionMsg(progress);
|
||||
}
|
||||
|
||||
public void sendConversionCompletedMessage(UploadedPresentation pres) {
|
||||
if (generatedSlidesInfoHelper == null) {
|
||||
log.error("GeneratedSlidesInfoHelper was not set. Could not notify interested listeners.");
|
||||
return;
|
||||
}
|
||||
|
||||
DocPageCompletedProgress progress = new DocPageCompletedProgress(pres.getPodId(), pres.getMeetingId(),
|
||||
pres.getId(), pres.getTemporaryPresentationId(), pres.getId(),
|
||||
pres.getName(), "notUsedYet", "notUsedYet",
|
||||
pres.isDownloadable(), pres.isRemovable(), ConversionMessageConstants.CONVERSION_COMPLETED_KEY,
|
||||
pres.getNumberOfPages(), generateBasePresUrl(pres), pres.isCurrent());
|
||||
messagingService.sendDocConversionMsg(progress);
|
||||
}
|
||||
|
||||
private String generateBasePresUrl(UploadedPresentation pres) {
|
||||
return pres.getBaseUrl() + "/" + pres.getMeetingId() + "/" + pres.getMeetingId() + "/" + pres.getId();
|
||||
}
|
||||
|
||||
public void setMessagingService(IBbbWebApiGWApp m) {
|
||||
messagingService = m;
|
||||
}
|
||||
|
||||
public void setGeneratedSlidesInfoHelper(GeneratedSlidesInfoHelper helper) {
|
||||
generatedSlidesInfoHelper = helper;
|
||||
}
|
||||
|
||||
public void sendCreatingTextFilesUpdateMessage(UploadedPresentation pres) {
|
||||
DocConversionProgress progress = new DocConversionProgress(pres.getPodId(), pres.getMeetingId(),
|
||||
pres.getId(), pres.getId(),
|
||||
pres.getName(), "notUsedYet", "notUsedYet",
|
||||
pres.isDownloadable(), pres.isRemovable(), ConversionMessageConstants.GENERATING_TEXTFILES_KEY,
|
||||
pres.getTemporaryPresentationId());
|
||||
messagingService.sendDocConversionMsg(progress);
|
||||
}
|
||||
|
||||
public void sendCreatingSvgImagesUpdateMessage(UploadedPresentation pres) {
|
||||
DocConversionProgress progress = new DocConversionProgress(pres.getPodId(), pres.getMeetingId(),
|
||||
pres.getId(), pres.getId(),
|
||||
pres.getName(), "notUsedYet", "notUsedYet",
|
||||
pres.isDownloadable(), pres.isRemovable(), ConversionMessageConstants.GENERATING_SVGIMAGES_KEY,
|
||||
pres.getTemporaryPresentationId());
|
||||
messagingService.sendDocConversionMsg(progress);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
* <p>
|
||||
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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 org.bigbluebutton.api.messaging.messages.PresentationUploadToken;
|
||||
import org.bigbluebutton.api2.IBbbWebApiGWApp;
|
||||
import org.bigbluebutton.presentation.ConversionMessageConstants;
|
||||
import org.bigbluebutton.presentation.GeneratedSlidesInfoHelper;
|
||||
import org.bigbluebutton.presentation.UploadedPresentation;
|
||||
import org.bigbluebutton.presentation.messages.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class SlidesGenerationProgressNotifier {
|
||||
private static Logger log = LoggerFactory.getLogger(SlidesGenerationProgressNotifier.class);
|
||||
|
||||
private IBbbWebApiGWApp messagingService;
|
||||
private int maxNumberOfAttempts = 3;
|
||||
private GeneratedSlidesInfoHelper generatedSlidesInfoHelper;
|
||||
|
||||
|
||||
public void sendDocConversionProgress(IDocConversionMsg msg) {
|
||||
messagingService.sendDocConversionMsg(msg);
|
||||
}
|
||||
|
||||
public void sendUploadFileTooLargeMessage(PresentationUploadToken pres, int uploadedFileSize, int maxUploadFileSize) {
|
||||
UploadFileTooLargeMessage progress = new UploadFileTooLargeMessage(
|
||||
pres.podId,
|
||||
pres.meetingId,
|
||||
pres.filename,
|
||||
pres.authzToken,
|
||||
ConversionMessageConstants.FILE_TOO_LARGE,
|
||||
uploadedFileSize,
|
||||
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(),
|
||||
pres.getMeetingId(),
|
||||
pres.getId(),
|
||||
pres.getId(),
|
||||
pres.getName(),
|
||||
"notUsedYet",
|
||||
"notUsedYet",
|
||||
pres.isDownloadable(),
|
||||
pres.isRemovable(),
|
||||
ConversionMessageConstants.GENERATED_SLIDE_KEY,
|
||||
pres.getNumberOfPages(),
|
||||
slidesCompleted,
|
||||
generateBasePresUrl(pres),
|
||||
pageGenerated,
|
||||
(pageGenerated == 1));
|
||||
messagingService.sendDocConversionMsg(progress);
|
||||
}
|
||||
|
||||
public void sendCreatingThumbnailsUpdateMessage(UploadedPresentation pres) {
|
||||
DocConversionProgress progress = new DocConversionProgress(pres.getPodId(), pres.getMeetingId(),
|
||||
pres.getId(), pres.getId(),
|
||||
pres.getName(), "notUsedYet", "notUsedYet",
|
||||
pres.isDownloadable(), pres.isRemovable(), ConversionMessageConstants.GENERATING_THUMBNAIL_KEY,
|
||||
pres.getTemporaryPresentationId());
|
||||
messagingService.sendDocConversionMsg(progress);
|
||||
}
|
||||
|
||||
public void sendConversionCompletedMessage(UploadedPresentation pres) {
|
||||
if (generatedSlidesInfoHelper == null) {
|
||||
log.error("GeneratedSlidesInfoHelper was not set. Could not notify interested listeners.");
|
||||
return;
|
||||
}
|
||||
|
||||
DocPageCompletedProgress progress = new DocPageCompletedProgress(pres.getPodId(), pres.getMeetingId(),
|
||||
pres.getId(), pres.getTemporaryPresentationId(), pres.getId(),
|
||||
pres.getName(), "notUsedYet", "notUsedYet",
|
||||
pres.isDownloadable(), pres.isRemovable(), ConversionMessageConstants.CONVERSION_COMPLETED_KEY,
|
||||
pres.getNumberOfPages(), generateBasePresUrl(pres), pres.isCurrent());
|
||||
messagingService.sendDocConversionMsg(progress);
|
||||
}
|
||||
|
||||
private String generateBasePresUrl(UploadedPresentation pres) {
|
||||
return pres.getBaseUrl() + "/" + pres.getMeetingId() + "/" + pres.getMeetingId() + "/" + pres.getId();
|
||||
}
|
||||
|
||||
public void setMessagingService(IBbbWebApiGWApp m) {
|
||||
messagingService = m;
|
||||
}
|
||||
|
||||
public void setMaxNumberOfAttempts(int maxNumberOfAttempts) {
|
||||
this.maxNumberOfAttempts = maxNumberOfAttempts;
|
||||
}
|
||||
|
||||
public void setGeneratedSlidesInfoHelper(GeneratedSlidesInfoHelper helper) {
|
||||
generatedSlidesInfoHelper = helper;
|
||||
}
|
||||
|
||||
public void sendCreatingTextFilesUpdateMessage(UploadedPresentation pres) {
|
||||
DocConversionProgress progress = new DocConversionProgress(pres.getPodId(), pres.getMeetingId(),
|
||||
pres.getId(), pres.getId(),
|
||||
pres.getName(), "notUsedYet", "notUsedYet",
|
||||
pres.isDownloadable(), pres.isRemovable(), ConversionMessageConstants.GENERATING_TEXTFILES_KEY,
|
||||
pres.getTemporaryPresentationId());
|
||||
messagingService.sendDocConversionMsg(progress);
|
||||
}
|
||||
|
||||
public void sendCreatingSvgImagesUpdateMessage(UploadedPresentation pres) {
|
||||
DocConversionProgress progress = new DocConversionProgress(pres.getPodId(), pres.getMeetingId(),
|
||||
pres.getId(), pres.getId(),
|
||||
pres.getName(), "notUsedYet", "notUsedYet",
|
||||
pres.isDownloadable(), pres.isRemovable(), ConversionMessageConstants.GENERATING_SVGIMAGES_KEY,
|
||||
pres.getTemporaryPresentationId());
|
||||
messagingService.sendDocConversionMsg(progress);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
if(detectFontType3tHandler.isCommandTimeout()) {
|
||||
log.error("Command execution (detectFontType3) exceeded the {} secs timeout for {} page {}.", convPdfToSvgTimeout, pres.getName(), page);
|
||||
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.");
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -102,9 +102,11 @@ 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,
|
||||
))
|
||||
|
||||
|
||||
}
|
||||
|
||||
def handleRecordingStatusChangedEvtMsg(msg: RecordingStatusChangedEvtMsg): Unit = {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
32
bbb-export-annotations/lib/utils/worker-starter.js
Normal file
32
bbb-export-annotations/lib/utils/worker-starter.js
Normal 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);
|
||||
};
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
];
|
||||
|
||||
cp.spawnSync(config.shared.pdftocairo, extract_png_from_pdf, {shell: false});
|
||||
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}`);
|
||||
}
|
||||
|
@ -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));
|
||||
|
||||
const res = await axios.post(callbackUrl, formData,
|
||||
{headers: formData.getHeaders()});
|
||||
logger.info(`Upload of job ${exportJob.jobId} returned ${res.data}`);
|
||||
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}`);
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user