Merge remote-tracking branch 'upstream/develop' into PR-11359
This commit is contained in:
commit
1cc066366f
@ -11,7 +11,7 @@ stages:
|
||||
|
||||
# define which docker image to use for builds
|
||||
default:
|
||||
image: gitlab.senfcall.de:5050/senfcall-public/docker-bbb-build:v2021-08-10
|
||||
image: gitlab.senfcall.de:5050/senfcall-public/docker-bbb-build:v2021-10-17
|
||||
|
||||
# This stage uses git to find out since when each package has been unmodified.
|
||||
# it then checks an API endpoint on the package server to find out for which of
|
||||
|
@ -1,5 +1,8 @@
|
||||
package org.bigbluebutton.core.apps
|
||||
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
|
||||
import org.bigbluebutton.core2.message.senders.MsgBuilder
|
||||
|
||||
object ScreenshareModel {
|
||||
def resetDesktopSharingParams(status: ScreenshareModel) = {
|
||||
status.broadcastingRTMP = false
|
||||
@ -88,6 +91,24 @@ object ScreenshareModel {
|
||||
def getHasAudio(status: ScreenshareModel): Boolean = {
|
||||
status.hasAudio
|
||||
}
|
||||
|
||||
def stop(outGW: OutMsgRouter, liveMeeting: LiveMeeting): Unit = {
|
||||
if (isBroadcastingRTMP(liveMeeting.screenshareModel)) {
|
||||
this.resetDesktopSharingParams(liveMeeting.screenshareModel)
|
||||
|
||||
val event = MsgBuilder.buildStopScreenshareRtmpBroadcastEvtMsg(
|
||||
liveMeeting.props.meetingProp.intId,
|
||||
getVoiceConf(liveMeeting.screenshareModel),
|
||||
getScreenshareConf(liveMeeting.screenshareModel),
|
||||
getRTMPBroadcastingUrl(liveMeeting.screenshareModel),
|
||||
getScreenshareVideoWidth(liveMeeting.screenshareModel),
|
||||
getScreenshareVideoHeight(liveMeeting.screenshareModel),
|
||||
getTimestamp(liveMeeting.screenshareModel)
|
||||
)
|
||||
outGW.send(event)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ScreenshareModel {
|
||||
|
@ -1,9 +1,9 @@
|
||||
package org.bigbluebutton.core.apps.externalvideo
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait, ExternalVideoModel }
|
||||
import org.bigbluebutton.core.apps.{ ExternalVideoModel, PermissionCheck, RightsManagementTrait, ScreenshareModel }
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting }
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
|
||||
trait StartExternalVideoPubMsgHdlr extends RightsManagementTrait {
|
||||
this: ExternalVideoApp2x =>
|
||||
@ -28,6 +28,10 @@ trait StartExternalVideoPubMsgHdlr extends RightsManagementTrait {
|
||||
val reason = "You need to be the presenter to start external videos"
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
|
||||
} else {
|
||||
|
||||
//Stop ScreenShare if it's running
|
||||
ScreenshareModel.stop(bus.outGW, liveMeeting)
|
||||
|
||||
ExternalVideoModel.setURL(liveMeeting.externalVideoModel, msg.body.externalVideoUrl)
|
||||
broadcastEvent(msg)
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
package org.bigbluebutton.core.apps.externalvideo
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait, ExternalVideoModel }
|
||||
import org.bigbluebutton.core.apps.{ ExternalVideoModel, PermissionCheck, RightsManagementTrait }
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.running.{ LiveMeeting }
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
import org.bigbluebutton.core2.message.senders.MsgBuilder
|
||||
|
||||
trait StopExternalVideoPubMsgHdlr extends RightsManagementTrait {
|
||||
this: ExternalVideoApp2x =>
|
||||
@ -11,25 +12,16 @@ trait StopExternalVideoPubMsgHdlr extends RightsManagementTrait {
|
||||
def handle(msg: StopExternalVideoPubMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
|
||||
log.info("Received StopExternalVideoPubMsgr meetingId={}", liveMeeting.props.meetingProp.intId)
|
||||
|
||||
def broadcastEvent() {
|
||||
|
||||
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, liveMeeting.props.meetingProp.intId, "nodeJSapp")
|
||||
val envelope = BbbCoreEnvelope(StopExternalVideoEvtMsg.NAME, routing)
|
||||
val header = BbbClientMsgHeader(StopExternalVideoEvtMsg.NAME, liveMeeting.props.meetingProp.intId, msg.header.userId)
|
||||
|
||||
val body = StopExternalVideoEvtMsgBody()
|
||||
val event = StopExternalVideoEvtMsg(header, body)
|
||||
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
|
||||
bus.outGW.send(msgEvent)
|
||||
}
|
||||
|
||||
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 stop external video"
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
|
||||
} else {
|
||||
ExternalVideoModel.clear(liveMeeting.externalVideoModel)
|
||||
broadcastEvent()
|
||||
|
||||
//broadcastEvent
|
||||
val msgEvent = MsgBuilder.buildStopExternalVideoEvtMsg(liveMeeting.props.meetingProp.intId, msg.header.userId)
|
||||
bus.outGW.send(msgEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,32 +4,13 @@ import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.apps.ScreenshareModel
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
import org.bigbluebutton.core2.message.senders.MsgBuilder
|
||||
|
||||
trait ScreenshareRtmpBroadcastStoppedVoiceConfEvtMsgHdlr {
|
||||
this: ScreenshareApp2x =>
|
||||
|
||||
def handle(msg: ScreenshareRtmpBroadcastStoppedVoiceConfEvtMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
|
||||
|
||||
def broadcastEvent(voiceConf: String, screenshareConf: String,
|
||||
stream: String, vidWidth: Int, vidHeight: Int,
|
||||
timestamp: String): BbbCommonEnvCoreMsg = {
|
||||
|
||||
val routing = Routing.addMsgToClientRouting(
|
||||
MessageTypes.BROADCAST_TO_MEETING,
|
||||
liveMeeting.props.meetingProp.intId, "not-used"
|
||||
)
|
||||
val envelope = BbbCoreEnvelope(ScreenshareRtmpBroadcastStoppedEvtMsg.NAME, routing)
|
||||
val header = BbbClientMsgHeader(
|
||||
ScreenshareRtmpBroadcastStoppedEvtMsg.NAME,
|
||||
liveMeeting.props.meetingProp.intId, "not-used"
|
||||
)
|
||||
|
||||
val body = ScreenshareRtmpBroadcastStoppedEvtMsgBody(voiceConf, screenshareConf,
|
||||
stream, vidWidth, vidHeight, timestamp)
|
||||
val event = ScreenshareRtmpBroadcastStoppedEvtMsg(header, body)
|
||||
BbbCommonEnvCoreMsg(envelope, event)
|
||||
}
|
||||
|
||||
log.info("handleScreenshareRTMPBroadcastStoppedRequest: isBroadcastingRTMP=" +
|
||||
ScreenshareModel.isBroadcastingRTMP(liveMeeting.screenshareModel) + " URL:" +
|
||||
ScreenshareModel.getRTMPBroadcastingUrl(liveMeeting.screenshareModel))
|
||||
@ -40,8 +21,12 @@ trait ScreenshareRtmpBroadcastStoppedVoiceConfEvtMsgHdlr {
|
||||
ScreenshareModel.broadcastingRTMPStopped(liveMeeting.screenshareModel)
|
||||
|
||||
// notify viewers that RTMP broadcast stopped
|
||||
val msgEvent = broadcastEvent(msg.body.voiceConf, msg.body.screenshareConf, msg.body.stream,
|
||||
msg.body.vidWidth, msg.body.vidHeight, msg.body.timestamp)
|
||||
val msgEvent = MsgBuilder.buildStopScreenshareRtmpBroadcastEvtMsg(
|
||||
liveMeeting.props.meetingProp.intId,
|
||||
msg.body.voiceConf, msg.body.screenshareConf, msg.body.stream,
|
||||
msg.body.vidWidth, msg.body.vidHeight, msg.body.timestamp
|
||||
)
|
||||
|
||||
bus.outGW.send(msgEvent)
|
||||
} else {
|
||||
log.info("STOP broadcast NOT ALLOWED when isBroadcastingRTMP=false")
|
||||
|
@ -12,7 +12,16 @@ trait ChangeUserEmojiCmdMsgHdlr extends RightsManagementTrait {
|
||||
val outGW: OutMsgRouter
|
||||
|
||||
def handleChangeUserEmojiCmdMsg(msg: ChangeUserEmojiCmdMsg) {
|
||||
if (msg.header.userId != msg.body.userId && permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
|
||||
// Usually only moderators are allowed to change someone else's emoji status
|
||||
// Exceptional case: Viewers who are presenter are allowed to lower someone else's raised hand:
|
||||
val isViewerProhibitedFromLoweringOthersHand =
|
||||
!(Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId).get.emoji.equals("raiseHand") &&
|
||||
msg.body.emoji.equals("none")) ||
|
||||
permissionFailed(PermissionCheck.VIEWER_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)
|
||||
|
||||
if (msg.header.userId != msg.body.userId &&
|
||||
permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId) &&
|
||||
isViewerProhibitedFromLoweringOthersHand) {
|
||||
val meetingId = liveMeeting.props.meetingProp.intId
|
||||
val reason = "No permission to clear change user emoji status."
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)
|
||||
|
@ -339,7 +339,8 @@ class MeetingActor(
|
||||
state = state.update(tracker)
|
||||
}
|
||||
} else {
|
||||
if (state.expiryTracker.moderatorHasJoined == true) {
|
||||
if (state.expiryTracker.moderatorHasJoined == true &&
|
||||
state.expiryTracker.lastModeratorLeftOnInMs == 0) {
|
||||
log.info("All moderators have left. Setting setLastModeratorLeftOn(). meetingId=" + props.meetingProp.intId)
|
||||
val tracker = state.expiryTracker.setLastModeratorLeftOn(TimeUtil.timeNowInMs())
|
||||
state = state.update(tracker)
|
||||
|
@ -160,17 +160,32 @@ object MsgBuilder {
|
||||
BbbCommonEnvCoreMsg(envelope, event)
|
||||
}
|
||||
|
||||
def buildStopExternalVideoEvtMsg(meetingId: String): BbbCommonEnvCoreMsg = {
|
||||
def buildStopExternalVideoEvtMsg(meetingId: String, userId: String = "not-used"): BbbCommonEnvCoreMsg = {
|
||||
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, "nodeJSapp")
|
||||
val envelope = BbbCoreEnvelope(StopExternalVideoEvtMsg.NAME, routing)
|
||||
|
||||
val body = StopExternalVideoEvtMsgBody()
|
||||
val header = BbbClientMsgHeader(StopExternalVideoEvtMsg.NAME, meetingId, "not-used")
|
||||
val header = BbbClientMsgHeader(StopExternalVideoEvtMsg.NAME, meetingId, userId)
|
||||
val event = StopExternalVideoEvtMsg(header, body)
|
||||
|
||||
BbbCommonEnvCoreMsg(envelope, event)
|
||||
}
|
||||
|
||||
def buildStopScreenshareRtmpBroadcastEvtMsg(
|
||||
meetingId: String,
|
||||
voiceConf: String, screenshareConf: String,
|
||||
stream: String, vidWidth: Int, vidHeight: Int,
|
||||
timestamp: String
|
||||
): BbbCommonEnvCoreMsg = {
|
||||
|
||||
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, "not-used")
|
||||
val envelope = BbbCoreEnvelope(ScreenshareRtmpBroadcastStoppedEvtMsg.NAME, routing)
|
||||
val header = BbbClientMsgHeader(ScreenshareRtmpBroadcastStoppedEvtMsg.NAME, meetingId, "not-used")
|
||||
val body = ScreenshareRtmpBroadcastStoppedEvtMsgBody(voiceConf, screenshareConf, stream, vidWidth, vidHeight, timestamp)
|
||||
val event = ScreenshareRtmpBroadcastStoppedEvtMsg(header, body)
|
||||
BbbCommonEnvCoreMsg(envelope, event)
|
||||
}
|
||||
|
||||
def buildMeetingCreatedEvtMsg(meetingId: String, props: DefaultProps): BbbCommonEnvCoreMsg = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
|
||||
val envelope = BbbCoreEnvelope(MeetingCreatedEvtMsg.NAME, routing)
|
||||
|
@ -419,29 +419,36 @@ public class ParamsProcessorUtil {
|
||||
}
|
||||
}
|
||||
|
||||
boolean learningDashboardEn = learningDashboardEnabled;
|
||||
if (!StringUtils.isEmpty(params.get(ApiParams.LEARNING_DASHBOARD_ENABLED))) {
|
||||
try {
|
||||
learningDashboardEn = Boolean.parseBoolean(params
|
||||
.get(ApiParams.LEARNING_DASHBOARD_ENABLED));
|
||||
} catch (Exception ex) {
|
||||
log.warn(
|
||||
"Invalid param [learningDashboardEnabled] for meeting=[{}]",
|
||||
internalMeetingId);
|
||||
boolean learningDashboardEn = false;
|
||||
int learningDashboardCleanupMins = 0;
|
||||
|
||||
// Learning Dashboard not allowed for Breakout Rooms
|
||||
if(!isBreakout) {
|
||||
learningDashboardEn = learningDashboardEnabled;
|
||||
if (!StringUtils.isEmpty(params.get(ApiParams.LEARNING_DASHBOARD_ENABLED))) {
|
||||
try {
|
||||
learningDashboardEn = Boolean.parseBoolean(params
|
||||
.get(ApiParams.LEARNING_DASHBOARD_ENABLED));
|
||||
} catch (Exception ex) {
|
||||
log.warn(
|
||||
"Invalid param [learningDashboardEnabled] for meeting=[{}]",
|
||||
internalMeetingId);
|
||||
}
|
||||
}
|
||||
|
||||
learningDashboardCleanupMins = learningDashboardCleanupDelayInMinutes;
|
||||
if (!StringUtils.isEmpty(params.get(ApiParams.LEARNING_DASHBOARD_CLEANUP_DELAY_IN_MINUTES))) {
|
||||
try {
|
||||
learningDashboardCleanupMins = Integer.parseInt(params
|
||||
.get(ApiParams.LEARNING_DASHBOARD_CLEANUP_DELAY_IN_MINUTES));
|
||||
} catch (Exception ex) {
|
||||
log.warn(
|
||||
"Invalid param [learningDashboardCleanupDelayInMinutes] for meeting=[{}]",
|
||||
internalMeetingId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int learningDashboardCleanupMins = learningDashboardCleanupDelayInMinutes;
|
||||
if (!StringUtils.isEmpty(params.get(ApiParams.LEARNING_DASHBOARD_CLEANUP_DELAY_IN_MINUTES))) {
|
||||
try {
|
||||
learningDashboardCleanupMins = Integer.parseInt(params
|
||||
.get(ApiParams.LEARNING_DASHBOARD_CLEANUP_DELAY_IN_MINUTES));
|
||||
} catch (Exception ex) {
|
||||
log.warn(
|
||||
"Invalid param [learningDashboardCleanupDelayInMinutes] for meeting=[{}]",
|
||||
internalMeetingId);
|
||||
}
|
||||
}
|
||||
|
||||
//Generate token to access Activity Report
|
||||
String learningDashboardAccessToken = "";
|
||||
|
@ -15,7 +15,8 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
@Retention(RUNTIME)
|
||||
public @interface GetChecksumConstraint {
|
||||
|
||||
String message() default "Invalid checksum: checksums do not match";
|
||||
String key() default "checksumError";
|
||||
String message() default "Checksums do not match";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
@Retention(RUNTIME)
|
||||
public @interface GuestPolicyConstraint {
|
||||
|
||||
String key() default "guestDeny";
|
||||
String message() default "User denied access for this session";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
|
@ -15,7 +15,8 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
@Retention(RUNTIME)
|
||||
public @interface IsBooleanConstraint {
|
||||
|
||||
String message() default "Validation error: value must be a boolean";
|
||||
String key() default "validationError";
|
||||
String message() default "Value must be a boolean";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
|
@ -15,7 +15,8 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
@Retention(RUNTIME)
|
||||
public @interface IsIntegralConstraint {
|
||||
|
||||
String message() default "Validation error: value must be an integral number";
|
||||
String key() default "validationError";
|
||||
String message() default "Value must be an integral number";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
@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 {};
|
||||
|
@ -15,7 +15,8 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
@Retention(RUNTIME)
|
||||
public @interface MeetingEndedConstraint {
|
||||
|
||||
String message() default "You can not re-join a meeting that has already been forcibly ended";
|
||||
String key() default "meetingForciblyEnded";
|
||||
String message() default "You can not join a meeting that has already been forcibly ended";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
|
@ -16,7 +16,8 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
@Retention(RUNTIME)
|
||||
public @interface MeetingExistsConstraint {
|
||||
|
||||
String message() default "Invalid meeting ID: A meeting with that ID does not exist";
|
||||
String key() default "notFound";
|
||||
String message() default "A meeting with that ID does not exist";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
|
@ -2,16 +2,13 @@ package org.bigbluebutton.api.model.constraint;
|
||||
|
||||
import javax.validation.Constraint;
|
||||
import javax.validation.Payload;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.Pattern;
|
||||
import javax.validation.constraints.Size;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.FIELD;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@NotEmpty(message = "You must provide a meeting ID")
|
||||
@NotEmpty(key = "missingParamMeetingID", message = "You must provide a meeting ID")
|
||||
@Size(min = 2, max = 256, message = "Meeting ID must be between 2 and 256 characters")
|
||||
@Pattern(regexp = "^[^,]+$", message = "Meeting ID cannot contain ','")
|
||||
@Constraint(validatedBy = {})
|
||||
@ -19,6 +16,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
@Retention(RUNTIME)
|
||||
public @interface MeetingIDConstraint {
|
||||
|
||||
String key() default "validationError";
|
||||
String message() default "Invalid meeting ID";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
|
@ -2,23 +2,21 @@ package org.bigbluebutton.api.model.constraint;
|
||||
|
||||
import javax.validation.Constraint;
|
||||
import javax.validation.Payload;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.Pattern;
|
||||
import javax.validation.constraints.Size;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.FIELD;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@NotNull(message = "You must provide a meeting name")
|
||||
@NotEmpty(message = "You must provide a meeting name")
|
||||
@Size(min = 2, max = 256, message = "Meeting name must be between 2 and 256 characters")
|
||||
//@Pattern(regexp = "^[^,]+$", message = "Meeting name cannot contain ','")
|
||||
@Constraint(validatedBy = {})
|
||||
@Target(FIELD)
|
||||
@Retention(RUNTIME)
|
||||
public @interface MeetingNameConstraint {
|
||||
|
||||
String key() default "validationError";
|
||||
String message() default "Invalid meeting name";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
|
@ -15,7 +15,8 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
@Retention(RUNTIME)
|
||||
public @interface ModeratorPasswordConstraint {
|
||||
|
||||
String message() default "Invalid password: The supplied moderator password is incorrect";
|
||||
String key() default "invalidPassword";
|
||||
String message() default "The supplied moderator password is incorrect";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
|
@ -0,0 +1,20 @@
|
||||
package org.bigbluebutton.api.model.constraint;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.list.NotEmptyList;
|
||||
import org.bigbluebutton.api.model.validator.NotEmptyValidator;
|
||||
|
||||
import javax.validation.Constraint;
|
||||
import javax.validation.Payload;
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Constraint(validatedBy = NotEmptyValidator.class)
|
||||
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Repeatable(NotEmptyList.class)
|
||||
public @interface NotEmpty {
|
||||
|
||||
String key() default "emptyError";
|
||||
String message() default "Field must contain a value";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package org.bigbluebutton.api.model.constraint;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.list.NotNullList;
|
||||
import org.bigbluebutton.api.model.validator.NotNullValidator;
|
||||
|
||||
import javax.validation.Constraint;
|
||||
import javax.validation.Payload;
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Repeatable(NotNullList.class)
|
||||
@Constraint(validatedBy = NotNullValidator.class)
|
||||
public @interface NotNull {
|
||||
|
||||
String key() default "nullError";
|
||||
String message() default "Value cannot be null";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
@ -2,21 +2,19 @@ package org.bigbluebutton.api.model.constraint;
|
||||
|
||||
import javax.validation.Constraint;
|
||||
import javax.validation.Payload;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.Size;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.FIELD;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@NotEmpty(message = "You must provide your password")
|
||||
@Size(min = 2, max = 64, message = "Password must be between 8 and 20 characters")
|
||||
@Constraint(validatedBy = {})
|
||||
@Target(FIELD)
|
||||
@Retention(RUNTIME)
|
||||
public @interface PasswordConstraint {
|
||||
|
||||
String key() default "invalidPassword";
|
||||
String message() default "Invalid password";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
|
@ -0,0 +1,42 @@
|
||||
package org.bigbluebutton.api.model.constraint;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.list.PatternList;
|
||||
import org.bigbluebutton.api.model.validator.PatternValidator;
|
||||
|
||||
import javax.validation.Constraint;
|
||||
import javax.validation.Payload;
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Repeatable(PatternList.class)
|
||||
@Constraint(validatedBy = PatternValidator.class)
|
||||
public @interface Pattern {
|
||||
|
||||
String regexp();
|
||||
Flag[] flags() default {};
|
||||
String key() default "validationError";
|
||||
String message() default "Value contains invalid characters";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
|
||||
enum Flag {
|
||||
UNIX_LINES(1),
|
||||
CASE_INSENSITIVE(2),
|
||||
COMMENTS(4),
|
||||
MULTILINE(8),
|
||||
DOTALL(32),
|
||||
UNICODE_CASE(64),
|
||||
CANON_EQ(128);
|
||||
|
||||
private final int value;
|
||||
|
||||
private Flag(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
}
|
@ -15,7 +15,8 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
@Retention(RUNTIME)
|
||||
public @interface PostChecksumConstraint {
|
||||
|
||||
String message() default "Invalid checksum: checksums do not match";
|
||||
String key() default "checksumError";
|
||||
String message() default "Checksums do not match";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
|
@ -0,0 +1,22 @@
|
||||
package org.bigbluebutton.api.model.constraint;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.list.SizeList;
|
||||
import org.bigbluebutton.api.model.validator.SizeValidator;
|
||||
|
||||
import javax.validation.Constraint;
|
||||
import javax.validation.Payload;
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Repeatable(SizeList.class)
|
||||
@Constraint(validatedBy = SizeValidator.class)
|
||||
public @interface Size {
|
||||
|
||||
String key() default "sizeError";
|
||||
String message() default "Value does not conform to size restrictions";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
int min() default 0;
|
||||
int max() default 2147483647;
|
||||
}
|
@ -10,11 +10,13 @@ import java.lang.annotation.Target;
|
||||
import static java.lang.annotation.ElementType.FIELD;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@NotNull(key = "missingToken", message = "You must provide a session token")
|
||||
@Constraint(validatedBy = { UserSessionValidator.class })
|
||||
@Target(FIELD)
|
||||
@Retention(RUNTIME)
|
||||
public @interface UserSessionConstraint {
|
||||
|
||||
String key() default "missingSession";
|
||||
String message() default "Invalid session token";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
|
@ -0,0 +1,14 @@
|
||||
package org.bigbluebutton.api.model.constraint.list;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.NotEmpty;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface NotEmptyList {
|
||||
NotEmpty[] value();
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package org.bigbluebutton.api.model.constraint.list;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.NotNull;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface NotNullList {
|
||||
NotNull[] value();
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package org.bigbluebutton.api.model.constraint.list;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.Pattern;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface PatternList {
|
||||
Pattern[] value();
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package org.bigbluebutton.api.model.constraint.list;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.Size;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface SizeList {
|
||||
Size[] value();
|
||||
}
|
@ -31,8 +31,7 @@ public class CreateMeeting extends RequestWithChecksum<CreateMeeting.Params> {
|
||||
@MeetingIDConstraint
|
||||
private String meetingID;
|
||||
|
||||
//@NotEmpty(message = "You must provide a voice bridge")
|
||||
@IsIntegralConstraint(message = "Voice bridge must be a 5-digit integral value")
|
||||
@IsIntegralConstraint(message = "Voice bridge must be an integral value")
|
||||
private String voiceBridgeString;
|
||||
private Integer voiceBridge;
|
||||
|
||||
|
@ -2,6 +2,7 @@ package org.bigbluebutton.api.model.request;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.MeetingExistsConstraint;
|
||||
import org.bigbluebutton.api.model.constraint.MeetingIDConstraint;
|
||||
import org.bigbluebutton.api.model.constraint.NotEmpty;
|
||||
import org.bigbluebutton.api.model.constraint.PasswordConstraint;
|
||||
import org.bigbluebutton.api.model.shared.Checksum;
|
||||
import org.bigbluebutton.api.model.shared.ModeratorPassword;
|
||||
@ -27,6 +28,7 @@ public class EndMeeting extends RequestWithChecksum<EndMeeting.Params> {
|
||||
private String meetingID;
|
||||
|
||||
@PasswordConstraint
|
||||
@NotEmpty(message = "You must provide the moderator password")
|
||||
private String password;
|
||||
|
||||
@Valid
|
||||
|
@ -17,9 +17,7 @@ public class Enter implements Request<Enter.Params> {
|
||||
public String getValue() { return value; }
|
||||
}
|
||||
|
||||
@NotNull(message = "You must provide a session token")
|
||||
@UserSessionConstraint
|
||||
//@MaxParticipantsConstraint
|
||||
@GuestPolicyConstraint
|
||||
private String sessionToken;
|
||||
|
||||
|
@ -21,9 +21,7 @@ public class GuestWait implements Request<GuestWait.Params> {
|
||||
public String getValue() { return value; }
|
||||
}
|
||||
|
||||
@NotNull(message = "You must provide the session token")
|
||||
@UserSessionConstraint
|
||||
// @MaxParticipantsConstraint
|
||||
private String sessionToken;
|
||||
|
||||
@MeetingExistsConstraint
|
||||
|
@ -3,7 +3,6 @@ package org.bigbluebutton.api.model.request;
|
||||
import org.bigbluebutton.api.model.constraint.*;
|
||||
import org.bigbluebutton.api.model.shared.Checksum;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import java.util.Map;
|
||||
|
||||
public class JoinMeeting extends RequestWithChecksum<JoinMeeting.Params> {
|
||||
@ -25,16 +24,17 @@ public class JoinMeeting extends RequestWithChecksum<JoinMeeting.Params> {
|
||||
}
|
||||
|
||||
@MeetingIDConstraint
|
||||
@MeetingExistsConstraint
|
||||
@MeetingExistsConstraint(key = "invalidMeetingIdentifier")
|
||||
@MeetingEndedConstraint
|
||||
private String meetingID;
|
||||
|
||||
private String userID;
|
||||
|
||||
@NotEmpty(message = "You must provide your name")
|
||||
@NotEmpty(key = "missingParamFullName", message = "You must provide your name")
|
||||
private String fullName;
|
||||
|
||||
@PasswordConstraint
|
||||
@NotEmpty(key = "invalidPassword", message = "You must provide either the moderator or attendee password")
|
||||
private String password;
|
||||
|
||||
@IsBooleanConstraint(message = "Guest must be a boolean value (true or false)")
|
||||
|
@ -2,9 +2,9 @@ package org.bigbluebutton.api.model.request;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.MeetingExistsConstraint;
|
||||
import org.bigbluebutton.api.model.constraint.MeetingIDConstraint;
|
||||
import org.bigbluebutton.api.model.constraint.NotEmpty;
|
||||
import org.bigbluebutton.api.model.shared.Checksum;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import java.util.Map;
|
||||
|
||||
public class SetPollXML extends RequestWithChecksum<SetPollXML.Params> {
|
||||
@ -21,10 +21,10 @@ public class SetPollXML extends RequestWithChecksum<SetPollXML.Params> {
|
||||
}
|
||||
|
||||
@MeetingIDConstraint
|
||||
@MeetingExistsConstraint
|
||||
@MeetingExistsConstraint(key = "invalidMeetingIdentifier")
|
||||
private String meetingID;
|
||||
|
||||
@NotEmpty(message = "You must provide the poll")
|
||||
@NotEmpty(key = "configXMLError", message = "You did not pass a poll XML")
|
||||
private String pollXML;
|
||||
|
||||
public SetPollXML(Checksum checksum) {
|
||||
|
@ -17,7 +17,6 @@ public class SignOut implements Request<SignOut.Params> {
|
||||
public String getValue() { return value; }
|
||||
}
|
||||
|
||||
@NotNull(message = "You must provide a session token")
|
||||
@UserSessionConstraint
|
||||
private String sessionToken;
|
||||
|
||||
|
@ -3,7 +3,6 @@ package org.bigbluebutton.api.model.request;
|
||||
import org.bigbluebutton.api.model.constraint.*;
|
||||
import org.bigbluebutton.api.service.SessionService;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Map;
|
||||
|
||||
public class Stuns implements Request<Stuns.Params> {
|
||||
@ -18,7 +17,6 @@ public class Stuns implements Request<Stuns.Params> {
|
||||
public String getValue() { return value; }
|
||||
}
|
||||
|
||||
@NotNull(message = "You must provide a session token")
|
||||
@UserSessionConstraint
|
||||
private String sessionToken;
|
||||
|
||||
|
@ -1,15 +1,14 @@
|
||||
package org.bigbluebutton.api.model.shared;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.NotEmpty;
|
||||
import org.bigbluebutton.api.util.ParamsUtil;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
public abstract class Checksum {
|
||||
|
||||
@NotEmpty(message = "You must provide the API call")
|
||||
@NotEmpty(message = "You must provide the API call", groups = ChecksumValidationGroup.class)
|
||||
protected String apiCall;
|
||||
|
||||
@NotEmpty(message = "You must provide the checksum")
|
||||
@NotEmpty(key = "checksumError", message = "You must provide the checksum", groups = ChecksumValidationGroup.class)
|
||||
protected String checksum;
|
||||
|
||||
protected String queryStringWithoutChecksum;
|
||||
|
@ -0,0 +1,4 @@
|
||||
package org.bigbluebutton.api.model.shared;
|
||||
|
||||
public interface ChecksumValidationGroup {
|
||||
}
|
@ -5,7 +5,7 @@ import org.bigbluebutton.api.util.ParamsUtil;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
@GetChecksumConstraint(message = "Checksums do not match")
|
||||
@GetChecksumConstraint(groups = ChecksumValidationGroup.class)
|
||||
public class GetChecksum extends Checksum {
|
||||
|
||||
@NotEmpty(message = "You must provide the query string")
|
||||
|
@ -1,15 +1,11 @@
|
||||
package org.bigbluebutton.api.model.shared;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.PostChecksumConstraint;
|
||||
import org.bigbluebutton.api.service.ValidationService;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
@PostChecksumConstraint(message = "Checksums do not match")
|
||||
@PostChecksumConstraint(groups = ChecksumValidationGroup.class)
|
||||
public class PostChecksum extends Checksum {
|
||||
|
||||
Map<String, String[]> params;
|
||||
@ -17,48 +13,7 @@ public class PostChecksum extends Checksum {
|
||||
public PostChecksum(String apiCall, String checksum, Map<String, String[]> params) {
|
||||
super(apiCall, checksum);
|
||||
this.params = params;
|
||||
buildQueryStringFromParamsMap();
|
||||
}
|
||||
|
||||
private void buildQueryStringFromParamsMap() {
|
||||
StringBuilder queryString = new StringBuilder();
|
||||
SortedSet<String> keys = new TreeSet<>(params.keySet());
|
||||
|
||||
boolean firstParam = true;
|
||||
for(String key: keys) {
|
||||
|
||||
if(key.equals("checksum")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for(String value: params.get(key)) {
|
||||
if(firstParam) {
|
||||
firstParam = false;
|
||||
} else {
|
||||
queryString.append("&");
|
||||
}
|
||||
|
||||
queryString.append(key);
|
||||
queryString.append("=");
|
||||
|
||||
String encodedValue = encodeString(value);
|
||||
queryString.append(encodedValue);
|
||||
}
|
||||
}
|
||||
|
||||
queryStringWithoutChecksum = queryString.toString();
|
||||
}
|
||||
|
||||
private String encodeString(String stringToEncode) {
|
||||
String encodedResult;
|
||||
|
||||
try {
|
||||
encodedResult = URLEncoder.encode(stringToEncode, StandardCharsets.UTF_8.name());
|
||||
} catch(UnsupportedEncodingException ex) {
|
||||
encodedResult = stringToEncode;
|
||||
}
|
||||
|
||||
return encodedResult;
|
||||
queryStringWithoutChecksum = ValidationService.buildQueryStringFromParamsMap(params);
|
||||
}
|
||||
|
||||
public Map<String, String[]> getParams() { return params; }
|
||||
|
@ -0,0 +1,18 @@
|
||||
package org.bigbluebutton.api.model.validator;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.NotEmpty;
|
||||
|
||||
import javax.validation.ConstraintValidator;
|
||||
import javax.validation.ConstraintValidatorContext;
|
||||
|
||||
public class NotEmptyValidator implements ConstraintValidator<NotEmpty, String> {
|
||||
|
||||
@Override
|
||||
public void initialize(NotEmpty constraintAnnotation) {}
|
||||
|
||||
@Override
|
||||
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
|
||||
if(s == null) return true;
|
||||
return !s.isEmpty();
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package org.bigbluebutton.api.model.validator;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.NotNull;
|
||||
|
||||
import javax.validation.ConstraintValidator;
|
||||
import javax.validation.ConstraintValidatorContext;
|
||||
|
||||
public class NotNullValidator implements ConstraintValidator<NotNull, Object> {
|
||||
|
||||
@Override
|
||||
public void initialize(NotNull constraintAnnotation) {}
|
||||
|
||||
@Override
|
||||
public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
|
||||
return !(o == null);
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package org.bigbluebutton.api.model.validator;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.Pattern;
|
||||
|
||||
import javax.validation.ConstraintValidator;
|
||||
import javax.validation.ConstraintValidatorContext;
|
||||
|
||||
public class PatternValidator implements ConstraintValidator<Pattern, String> {
|
||||
|
||||
String regexp;
|
||||
|
||||
@Override
|
||||
public void initialize(Pattern constraintAnnotation) {
|
||||
regexp = constraintAnnotation.regexp();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
|
||||
if(s == null) return true;
|
||||
return java.util.regex.Pattern.matches(regexp, s);
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package org.bigbluebutton.api.model.validator;
|
||||
|
||||
import org.bigbluebutton.api.model.constraint.Size;
|
||||
|
||||
import javax.validation.ConstraintValidator;
|
||||
import javax.validation.ConstraintValidatorContext;
|
||||
|
||||
public class SizeValidator implements ConstraintValidator<Size, String> {
|
||||
|
||||
private int min;
|
||||
private int max;
|
||||
|
||||
@Override
|
||||
public void initialize(Size constraintAnnotation) {
|
||||
min = constraintAnnotation.min();
|
||||
max = constraintAnnotation.max();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
|
||||
if(s == null) return true;
|
||||
return (s.length() >= min && s.length() <= max);
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package org.bigbluebutton.api.service;
|
||||
|
||||
import org.bigbluebutton.api.model.request.*;
|
||||
import org.bigbluebutton.api.model.shared.Checksum;
|
||||
import org.bigbluebutton.api.model.shared.ChecksumValidationGroup;
|
||||
import org.bigbluebutton.api.model.shared.GetChecksum;
|
||||
import org.bigbluebutton.api.model.shared.PostChecksum;
|
||||
import org.bigbluebutton.api.util.ParamsUtil;
|
||||
@ -12,6 +13,9 @@ import javax.validation.ConstraintViolation;
|
||||
import javax.validation.Validation;
|
||||
import javax.validation.Validator;
|
||||
import javax.validation.ValidatorFactory;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
public class ValidationService {
|
||||
@ -60,16 +64,16 @@ public class ValidationService {
|
||||
validator = validatorFactory.getValidator();
|
||||
}
|
||||
|
||||
public Set<String> validate(ApiCall apiCall, Map<String, String[]> params, String queryString) {
|
||||
public Map<String, String> validate(ApiCall apiCall, Map<String, String[]> params, String queryString) {
|
||||
log.info("Validating {} request with query string {}", apiCall.getName(), queryString);
|
||||
|
||||
params = sanitizeParams(params);
|
||||
|
||||
Request request = initializeRequest(apiCall, params, queryString);
|
||||
Set<String> violations = new HashSet<>();
|
||||
Map<String,String> violations = new HashMap<>();
|
||||
|
||||
if(request == null) {
|
||||
violations.add("validationError: Request not recognized");
|
||||
violations.put("validationError", "Request not recognized");
|
||||
} else {
|
||||
request.populateFromParamsMap(params);
|
||||
violations = performValidation(request);
|
||||
@ -87,6 +91,10 @@ public class ValidationService {
|
||||
checksumValue = params.get("checksum")[0];
|
||||
}
|
||||
|
||||
if(queryString == null || queryString.isEmpty()) {
|
||||
queryString = buildQueryStringFromParamsMap(params);
|
||||
}
|
||||
|
||||
switch(apiCall.requestType) {
|
||||
case GET:
|
||||
checksum = new GetChecksum(apiCall.getName(), checksumValue, queryString);
|
||||
@ -137,19 +145,44 @@ public class ValidationService {
|
||||
return request;
|
||||
}
|
||||
|
||||
private <R extends Request> Set<String> performValidation(R classToValidate) {
|
||||
Set<ConstraintViolation<R>> violations = validator.validate(classToValidate);
|
||||
Set<String> violationSet = new HashSet<>();
|
||||
private <R extends Request> Map<String, String> performValidation(R classToValidate) {
|
||||
Set<ConstraintViolation<R>> violations = validator.validate(classToValidate, ChecksumValidationGroup.class);
|
||||
|
||||
for(ConstraintViolation<R> violation: violations) {
|
||||
violationSet.add(violation.getMessage());
|
||||
if(violations.isEmpty()) {
|
||||
violations = validator.validate(classToValidate);
|
||||
}
|
||||
|
||||
if(violationSet.isEmpty()) {
|
||||
return buildViolationsMap(classToValidate, violations);
|
||||
}
|
||||
|
||||
private <R extends Request> Map<String, String> buildViolationsMap(R classToValidate, Set<ConstraintViolation<R>> violations) {
|
||||
Map<String, String> violationMap = new HashMap<>();
|
||||
|
||||
for(ConstraintViolation<R> violation: violations) {
|
||||
Map<String, Object> attributes = violation.getConstraintDescriptor().getAttributes();
|
||||
String key;
|
||||
String message;
|
||||
|
||||
if(attributes.containsKey("key")) {
|
||||
key = (String) attributes.get("key");
|
||||
} else {
|
||||
key = "validationError";
|
||||
}
|
||||
|
||||
if(attributes.containsKey("message")) {
|
||||
message = (String) attributes.get("message");
|
||||
} else {
|
||||
message = "An unknown validation error occurred";
|
||||
}
|
||||
|
||||
violationMap.put(key, message);
|
||||
}
|
||||
|
||||
if(violationMap.isEmpty()) {
|
||||
classToValidate.convertParamsFromString();
|
||||
}
|
||||
|
||||
return violationSet;
|
||||
return violationMap;
|
||||
}
|
||||
|
||||
private Map<String, String[]> sanitizeParams(Map<String, String[]> params) {
|
||||
@ -192,6 +225,47 @@ public class ValidationService {
|
||||
return mapString.toString();
|
||||
}
|
||||
|
||||
public static String buildQueryStringFromParamsMap(Map<String, String[]> params) {
|
||||
StringBuilder queryString = new StringBuilder();
|
||||
SortedSet<String> keys = new TreeSet<>(params.keySet());
|
||||
|
||||
boolean firstParam = true;
|
||||
for(String key: keys) {
|
||||
|
||||
if(key.equals("checksum")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for(String value: params.get(key)) {
|
||||
if(firstParam) {
|
||||
firstParam = false;
|
||||
} else {
|
||||
queryString.append("&");
|
||||
}
|
||||
|
||||
queryString.append(key);
|
||||
queryString.append("=");
|
||||
|
||||
String encodedValue = encodeString(value);
|
||||
queryString.append(encodedValue);
|
||||
}
|
||||
}
|
||||
|
||||
return queryString.toString();
|
||||
}
|
||||
|
||||
private static String encodeString(String stringToEncode) {
|
||||
String encodedResult;
|
||||
|
||||
try {
|
||||
encodedResult = URLEncoder.encode(stringToEncode, StandardCharsets.UTF_8.name());
|
||||
} catch(UnsupportedEncodingException ex) {
|
||||
encodedResult = stringToEncode;
|
||||
}
|
||||
|
||||
return encodedResult;
|
||||
}
|
||||
|
||||
public void setSecuritySalt(String securitySalt) { this.securitySalt = securitySalt; }
|
||||
public String getSecuritySalt() { return securitySalt; }
|
||||
|
||||
|
@ -67,6 +67,10 @@ public class PageToConvert {
|
||||
return pres.getId();
|
||||
}
|
||||
|
||||
public String getMeetingId() {
|
||||
return pres.getMeetingId();
|
||||
}
|
||||
|
||||
public PageToConvert convert() {
|
||||
|
||||
// Only create SWF files if the configuration requires it
|
||||
|
@ -1,13 +1,13 @@
|
||||
/**
|
||||
* 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.
|
||||
@ -46,6 +46,7 @@ public class PdfToSwfSlidesGenerationService {
|
||||
PageConvertProgressMessage msg = new PageConvertProgressMessage(
|
||||
pageToConvert.getPageNumber(),
|
||||
pageToConvert.getPresId(),
|
||||
pageToConvert.getMeetingId(),
|
||||
new ArrayList<>());
|
||||
presentationConversionCompletionService.handle(msg);
|
||||
pageToConvert.getPageFile().delete();
|
||||
|
@ -36,11 +36,16 @@ public class PresentationConversionCompletionService {
|
||||
if (msg instanceof PresentationConvertMessage) {
|
||||
PresentationConvertMessage m = (PresentationConvertMessage) msg;
|
||||
PresentationToConvert p = new PresentationToConvert(m.pres);
|
||||
presentationsToConvert.put(p.getKey(), p);
|
||||
} else if (msg instanceof PageConvertProgressMessage) {
|
||||
|
||||
String presentationToConvertKey = p.getKey() + "_" + m.pres.getMeetingId();
|
||||
|
||||
presentationsToConvert.put(presentationToConvertKey, p);
|
||||
} else if (msg instanceof PageConvertProgressMessage) {
|
||||
PageConvertProgressMessage m = (PageConvertProgressMessage) msg;
|
||||
PresentationToConvert p = presentationsToConvert.get(m.presId);
|
||||
|
||||
String presentationToConvertKey = m.presId + "_" + m.meetingId;
|
||||
|
||||
PresentationToConvert p = presentationsToConvert.get(presentationToConvertKey);
|
||||
if (p != null) {
|
||||
p.incrementPagesCompleted();
|
||||
notifier.sendConversionUpdateMessage(p.getPagesCompleted(), p.pres, m.page);
|
||||
@ -52,7 +57,9 @@ public class PresentationConversionCompletionService {
|
||||
}
|
||||
|
||||
private void handleEndProcessing(PresentationToConvert p) {
|
||||
presentationsToConvert.remove(p.getKey());
|
||||
String presentationToConvertKey = p.getKey() + "_" + p.pres.getMeetingId();
|
||||
|
||||
presentationsToConvert.remove(presentationToConvertKey);
|
||||
|
||||
Map<String, Object> logData = new HashMap<String, Object>();
|
||||
logData = new HashMap<String, Object>();
|
||||
|
@ -7,9 +7,11 @@ public class PageConvertProgressMessage implements IPresentationCompletionMessag
|
||||
public final String presId;
|
||||
public final int page;
|
||||
public final List<String> errors;
|
||||
public final String meetingId;
|
||||
|
||||
public PageConvertProgressMessage(int page, String presId, List<String> errors) {
|
||||
public PageConvertProgressMessage(int page, String presId, String meetingId, List<String> errors) {
|
||||
this.presId = presId;
|
||||
this.meetingId = meetingId;
|
||||
this.page = page;
|
||||
this.errors = errors;
|
||||
}
|
||||
|
29634
bbb-learning-dashboard/package-lock.json
generated
29634
bbb-learning-dashboard/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -36,3 +36,17 @@
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.col-text-left {
|
||||
text-align: left;
|
||||
}
|
||||
[dir="rtl"] .col-text-left {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.col-text-right {
|
||||
text-align: right;
|
||||
}
|
||||
[dir="rtl"] .col-text-right {
|
||||
text-align: left;
|
||||
}
|
||||
|
@ -6,29 +6,38 @@ import Card from './components/Card';
|
||||
import UsersTable from './components/UsersTable';
|
||||
import StatusTable from './components/StatusTable';
|
||||
import PollsTable from './components/PollsTable';
|
||||
import ErrorMessage from './components/ErrorMessage';
|
||||
|
||||
class App extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: true,
|
||||
activitiesJson: {},
|
||||
tab: 'overview',
|
||||
meetingId: '',
|
||||
learningDashboardAccessToken: '',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchActivitiesJson();
|
||||
this.setDashboardParams();
|
||||
setInterval(() => {
|
||||
this.fetchActivitiesJson();
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
fetchActivitiesJson() {
|
||||
setDashboardParams() {
|
||||
let learningDashboardAccessToken = '';
|
||||
let meetingId = '';
|
||||
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
const params = Object.fromEntries(urlSearchParams.entries());
|
||||
if (typeof params.meeting === 'undefined') return;
|
||||
|
||||
let learningDashboardAccessToken = '';
|
||||
if (typeof params.meeting !== 'undefined') {
|
||||
meetingId = params.meeting;
|
||||
}
|
||||
|
||||
if (typeof params.report !== 'undefined') {
|
||||
learningDashboardAccessToken = params.report;
|
||||
} else {
|
||||
@ -38,19 +47,39 @@ class App extends React.Component {
|
||||
cArr.forEach((val) => {
|
||||
if (val.indexOf(`${cookieName}=`) === 0) learningDashboardAccessToken = val.substring((`${cookieName}=`).length);
|
||||
});
|
||||
|
||||
// Extend AccessToken lifetime by 30d (in each access)
|
||||
if (learningDashboardAccessToken !== '') {
|
||||
const cookieExpiresDate = new Date();
|
||||
cookieExpiresDate.setTime(cookieExpiresDate.getTime() + (3600000 * 24 * 30));
|
||||
document.cookie = `learningDashboardAccessToken-${meetingId}=${learningDashboardAccessToken}; expires=${cookieExpiresDate.toGMTString()}; path=/;SameSite=None;Secure`;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ learningDashboardAccessToken, meetingId }, this.fetchActivitiesJson);
|
||||
}
|
||||
|
||||
fetchActivitiesJson() {
|
||||
const { learningDashboardAccessToken, meetingId } = this.state;
|
||||
|
||||
if (learningDashboardAccessToken !== '') {
|
||||
fetch(`${params.meeting}/${learningDashboardAccessToken}/learning_dashboard_data.json`)
|
||||
fetch(`${meetingId}/${learningDashboardAccessToken}/learning_dashboard_data.json`)
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
this.setState({ activitiesJson: json });
|
||||
this.setState({ activitiesJson: json, loading: false });
|
||||
document.title = `Learning Dashboard - ${json.name}`;
|
||||
}).catch(() => {
|
||||
this.setState({ loading: false });
|
||||
});
|
||||
} else {
|
||||
this.setState({ loading: false });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { activitiesJson, tab } = this.state;
|
||||
const {
|
||||
activitiesJson, tab, learningDashboardAccessToken, loading,
|
||||
} = this.state;
|
||||
const { intl } = this.props;
|
||||
|
||||
document.title = `${intl.formatMessage({ id: 'app.learningDashboard.dashboardTitle', defaultMessage: 'Learning Dashboard' })} - ${activitiesJson.name}`;
|
||||
@ -132,6 +161,15 @@ class App extends React.Component {
|
||||
return meetingAveragePoints;
|
||||
}
|
||||
|
||||
function getErrorMessage() {
|
||||
if (learningDashboardAccessToken === '') {
|
||||
return intl.formatMessage({ id: 'app.learningDashboard.errors.invalidToken', defaultMessage: 'Invalid session token' });
|
||||
}
|
||||
return intl.formatMessage({ id: 'app.learningDashboard.errors.dataUnavailable', defaultMessage: 'Data is no longer available' });
|
||||
}
|
||||
|
||||
if (loading === false && typeof activitiesJson.name === 'undefined') return <ErrorMessage message={getErrorMessage()} />;
|
||||
|
||||
return (
|
||||
<div className="mx-10">
|
||||
<div className="flex items-start justify-between pb-3">
|
||||
@ -140,27 +178,35 @@ class App extends React.Component {
|
||||
<br />
|
||||
<span className="text-sm font-medium">{activitiesJson.name || ''}</span>
|
||||
</h1>
|
||||
<div className="mt-3 text-right px-4 py-1 text-gray-500 inline-block">
|
||||
<div className="mt-3 col-text-right py-1 text-gray-500 inline-block">
|
||||
<p className="font-bold">
|
||||
<FormattedDate
|
||||
value={activitiesJson.createdOn}
|
||||
year="numeric"
|
||||
month="short"
|
||||
day="numeric"
|
||||
/>
|
||||
<div className="inline">
|
||||
<FormattedDate
|
||||
value={activitiesJson.createdOn}
|
||||
year="numeric"
|
||||
month="short"
|
||||
day="numeric"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{
|
||||
activitiesJson.endedOn > 0
|
||||
? (
|
||||
<span className="px-2 py-1 ml-3 font-semibold leading-tight text-red-700 bg-red-100 rounded-full">
|
||||
<FormattedMessage id="app.learningDashboard.indicators.meetingStatusEnded" defaultMessage="Ended" />
|
||||
</span>
|
||||
)
|
||||
: (
|
||||
<span className="px-2 py-1 ml-3 font-semibold leading-tight text-green-700 bg-green-100 rounded-full">
|
||||
<FormattedMessage id="app.learningDashboard.indicators.meetingStatusActive" defaultMessage="Active" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
activitiesJson.endedOn > 0
|
||||
? (
|
||||
<span className="px-2 py-1 font-semibold leading-tight text-red-700 bg-red-100 rounded-full">
|
||||
<FormattedMessage id="app.learningDashboard.indicators.meetingStatusEnded" defaultMessage="Ended" />
|
||||
</span>
|
||||
)
|
||||
: null
|
||||
}
|
||||
{
|
||||
activitiesJson.endedOn === 0
|
||||
? (
|
||||
<span className="px-2 py-1 font-semibold leading-tight text-green-700 bg-green-100 rounded-full">
|
||||
<FormattedMessage id="app.learningDashboard.indicators.meetingStatusActive" defaultMessage="Active" />
|
||||
</span>
|
||||
)
|
||||
: null
|
||||
}
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage id="app.learningDashboard.indicators.duration" defaultMessage="Duration" />
|
||||
@ -175,8 +221,8 @@ class App extends React.Component {
|
||||
<Card
|
||||
name={
|
||||
activitiesJson.endedOn === 0
|
||||
? intl.formatMessage({ id: 'app.learningDashboard.indicators.participantsOnline', defaultMessage: 'Active Participants' })
|
||||
: intl.formatMessage({ id: 'app.learningDashboard.indicators.participantsTotal', defaultMessage: 'Total Number Of Participants' })
|
||||
? intl.formatMessage({ id: 'app.learningDashboard.indicators.usersOnline', defaultMessage: 'Active Users' })
|
||||
: intl.formatMessage({ id: 'app.learningDashboard.indicators.usersTotal', defaultMessage: 'Total Number Of Users' })
|
||||
}
|
||||
number={Object.values(activitiesJson.users || {})
|
||||
.filter((u) => activitiesJson.endedOn > 0 || u.leftOn === 0).length}
|
||||
@ -283,7 +329,7 @@ class App extends React.Component {
|
||||
</div>
|
||||
<h1 className="block my-1 pr-2 text-xl font-semibold">
|
||||
{ tab === 'overview' || tab === 'overview_activityscore'
|
||||
? <FormattedMessage id="app.learningDashboard.participantsTable.title" defaultMessage="Overview" />
|
||||
? <FormattedMessage id="app.learningDashboard.usersTable.title" defaultMessage="Overview" />
|
||||
: null }
|
||||
{ tab === 'status_timeline'
|
||||
? <FormattedMessage id="app.learningDashboard.statusTimelineTable.title" defaultMessage="Status Timeline" />
|
||||
|
22
bbb-learning-dashboard/src/components/ErrorMessage.jsx
Normal file
22
bbb-learning-dashboard/src/components/ErrorMessage.jsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
|
||||
function ErrorMessage(props) {
|
||||
const { message } = props;
|
||||
|
||||
return (
|
||||
<div className="container flex flex-col items-center px-6 mx-auto">
|
||||
<svg className="w-12 h-12 my-8 text-gray-700" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M13.477 14.89A6 6 0 015.11 6.524l8.367 8.368zm1.414-1.414L6.524 5.11a6 6 0 018.367 8.367zM18 10a8 8 0 11-16 0 8 8 0 0116 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<h1 className="text-xl font-semibold text-gray-700 dark:text-gray-200">
|
||||
{message}
|
||||
</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorMessage;
|
@ -15,11 +15,11 @@ class PollsTable extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="w-full whitespace-no-wrap">
|
||||
<table className="w-full whitespace-nowrap">
|
||||
<thead>
|
||||
<tr className="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b bg-gray-100">
|
||||
<tr className="text-xs font-semibold tracking-wide col-text-left text-gray-500 uppercase border-b bg-gray-100">
|
||||
<th className="px-4 py-3">
|
||||
<FormattedMessage id="app.learningDashboard.pollsTable.colParticipant" defaultMessage="Participant" />
|
||||
<FormattedMessage id="app.learningDashboard.user" defaultMessage="User" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4 inline"
|
||||
@ -39,13 +39,21 @@ class PollsTable extends React.Component {
|
||||
{ typeof allUsers === 'object' && Object.values(allUsers || {}).length > 0 ? (
|
||||
Object.values(allUsers || {})
|
||||
.filter((user) => Object.values(user.answers).length > 0)
|
||||
.sort((a, b) => {
|
||||
if (a.isModerator === false && b.isModerator === true) return 1;
|
||||
if (a.isModerator === true && b.isModerator === false) return -1;
|
||||
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
|
||||
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
|
||||
return 0;
|
||||
})
|
||||
.map((user) => (
|
||||
<tr className="text-gray-700">
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center text-sm">
|
||||
<div className="relative hidden w-8 h-8 mr-3 rounded-full md:block">
|
||||
<div className="relative hidden w-8 h-8 rounded-full md:block">
|
||||
<UserAvatar user={user} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="font-semibold">{user.name}</p>
|
||||
</div>
|
||||
|
@ -29,11 +29,11 @@ class StatusTable extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="w-full whitespace-no-wrap">
|
||||
<table className="w-full whitespace-nowrap">
|
||||
<thead>
|
||||
<tr className="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b bg-gray-100">
|
||||
<th className="px-4 py-3">
|
||||
<FormattedMessage id="app.learningDashboard.statusTimelineTable.colParticipant" defaultMessage="Participant" />
|
||||
<tr className="text-xs font-semibold tracking-wide text-gray-500 uppercase border-b bg-gray-100">
|
||||
<th className="px-4 py-3 col-text-left">
|
||||
<FormattedMessage id="app.learningDashboard.user" defaultMessage="User" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4 inline"
|
||||
@ -44,19 +44,27 @@ class StatusTable extends React.Component {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 13l-5 5m0 0l-5-5m5 5V6" />
|
||||
</svg>
|
||||
</th>
|
||||
{ periods.map((period) => <th className="px-4 py-3 text-left">{ `${tsToHHmmss(period - firstRegisteredOnTime)}` }</th>) }
|
||||
{ periods.map((period) => <th className="px-4 py-3 col-text-left">{ `${tsToHHmmss(period - firstRegisteredOnTime)}` }</th>) }
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y">
|
||||
{ typeof allUsers === 'object' && Object.values(allUsers || {}).length > 0 ? (
|
||||
Object.values(allUsers || {})
|
||||
.sort((a, b) => {
|
||||
if (a.isModerator === false && b.isModerator === true) return 1;
|
||||
if (a.isModerator === true && b.isModerator === false) return -1;
|
||||
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
|
||||
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
|
||||
return 0;
|
||||
})
|
||||
.map((user) => (
|
||||
<tr className="text-gray-700">
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center text-sm">
|
||||
<div className="relative hidden w-8 h-8 mr-3 rounded-full md:block">
|
||||
<div className="relative hidden w-8 h-8 rounded-full md:block">
|
||||
<UserAvatar user={user} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="font-semibold">{user.name}</p>
|
||||
</div>
|
||||
@ -68,7 +76,7 @@ class StatusTable extends React.Component {
|
||||
period,
|
||||
period + spanMinutes);
|
||||
return (
|
||||
<td className="px-4 py-3 text-sm text-left">
|
||||
<td className="px-4 py-3 text-sm col-text-left">
|
||||
{
|
||||
user.registeredOn > period && user.registeredOn < period + spanMinutes
|
||||
? (
|
||||
|
@ -6,11 +6,30 @@ import { getUserEmojisSummary, emojiConfigs } from '../services/EmojiService';
|
||||
import UserAvatar from './UserAvatar';
|
||||
|
||||
class UsersTable extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
activityscoreOrder: 'desc',
|
||||
};
|
||||
}
|
||||
|
||||
toggleActivityScoreOrder() {
|
||||
const { activityscoreOrder } = this.state;
|
||||
|
||||
if (activityscoreOrder === 'asc') {
|
||||
this.setState({ activityscoreOrder: 'desc' });
|
||||
} else {
|
||||
this.setState({ activityscoreOrder: 'asc' });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
allUsers, totalOfActivityTime, totalOfPolls, tab,
|
||||
} = this.props;
|
||||
|
||||
const { activityscoreOrder } = this.state;
|
||||
|
||||
function getSumOfTime(eventsArr) {
|
||||
return eventsArr.reduce((prevVal, elem) => {
|
||||
if (elem.stoppedOn > 0) return prevVal + (elem.stoppedOn - elem.startedOn);
|
||||
@ -82,11 +101,11 @@ class UsersTable extends React.Component {
|
||||
});
|
||||
|
||||
return (
|
||||
<table className="w-full whitespace-no-wrap">
|
||||
<table className="w-full whitespace-nowrap">
|
||||
<thead>
|
||||
<tr className="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b bg-gray-100">
|
||||
<th className="px-4 py-3">
|
||||
<FormattedMessage id="app.learningDashboard.participantsTable.colParticipant" defaultMessage="Participant" />
|
||||
<th className="px-4 py-3 col-text-left">
|
||||
<FormattedMessage id="app.learningDashboard.user" defaultMessage="User" />
|
||||
{
|
||||
tab === 'overview'
|
||||
? (
|
||||
@ -104,25 +123,28 @@ class UsersTable extends React.Component {
|
||||
}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-center">
|
||||
<FormattedMessage id="app.learningDashboard.participantsTable.colOnline" defaultMessage="Online time" />
|
||||
<FormattedMessage id="app.learningDashboard.usersTable.colOnline" defaultMessage="Online time" />
|
||||
</th>
|
||||
<th className="px-4 py-3 text-center">
|
||||
<FormattedMessage id="app.learningDashboard.participantsTable.colTalk" defaultMessage="Talk time" />
|
||||
<FormattedMessage id="app.learningDashboard.usersTable.colTalk" defaultMessage="Talk time" />
|
||||
</th>
|
||||
<th className="px-4 py-3 text-center">
|
||||
<FormattedMessage id="app.learningDashboard.participantsTable.colWebcam" defaultMessage="Webcam Time" />
|
||||
<FormattedMessage id="app.learningDashboard.usersTable.colWebcam" defaultMessage="Webcam Time" />
|
||||
</th>
|
||||
<th className="px-4 py-3 text-center">
|
||||
<FormattedMessage id="app.learningDashboard.participantsTable.colMessages" defaultMessage="Messages" />
|
||||
<FormattedMessage id="app.learningDashboard.usersTable.colMessages" defaultMessage="Messages" />
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left">
|
||||
<FormattedMessage id="app.learningDashboard.participantsTable.colEmojis" defaultMessage="Emojis" />
|
||||
<th className="px-4 py-3 col-text-left">
|
||||
<FormattedMessage id="app.learningDashboard.usersTable.colEmojis" defaultMessage="Emojis" />
|
||||
</th>
|
||||
<th className="px-4 py-3 text-center">
|
||||
<FormattedMessage id="app.learningDashboard.participantsTable.colRaiseHands" defaultMessage="Raise Hand" />
|
||||
<FormattedMessage id="app.learningDashboard.usersTable.colRaiseHands" defaultMessage="Raise Hand" />
|
||||
</th>
|
||||
<th className="px-4 py-3 text-center">
|
||||
<FormattedMessage id="app.learningDashboard.participantsTable.colActivityScore" defaultMessage="Activity Score" />
|
||||
<th
|
||||
className={`px-4 py-3 text-center ${tab === 'overview_activityscore' ? 'cursor-pointer' : ''}`}
|
||||
onClick={() => { if (tab === 'overview_activityscore') this.toggleActivityScoreOrder(); }}
|
||||
>
|
||||
<FormattedMessage id="app.learningDashboard.usersTable.colActivityScore" defaultMessage="Activity Score" />
|
||||
{
|
||||
tab === 'overview_activityscore'
|
||||
? (
|
||||
@ -133,14 +155,19 @@ class UsersTable extends React.Component {
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 13l-5 5m0 0l-5-5m5 5V6" />
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d={activityscoreOrder === 'asc' ? 'M17 13l-5 5m0 0l-5-5m5 5V6' : 'M7 11l5-5m0 0l5 5m-5-5v12'}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
: null
|
||||
}
|
||||
</th>
|
||||
<th className="px-4 py-3">
|
||||
<FormattedMessage id="app.learningDashboard.participantsTable.colStatus" defaultMessage="Status" />
|
||||
<th className="px-4 py-3 text-center">
|
||||
<FormattedMessage id="app.learningDashboard.usersTable.colStatus" defaultMessage="Status" />
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -148,8 +175,12 @@ class UsersTable extends React.Component {
|
||||
{ typeof allUsers === 'object' && Object.values(allUsers || {}).length > 0 ? (
|
||||
Object.values(allUsers || {})
|
||||
.sort((a, b) => {
|
||||
if (tab === 'overview_activityscore' && usersActivityScore[a.intId] < usersActivityScore[b.intId]) return 1;
|
||||
if (tab === 'overview_activityscore' && usersActivityScore[a.intId] > usersActivityScore[b.intId]) return -1;
|
||||
if (tab === 'overview_activityscore' && usersActivityScore[a.intId] < usersActivityScore[b.intId]) {
|
||||
return activityscoreOrder === 'desc' ? 1 : -1;
|
||||
}
|
||||
if (tab === 'overview_activityscore' && usersActivityScore[a.intId] > usersActivityScore[b.intId]) {
|
||||
return activityscoreOrder === 'desc' ? -1 : 1;
|
||||
}
|
||||
if (a.isModerator === false && b.isModerator === true) return 1;
|
||||
if (a.isModerator === true && b.isModerator === false) return -1;
|
||||
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
|
||||
@ -158,84 +189,83 @@ class UsersTable extends React.Component {
|
||||
})
|
||||
.map((user) => (
|
||||
<tr key={user} className="text-gray-700">
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center text-sm">
|
||||
<div className="relative hidden w-8 h-8 mr-3 rounded-full md:block">
|
||||
{/* <img className="object-cover w-full h-full rounded-full" */}
|
||||
{/* src="" */}
|
||||
{/* alt="" loading="lazy" /> */}
|
||||
<UserAvatar user={user} />
|
||||
<div
|
||||
className="absolute inset-0 rounded-full shadow-inner"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold">
|
||||
{user.name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4 inline"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
<FormattedDate
|
||||
value={user.registeredOn}
|
||||
month="short"
|
||||
day="numeric"
|
||||
hour="2-digit"
|
||||
minute="2-digit"
|
||||
second="2-digit"
|
||||
<td className="px-4 py-3 col-text-left text-sm">
|
||||
<div className="inline-block relative w-8 h-8 rounded-full">
|
||||
{/* <img className="object-cover w-full h-full rounded-full" */}
|
||||
{/* src="" */}
|
||||
{/* alt="" loading="lazy" /> */}
|
||||
<UserAvatar user={user} />
|
||||
<div
|
||||
className="absolute inset-0 rounded-full shadow-inner"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="inline-block">
|
||||
<p className="font-semibold">
|
||||
{user.name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4 inline"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
|
||||
/>
|
||||
</p>
|
||||
{
|
||||
user.leftOn > 0
|
||||
? (
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4 inline"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<FormattedDate
|
||||
value={user.leftOn}
|
||||
month="short"
|
||||
day="numeric"
|
||||
hour="2-digit"
|
||||
minute="2-digit"
|
||||
second="2-digit"
|
||||
</svg>
|
||||
<FormattedDate
|
||||
value={user.registeredOn}
|
||||
month="short"
|
||||
day="numeric"
|
||||
hour="2-digit"
|
||||
minute="2-digit"
|
||||
second="2-digit"
|
||||
/>
|
||||
</p>
|
||||
{
|
||||
user.leftOn > 0
|
||||
? (
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4 inline"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
||||
/>
|
||||
</p>
|
||||
)
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
</svg>
|
||||
|
||||
<FormattedDate
|
||||
value={user.leftOn}
|
||||
month="short"
|
||||
day="numeric"
|
||||
hour="2-digit"
|
||||
minute="2-digit"
|
||||
second="2-digit"
|
||||
/>
|
||||
</p>
|
||||
)
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-center items-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4 mr-1 inline"
|
||||
className="h-4 w-4 inline"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@ -247,6 +277,7 @@ class UsersTable extends React.Component {
|
||||
d="M5.636 18.364a9 9 0 010-12.728m12.728 0a9 9 0 010 12.728m-9.9-2.829a5 5 0 010-7.07m7.072 0a5 5 0 010 7.07M13 12a1 1 0 11-2 0 1 1 0 012 0z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{ tsToHHmmss(
|
||||
(user.leftOn > 0
|
||||
? user.leftOn
|
||||
@ -271,7 +302,7 @@ class UsersTable extends React.Component {
|
||||
<span className="text-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4 mr-1 inline"
|
||||
className="h-4 w-4 inline"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@ -283,6 +314,7 @@ class UsersTable extends React.Component {
|
||||
d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{ tsToHHmmss(user.talk.totalTime) }
|
||||
</span>
|
||||
) : null }
|
||||
@ -293,7 +325,7 @@ class UsersTable extends React.Component {
|
||||
<span className="text-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4 mr-1 inline"
|
||||
className="h-4 w-4 inline"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@ -305,6 +337,7 @@ class UsersTable extends React.Component {
|
||||
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{ tsToHHmmss(getSumOfTime(user.webcams)) }
|
||||
</span>
|
||||
) : null }
|
||||
@ -315,7 +348,7 @@ class UsersTable extends React.Component {
|
||||
<span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4 mr-1 inline"
|
||||
className="h-4 w-4 inline"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@ -327,14 +360,15 @@ class UsersTable extends React.Component {
|
||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{user.totalOfMessages}
|
||||
</span>
|
||||
) : null }
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-left">
|
||||
<td className="px-4 py-3 text-sm col-text-left">
|
||||
{
|
||||
Object.keys(usersEmojisSummary[user.intId] || {}).map((emoji) => (
|
||||
<div className="text-xs">
|
||||
<div className="text-xs whitespace-nowrap">
|
||||
<i className={`${emojiConfigs[emoji].icon} text-sm`} />
|
||||
|
||||
{ usersEmojisSummary[user.intId][emoji] }
|
||||
@ -353,7 +387,7 @@ class UsersTable extends React.Component {
|
||||
<span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4 mr-1 inline"
|
||||
className="h-4 w-4 inline"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@ -365,6 +399,7 @@ class UsersTable extends React.Component {
|
||||
d="M7 11.5V14m0-2.5v-6a1.5 1.5 0 113 0m-3 6a1.5 1.5 0 00-3 0v2a7.5 7.5 0 0015 0v-5a1.5 1.5 0 00-3 0m-6-3V11m0-5.5v-1a1.5 1.5 0 013 0v1m0 0V11m0-5.5a1.5 1.5 0 013 0v3m0 0V11"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{user.emojis.filter((emoji) => emoji.name === 'raiseHand').length}
|
||||
</span>
|
||||
) : null }
|
||||
@ -381,23 +416,23 @@ class UsersTable extends React.Component {
|
||||
<rect width="12" height="12" x="70" fill={usersActivityScore[user.intId] === 10 ? '#047857' : '#e4e4e7'} />
|
||||
</svg>
|
||||
|
||||
<span className="text-xs bg-gray-200 rounded-full px-2 ml-1">
|
||||
<span className="text-xs bg-gray-200 rounded-full px-2">
|
||||
<FormattedNumber value={usersActivityScore[user.intId]} minimumFractionDigits="0" maximumFractionDigits="1" />
|
||||
</span>
|
||||
</td>
|
||||
) : <td />
|
||||
}
|
||||
<td className="px-4 py-3 text-xs">
|
||||
<td className="px-4 py-3 text-xs text-center">
|
||||
{
|
||||
user.leftOn > 0
|
||||
? (
|
||||
<span className="px-2 py-1 font-semibold leading-tight text-red-700 bg-red-100 rounded-full">
|
||||
<FormattedMessage id="app.learningDashboard.participantsTable.userStatusOffline" defaultMessage="Offline" />
|
||||
<FormattedMessage id="app.learningDashboard.usersTable.userStatusOffline" defaultMessage="Offline" />
|
||||
</span>
|
||||
)
|
||||
: (
|
||||
<span className="px-2 py-1 font-semibold leading-tight text-green-700 bg-green-100 rounded-full">
|
||||
<FormattedMessage id="app.learningDashboard.participantsTable.userStatusOnline" defaultMessage="Online" />
|
||||
<FormattedMessage id="app.learningDashboard.usersTable.userStatusOnline" defaultMessage="Online" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@ -407,7 +442,7 @@ class UsersTable extends React.Component {
|
||||
) : (
|
||||
<tr className="text-gray-700">
|
||||
<td colSpan="8" className="px-4 py-3 text-sm text-center">
|
||||
<FormattedMessage id="app.learningDashboard.participantsTable.noUsers" defaultMessage="No users" />
|
||||
<FormattedMessage id="app.learningDashboard.usersTable.noUsers" defaultMessage="No users" />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
@ -5,6 +5,8 @@ import { IntlProvider } from 'react-intl';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
const RTL_LANGUAGES = ['ar', 'dv', 'fa', 'he'];
|
||||
|
||||
function getLanguage() {
|
||||
let { language } = navigator;
|
||||
|
||||
@ -27,6 +29,7 @@ class Dashboard extends React.Component {
|
||||
};
|
||||
|
||||
this.setMessages();
|
||||
this.setRtl();
|
||||
}
|
||||
|
||||
setMessages() {
|
||||
@ -54,6 +57,14 @@ class Dashboard extends React.Component {
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
setRtl() {
|
||||
const { intlLocale } = this.state;
|
||||
|
||||
if (RTL_LANGUAGES.includes(intlLocale)) {
|
||||
document.body.parentNode.setAttribute('dir', 'rtl');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intlLocale, intlMessages } = this.state;
|
||||
|
||||
|
@ -22,6 +22,7 @@ fi;
|
||||
#Create tmp dir for conversion
|
||||
mkdir -p "/tmp/bbb-soffice-$(whoami)/"
|
||||
tempDir="$(mktemp -d -p /tmp/bbb-soffice-$(whoami)/)"
|
||||
trap 'rm -fr "$tempDir"' EXIT
|
||||
|
||||
source="$1"
|
||||
dest="$2"
|
||||
@ -37,8 +38,5 @@ then
|
||||
fi
|
||||
|
||||
cp "${source}" "$tempDir/file"
|
||||
sudo /usr/bin/docker run --rm --network none --env="HOME=/tmp/" -w /tmp/ --user=$(printf %05d `id -u`) -v "$tempDir/":/data/ -v /usr/share/fonts/:/usr/share/fonts/:ro --rm bbb-soffice sh -c "/usr/bin/soffice -env:UserInstallation=file:///tmp/ $convertToParam --outdir /data /data/file"
|
||||
sudo /usr/bin/docker run --rm --memory=1g --memory-swap=1g --network none --env="HOME=/tmp/" -w /tmp/ --user=$(printf %05d `id -u`) -v "$tempDir/":/data/ -v /usr/share/fonts/:/usr/share/fonts/:ro --rm bbb-soffice sh -c "/usr/bin/soffice -env:UserInstallation=file:///tmp/ $convertToParam --outdir /data /data/file"
|
||||
cp "$tempDir/file.$convertTo" "${dest}"
|
||||
rm -r "$tempDir/"
|
||||
|
||||
exit 0
|
||||
|
@ -1,4 +1,4 @@
|
||||
bigbluebutton ALL=(ALL) NOPASSWD: /usr/bin/docker run --rm --network none --env=HOME=/tmp/ -w /tmp/ --user=[0-9][0-9][0-9][0-9][0-9] -v /tmp/bbb-soffice-bigbluebutton/tmp.[0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z]/\:/data/ -v /usr/share/fonts/\:/usr/share/fonts/\:ro --rm bbb-soffice sh -c /usr/bin/soffice -env\:UserInstallation=file\:///tmp/ --convert-to pdf --outdir /data /data/file
|
||||
etherpad ALL=(ALL) NOPASSWD: /usr/bin/docker run --rm --network none --env=HOME=/tmp/ -w /tmp/ --user=[0-9][0-9][0-9][0-9][0-9] -v /tmp/bbb-soffice-etherpad/tmp.[0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z]/\:/data/ -v /usr/share/fonts/\:/usr/share/fonts/\:ro --rm bbb-soffice sh -c /usr/bin/soffice -env\:UserInstallation=file\:///tmp/ --convert-to pdf --writer --outdir /data /data/file
|
||||
etherpad ALL=(ALL) NOPASSWD: /usr/bin/docker run --rm --network none --env=HOME=/tmp/ -w /tmp/ --user=[0-9][0-9][0-9][0-9][0-9] -v /tmp/bbb-soffice-etherpad/tmp.[0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z]/\:/data/ -v /usr/share/fonts/\:/usr/share/fonts/\:ro --rm bbb-soffice sh -c /usr/bin/soffice -env\:UserInstallation=file\:///tmp/ --convert-to odt --writer --outdir /data /data/file
|
||||
etherpad ALL=(ALL) NOPASSWD: /usr/bin/docker run --rm --network none --env=HOME=/tmp/ -w /tmp/ --user=[0-9][0-9][0-9][0-9][0-9] -v /tmp/bbb-soffice-etherpad/tmp.[0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z]/\:/data/ -v /usr/share/fonts/\:/usr/share/fonts/\:ro --rm bbb-soffice sh -c /usr/bin/soffice -env\:UserInstallation=file\:///tmp/ --convert-to doc --outdir /data /data/file
|
||||
bigbluebutton ALL=(ALL) NOPASSWD: /usr/bin/docker run --rm --memory=1g --memory-swap=1g --network none --env=HOME=/tmp/ -w /tmp/ --user=[0-9][0-9][0-9][0-9][0-9] -v /tmp/bbb-soffice-bigbluebutton/tmp.[0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z]/\:/data/ -v /usr/share/fonts/\:/usr/share/fonts/\:ro --rm bbb-soffice sh -c /usr/bin/soffice -env\:UserInstallation=file\:///tmp/ --convert-to pdf --outdir /data /data/file
|
||||
etherpad ALL=(ALL) NOPASSWD: /usr/bin/docker run --rm --memory=1g --memory-swap=1g --network none --env=HOME=/tmp/ -w /tmp/ --user=[0-9][0-9][0-9][0-9][0-9] -v /tmp/bbb-soffice-etherpad/tmp.[0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z]/\:/data/ -v /usr/share/fonts/\:/usr/share/fonts/\:ro --rm bbb-soffice sh -c /usr/bin/soffice -env\:UserInstallation=file\:///tmp/ --convert-to pdf --writer --outdir /data /data/file
|
||||
etherpad ALL=(ALL) NOPASSWD: /usr/bin/docker run --rm --memory=1g --memory-swap=1g --network none --env=HOME=/tmp/ -w /tmp/ --user=[0-9][0-9][0-9][0-9][0-9] -v /tmp/bbb-soffice-etherpad/tmp.[0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z]/\:/data/ -v /usr/share/fonts/\:/usr/share/fonts/\:ro --rm bbb-soffice sh -c /usr/bin/soffice -env\:UserInstallation=file\:///tmp/ --convert-to odt --writer --outdir /data /data/file
|
||||
etherpad ALL=(ALL) NOPASSWD: /usr/bin/docker run --rm --memory=1g --memory-swap=1g --network none --env=HOME=/tmp/ -w /tmp/ --user=[0-9][0-9][0-9][0-9][0-9] -v /tmp/bbb-soffice-etherpad/tmp.[0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z]/\:/data/ -v /usr/share/fonts/\:/usr/share/fonts/\:ro --rm bbb-soffice sh -c /usr/bin/soffice -env\:UserInstallation=file\:///tmp/ --convert-to doc --outdir /data /data/file
|
||||
|
@ -6,8 +6,6 @@ ENV DEBIAN_FRONTEND noninteractive
|
||||
#Required to install Libreoffice 7
|
||||
RUN echo "deb http://deb.debian.org/debian buster-backports main" >> /etc/apt/sources.list
|
||||
|
||||
RUN apt update
|
||||
|
||||
RUN apt -y install locales-all fontconfig libxt6 libxrender1
|
||||
RUN apt install -y -t buster-backports libreoffice
|
||||
RUN apt update && apt -y install locales-all fontconfig libxt6 libxrender1
|
||||
RUN apt update && apt -y install -t buster-backports libreoffice
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
<condition field="${sip_via_protocol}" expression="^wss?$" break="on-false">
|
||||
<action application="set" data="bbb_authorized=true"/>
|
||||
<action application="set" data="jb_use_timestamps=true"/>
|
||||
<action application="set" data="include_external_ip=true"/>
|
||||
<action application="transfer" data="${destination_number} XML default"/>
|
||||
</condition>
|
||||
</extension>
|
||||
|
@ -1 +1 @@
|
||||
git clone --branch v2.5.2 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu
|
||||
git clone --branch v2.6.0-beta.5 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu
|
||||
|
@ -1 +1 @@
|
||||
BIGBLUEBUTTON_RELEASE=2.4-rc-1
|
||||
BIGBLUEBUTTON_RELEASE=2.4-rc-2
|
||||
|
@ -1178,7 +1178,7 @@ check_state() {
|
||||
echo "#"
|
||||
fi
|
||||
|
||||
if [ "$(echo "$HTML5_CONFIG" | yq r - public.media.sipjsHackViaWs)" == "false" ]; then
|
||||
if [ "$(echo "$HTML5_CONFIG" | yq r - public.media.sipjsHackViaWs)" != "true" ]; then
|
||||
if [ "$PROTOCOL" == "https" ]; then
|
||||
if ! cat $SIP_CONFIG | grep -v '#' | grep proxy_pass | head -n 1 | grep -q https; then
|
||||
echo "# Warning: You have this server defined for https, but in"
|
||||
@ -1412,7 +1412,6 @@ if [ $CHECK ]; then
|
||||
echo " kurento.ip: $(echo "$KURENTO_CONFIG" | yq r - kurento[0].ip)"
|
||||
echo " kurento.url: $(echo "$KURENTO_CONFIG" | yq r - kurento[0].url)"
|
||||
echo " kurento.sip_ip: $(echo "$KURENTO_CONFIG" | yq r - freeswitch.sip_ip)"
|
||||
echo " localIpAddress: $(echo "$KURENTO_CONFIG" | yq r - localIpAddress)"
|
||||
echo " recordScreenSharing: $(echo "$KURENTO_CONFIG" | yq r - recordScreenSharing)"
|
||||
echo " recordWebcams: $(echo "$KURENTO_CONFIG" | yq r - recordWebcams)"
|
||||
echo " codec_video_main: $(echo "$KURENTO_CONFIG" | yq r - conference-media-specs.codec_video_main)"
|
||||
|
@ -34,15 +34,23 @@ log_history=28
|
||||
find /var/bigbluebutton/ -maxdepth 1 -type d -name "*-*" -mtime +$history -exec rm -rf '{}' +
|
||||
|
||||
#
|
||||
# Delete streams in kurento older than N days
|
||||
# Delete streams from Kurento and mediasoup older than N days
|
||||
#
|
||||
for app in recordings screenshare; do
|
||||
app_dir=/var/kurento/$app
|
||||
if [[ -d $app_dir ]]; then
|
||||
find $app_dir -name "*.mkv" -o -name "*.webm" -mtime +$history -delete
|
||||
find $app_dir -type d -empty -mtime +$history -exec rmdir '{}' +
|
||||
fi
|
||||
done
|
||||
kurento_dir=/var/kurento/
|
||||
mediasoup_dir=/var/mediasoup/
|
||||
|
||||
remove_stale_sfu_raw_files() {
|
||||
for app in recordings screenshare; do
|
||||
app_dir="${1}${app}"
|
||||
if [[ -d $app_dir ]]; then
|
||||
find "$app_dir" -name "*.mkv" -o -name "*.webm" -mtime +"$history" -delete
|
||||
find "$app_dir" -type d -empty -mtime +"$history" -exec rmdir '{}' +
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
remove_stale_sfu_raw_files "$kurento_dir"
|
||||
remove_stale_sfu_raw_files "$mediasoup_dir"
|
||||
|
||||
#
|
||||
# Delete FreeSWITCH wav/opus recordings older than N days
|
||||
|
@ -3,15 +3,15 @@
|
||||
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||
# but you can also edit it by hand.
|
||||
|
||||
meteor-base@1.4.0
|
||||
meteor-base@1.5.1
|
||||
mobile-experience@1.1.0
|
||||
mongo@1.10.0
|
||||
mongo@1.12.0
|
||||
reactive-var@1.0.11
|
||||
|
||||
standard-minifier-css@1.6.0
|
||||
standard-minifier-js@2.6.0
|
||||
standard-minifier-css@1.7.3
|
||||
standard-minifier-js@2.6.1
|
||||
es5-shim@4.8.0
|
||||
ecmascript@0.14.3
|
||||
ecmascript@0.15.3
|
||||
shell-server@0.5.0
|
||||
|
||||
static-html
|
||||
|
@ -1 +1 @@
|
||||
METEOR@1.10.2
|
||||
METEOR@2.3.6
|
||||
|
@ -1,81 +1,81 @@
|
||||
allow-deny@1.1.0
|
||||
autoupdate@1.6.0
|
||||
babel-compiler@7.5.3
|
||||
autoupdate@1.7.0
|
||||
babel-compiler@7.7.0
|
||||
babel-runtime@1.5.0
|
||||
base64@1.0.12
|
||||
binary-heap@1.0.11
|
||||
blaze-tools@1.0.10
|
||||
boilerplate-generator@1.7.0
|
||||
blaze-tools@1.1.2
|
||||
boilerplate-generator@1.7.1
|
||||
caching-compiler@1.2.2
|
||||
caching-html-compiler@1.1.3
|
||||
callback-hook@1.3.0
|
||||
caching-html-compiler@1.2.1
|
||||
callback-hook@1.3.1
|
||||
cfs:reactive-list@0.0.9
|
||||
check@1.3.1
|
||||
ddp@1.4.0
|
||||
ddp-client@2.3.3
|
||||
ddp-client@2.5.0
|
||||
ddp-common@1.4.0
|
||||
ddp-server@2.3.2
|
||||
ddp-server@2.4.1
|
||||
deps@1.0.12
|
||||
diff-sequence@1.1.1
|
||||
dynamic-import@0.5.2
|
||||
ecmascript@0.14.3
|
||||
dynamic-import@0.7.1
|
||||
ecmascript@0.15.3
|
||||
ecmascript-runtime@0.7.0
|
||||
ecmascript-runtime-client@0.10.0
|
||||
ecmascript-runtime-server@0.9.0
|
||||
ecmascript-runtime-client@0.11.1
|
||||
ecmascript-runtime-server@0.10.1
|
||||
ejson@1.1.1
|
||||
es5-shim@4.8.0
|
||||
fetch@0.1.1
|
||||
geojson-utils@1.0.10
|
||||
hot-code-push@1.0.4
|
||||
html-tools@1.0.11
|
||||
htmljs@1.0.11
|
||||
http@1.4.2
|
||||
id-map@1.1.0
|
||||
html-tools@1.1.2
|
||||
htmljs@1.1.1
|
||||
http@1.4.4
|
||||
id-map@1.1.1
|
||||
inter-process-messaging@0.1.1
|
||||
launch-screen@1.2.0
|
||||
livedata@1.0.18
|
||||
launch-screen@1.3.0
|
||||
lmieulet:meteor-coverage@3.2.0
|
||||
logging@1.1.20
|
||||
logging@1.2.0
|
||||
meteor@1.9.3
|
||||
meteor-base@1.4.0
|
||||
meteor-base@1.5.1
|
||||
meteortesting:browser-tests@1.3.4
|
||||
meteortesting:mocha@2.0.1
|
||||
meteortesting:mocha@2.0.2
|
||||
meteortesting:mocha-core@8.0.1
|
||||
minifier-css@1.5.1
|
||||
minifier-js@2.6.0
|
||||
minimongo@1.6.0
|
||||
minifier-css@1.5.4
|
||||
minifier-js@2.6.1
|
||||
minimongo@1.7.0
|
||||
mobile-experience@1.1.0
|
||||
mobile-status-bar@1.1.0
|
||||
modern-browsers@0.1.5
|
||||
modules@0.15.0
|
||||
modules@0.16.0
|
||||
modules-runtime@0.12.0
|
||||
mongo@1.10.0
|
||||
mongo-decimal@0.1.1
|
||||
mongo@1.12.0
|
||||
mongo-decimal@0.1.2
|
||||
mongo-dev-server@1.1.0
|
||||
mongo-id@1.0.7
|
||||
mongo-id@1.0.8
|
||||
nathantreid:css-modules@4.1.0
|
||||
npm-mongo@3.7.1
|
||||
npm-mongo@3.9.1
|
||||
ordered-dict@1.1.0
|
||||
promise@0.11.2
|
||||
promise@0.12.0
|
||||
random@1.2.0
|
||||
react-fast-refresh@0.1.1
|
||||
react-meteor-data@0.2.16
|
||||
reactive-dict@1.3.0
|
||||
reactive-var@1.0.11
|
||||
reload@1.3.0
|
||||
reload@1.3.1
|
||||
retry@1.1.0
|
||||
rocketchat:streamer@1.1.0
|
||||
routepolicy@1.1.0
|
||||
routepolicy@1.1.1
|
||||
session@1.2.0
|
||||
shell-server@0.5.0
|
||||
socket-stream-client@0.3.0
|
||||
spacebars-compiler@1.1.3
|
||||
standard-minifier-css@1.6.0
|
||||
standard-minifier-js@2.6.0
|
||||
static-html@1.2.2
|
||||
templating-tools@1.1.2
|
||||
socket-stream-client@0.4.0
|
||||
spacebars-compiler@1.3.0
|
||||
standard-minifier-css@1.7.3
|
||||
standard-minifier-js@2.6.1
|
||||
static-html@1.3.2
|
||||
templating-tools@1.2.1
|
||||
tmeasday:check-npm-versions@0.3.2
|
||||
tracker@1.2.0
|
||||
underscore@1.0.10
|
||||
url@1.3.1
|
||||
webapp@1.9.1
|
||||
webapp-hashing@1.0.9
|
||||
url@1.3.2
|
||||
webapp@1.11.1
|
||||
webapp-hashing@1.1.0
|
||||
|
@ -75,8 +75,13 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
textarea::-webkit-input-placeholder,
|
||||
input::-webkit-input-placeholder {
|
||||
::-webkit-input-placeholder {
|
||||
color: var(--palette-placeholder-text);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
:-moz-placeholder, /* Firefox 4 to 18 */
|
||||
::-moz-placeholder { /* Firefox 19+ */
|
||||
color: var(--palette-placeholder-text);
|
||||
opacity: 1;
|
||||
}
|
||||
@ -89,6 +94,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
<script src="compatibility/adapter.js?v=VERSION" language="javascript"></script>
|
||||
<script src="compatibility/sip.js?v=VERSION" language="javascript"></script>
|
||||
<script src="compatibility/kurento-utils.js?v=VERSION" language="javascript"></script>
|
||||
<script src="compatibility/tflite-simd.js?v=VERSION" language="javascript"></script>
|
||||
<script src="compatibility/tflite.js?v=VERSION" language="javascript"></script>
|
||||
<!-- fonts -->
|
||||
<link rel="preload" href="fonts/BbbIcons/bbb-icons.woff?j1ntjp" as="font" crossorigin="anonymous"/>
|
||||
<link rel="preload" href="fonts/SourceSansPro/SourceSansPro-Light.woff" as="font" crossorigin="anonymous"/>
|
||||
|
@ -18,6 +18,7 @@
|
||||
/* eslint no-unused-vars: 0 */
|
||||
|
||||
import './wdyr';
|
||||
import '../imports/ui/services/collection-hooks/collection-hooks';
|
||||
|
||||
import React from 'react';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
@ -33,6 +34,15 @@ import ChatAdapter from '/imports/ui/components/components-data/chat-context/ada
|
||||
import UsersAdapter from '/imports/ui/components/components-data/users-context/adapter';
|
||||
import GroupChatAdapter from '/imports/ui/components/components-data/group-chat-context/adapter';
|
||||
|
||||
import '/imports/ui/local-collections/meetings-collection/meetings';
|
||||
import '/imports/ui/local-collections/breakouts-collection/breakouts';
|
||||
import '/imports/ui/local-collections/guest-users-collection/guest-users';
|
||||
import '/imports/ui/local-collections/users-collection/users';
|
||||
|
||||
import('/imports/api/audio/client/bridge/bridge-whitelist').catch(() => {
|
||||
// bridge loading
|
||||
});
|
||||
|
||||
Meteor.startup(() => {
|
||||
// Logs all uncaught exceptions to the client logger
|
||||
window.addEventListener('error', (e) => {
|
||||
|
@ -320,3 +320,12 @@
|
||||
.icon-bbb-device_list_selector:before {
|
||||
content: "\e95b";
|
||||
}
|
||||
.icon-bbb-presentation_off:before {
|
||||
content: "\e95c";
|
||||
}
|
||||
.icon-bbb-external-video:before {
|
||||
content: "\e95d";
|
||||
}
|
||||
.icon-bbb-external-video_off:before {
|
||||
content: "\e95e";
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ UPPER_DESTINATION_DIR=/usr/share/meteor
|
||||
DESTINATION_DIR=$UPPER_DESTINATION_DIR/bundle
|
||||
|
||||
SERVICE_FILES_DIR=/usr/lib/systemd/system
|
||||
LOCAL_PACKAGING_DIR=/home/bigbluebutton/dev/bigbluebutton/bigbluebutton-html5/dev_local_deployment
|
||||
LOCAL_PACKAGING_DIR=/home/bigbluebutton/dev/bigbluebutton/build/packages-template/bbb-html5
|
||||
|
||||
if [ ! -d "$LOCAL_PACKAGING_DIR" ]; then
|
||||
echo "Did not find LOCAL_PACKAGING_DIR=$LOCAL_PACKAGING_DIR"
|
||||
@ -59,13 +59,13 @@ NUMBER_OF_FRONTEND_NODEJS_PROCESSES=2
|
||||
HERE
|
||||
|
||||
echo "writing $DESTINATION_DIR/systemd_start.sh"
|
||||
sudo cp $LOCAL_PACKAGING_DIR/systemd_start.sh "$DESTINATION_DIR"/systemd_start.sh
|
||||
sudo cp $LOCAL_PACKAGING_DIR/bionic/systemd_start.sh "$DESTINATION_DIR"/systemd_start.sh
|
||||
|
||||
echo "writing $DESTINATION_DIR/systemd_start_frontend.sh"
|
||||
sudo cp $LOCAL_PACKAGING_DIR/systemd_start_frontend.sh "$DESTINATION_DIR"/systemd_start_frontend.sh
|
||||
sudo cp $LOCAL_PACKAGING_DIR/bionic/systemd_start_frontend.sh "$DESTINATION_DIR"/systemd_start_frontend.sh
|
||||
|
||||
echo "writing $DESTINATION_DIR/workers-start.sh"
|
||||
sudo cp $LOCAL_PACKAGING_DIR/workers-start.sh "$DESTINATION_DIR"/workers-start.sh
|
||||
sudo cp $LOCAL_PACKAGING_DIR/bionic/workers-start.sh "$DESTINATION_DIR"/workers-start.sh
|
||||
|
||||
sudo chown -R meteor:meteor "$UPPER_DESTINATION_DIR"/
|
||||
sudo chmod +x "$DESTINATION_DIR"/mongod_start_pre.sh
|
||||
@ -76,10 +76,10 @@ sudo chmod +x "$DESTINATION_DIR"/workers-start.sh
|
||||
|
||||
|
||||
echo "writing $SERVICE_FILES_DIR/bbb-html5-frontend@.service"
|
||||
sudo cp $LOCAL_PACKAGING_DIR/bbb-html5-frontend@.service "$SERVICE_FILES_DIR"/bbb-html5-frontend@.service
|
||||
sudo cp $LOCAL_PACKAGING_DIR/bionic/bbb-html5-frontend@.service "$SERVICE_FILES_DIR"/bbb-html5-frontend@.service
|
||||
|
||||
echo "writing $SERVICE_FILES_DIR/bbb-html5-backend@.service"
|
||||
sudo cp $LOCAL_PACKAGING_DIR/bbb-html5-backend@.service "$SERVICE_FILES_DIR"/bbb-html5-backend@.service
|
||||
sudo cp $LOCAL_PACKAGING_DIR/bionic/bbb-html5-backend@.service "$SERVICE_FILES_DIR"/bbb-html5-backend@.service
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
|
@ -1,9 +0,0 @@
|
||||
Last change on Feb 16, 2021
|
||||
|
||||
This directory contains files needed for the correct deployment of bigbluebutton-html5 **on a development environment**.
|
||||
|
||||
They are very similar, or even identical to the files used for `bbb-html5` packaging, however, the main difference is that this set of files may be unintentionally **out of date**.
|
||||
|
||||
The script `deploy_to_usr_share.sh` was written to allow developers to be able to wipe out the `/usr/share/meteor` directory where `bbb-html5` package is installed, and at the same time build their local code and deploy it so it replaces the default `bbb-html5`. The script has been indispensible during the work on https://github.com/bigbluebutton/bigbluebutton/pull/11317 where multiple NodeJS processes were to run simultaneously but using different configuration.
|
||||
|
||||
|
@ -1,24 +0,0 @@
|
||||
[Unit]
|
||||
Description=BigBlueButton HTML5 service, backend instance %i
|
||||
Requires=bbb-html5.service
|
||||
Before=bbb-html5.service
|
||||
BindsTo=bbb-html5.service
|
||||
|
||||
[Service]
|
||||
PermissionsStartOnly=true
|
||||
#Type=simple
|
||||
Type=idle
|
||||
EnvironmentFile=/usr/share/meteor/bundle/bbb-html5-with-roles.conf
|
||||
ExecStart=/usr/share/meteor/bundle/systemd_start.sh %i $BACKEND_NODEJS_ROLE
|
||||
WorkingDirectory=/usr/share/meteor/bundle
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
TimeoutStartSec=10
|
||||
RestartSec=10
|
||||
User=meteor
|
||||
Group=meteor
|
||||
CPUSchedulingPolicy=fifo
|
||||
Nice=19
|
||||
|
||||
[Install]
|
||||
WantedBy=bbb-html5.service
|
@ -1,24 +0,0 @@
|
||||
[Unit]
|
||||
Description=BigBlueButton HTML5 service, frontend instance %i
|
||||
Requires=bbb-html5.service
|
||||
Before=bbb-html5.service
|
||||
BindsTo=bbb-html5.service
|
||||
|
||||
[Service]
|
||||
PermissionsStartOnly=true
|
||||
#Type=simple
|
||||
Type=idle
|
||||
EnvironmentFile=/usr/share/meteor/bundle/bbb-html5-with-roles.conf
|
||||
ExecStart=/usr/share/meteor/bundle/systemd_start_frontend.sh %i
|
||||
WorkingDirectory=/usr/share/meteor/bundle
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
TimeoutStartSec=10
|
||||
RestartSec=10
|
||||
User=meteor
|
||||
Group=meteor
|
||||
CPUSchedulingPolicy=fifo
|
||||
Nice=19
|
||||
|
||||
[Install]
|
||||
WantedBy=bbb-html5.service
|
@ -1,32 +0,0 @@
|
||||
# mongod.conf
|
||||
|
||||
# for documentation of all options, see:
|
||||
# http://docs.mongodb.org/manual/reference/configuration-options/
|
||||
|
||||
storage:
|
||||
dbPath: /mnt/mongo-ramdisk
|
||||
journal:
|
||||
enabled: true
|
||||
wiredTiger:
|
||||
engineConfig:
|
||||
cacheSizeGB: 0
|
||||
journalCompressor: none
|
||||
directoryForIndexes: true
|
||||
collectionConfig:
|
||||
blockCompressor: none
|
||||
indexConfig:
|
||||
prefixCompression: false
|
||||
|
||||
systemLog:
|
||||
destination: file
|
||||
logAppend: true
|
||||
path: /var/log/mongodb/mongod.log
|
||||
|
||||
net:
|
||||
port: 27017
|
||||
bindIp: 127.0.1.1
|
||||
|
||||
|
||||
replication:
|
||||
replSetName: rs0
|
||||
|
@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
rm -rf /mnt/mongo-ramdisk/*
|
||||
mkdir -p /mnt/mongo-ramdisk
|
||||
if /bin/findmnt | grep -q "/mnt/mongo-ramdisk"; then
|
||||
umount /mnt/mongo-ramdisk/
|
||||
fi
|
||||
if [ ! -f /.dockerenv ]; then
|
||||
mount -t tmpfs -o size=512m tmpfs /mnt/mongo-ramdisk
|
||||
fi
|
||||
|
||||
chown -R mongodb:mongodb /mnt/mongo-ramdisk
|
||||
|
||||
|
@ -1,51 +0,0 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
#Allow to run outside of directory
|
||||
cd $(dirname $0)
|
||||
|
||||
echo "Starting mongoDB"
|
||||
|
||||
#wait for mongo startup
|
||||
MONGO_OK=0
|
||||
|
||||
while [ "$MONGO_OK" = "0" ]; do
|
||||
MONGO_OK=$(netstat -lan | grep 127.0.1.1 | grep 27017 &> /dev/null && echo 1 || echo 0)
|
||||
sleep 1;
|
||||
done;
|
||||
|
||||
echo "Mongo started";
|
||||
|
||||
echo "Initializing replicaset"
|
||||
mongo 127.0.1.1 --eval 'rs.initiate({ _id: "rs0", members: [ {_id: 0, host: "127.0.1.1"} ]})'
|
||||
|
||||
|
||||
echo "Waiting to become a master"
|
||||
IS_MASTER="XX"
|
||||
while [ "$IS_MASTER" \!= "true" ]; do
|
||||
IS_MASTER=$(mongo mongodb://127.0.1.1:27017/ --eval 'db.isMaster().ismaster' | tail -n 1)
|
||||
sleep 0.5;
|
||||
done;
|
||||
|
||||
echo "I'm the master!"
|
||||
|
||||
if [ -z $1 ]
|
||||
then
|
||||
INSTANCE_ID=1
|
||||
else
|
||||
INSTANCE_ID=$1
|
||||
fi
|
||||
|
||||
PORT=$(echo "3999+$INSTANCE_ID" | bc)
|
||||
|
||||
echo "instanceId = $INSTANCE_ID and port = $PORT and role is backend (in backend file)"
|
||||
|
||||
export INSTANCE_ID=$INSTANCE_ID
|
||||
export BBB_HTML5_ROLE=backend
|
||||
export ROOT_URL=http://127.0.0.1/html5client
|
||||
export MONGO_OPLOG_URL=mongodb://127.0.1.1/local
|
||||
export MONGO_URL=mongodb://127.0.1.1/meteor
|
||||
export NODE_ENV=production
|
||||
export NODE_VERSION=node-v12.16.1-linux-x64
|
||||
export SERVER_WEBSOCKET_COMPRESSION=0
|
||||
export BIND_IP=127.0.0.1
|
||||
PORT=$PORT /usr/share/$NODE_VERSION/bin/node --max-old-space-size=2048 --max_semi_space_size=128 main.js NODEJS_BACKEND_INSTANCE_ID=$INSTANCE_ID
|
@ -1,51 +0,0 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
#Allow to run outside of directory
|
||||
cd $(dirname $0)
|
||||
|
||||
echo "Starting mongoDB"
|
||||
|
||||
#wait for mongo startup
|
||||
MONGO_OK=0
|
||||
|
||||
while [ "$MONGO_OK" = "0" ]; do
|
||||
MONGO_OK=$(netstat -lan | grep 127.0.1.1 | grep 27017 &> /dev/null && echo 1 || echo 0)
|
||||
sleep 1;
|
||||
done;
|
||||
|
||||
echo "Mongo started";
|
||||
|
||||
echo "Initializing replicaset"
|
||||
mongo 127.0.1.1 --eval 'rs.initiate({ _id: "rs0", members: [ {_id: 0, host: "127.0.1.1"} ]})'
|
||||
|
||||
|
||||
echo "Waiting to become a master"
|
||||
IS_MASTER="XX"
|
||||
while [ "$IS_MASTER" \!= "true" ]; do
|
||||
IS_MASTER=$(mongo mongodb://127.0.1.1:27017/ --eval 'db.isMaster().ismaster' | tail -n 1)
|
||||
sleep 0.5;
|
||||
done;
|
||||
|
||||
echo "I'm the master!"
|
||||
|
||||
if [ -z $1 ]
|
||||
then
|
||||
INSTANCE_ID=1
|
||||
else
|
||||
INSTANCE_ID=$1
|
||||
fi
|
||||
|
||||
PORT=$(echo "4099+$INSTANCE_ID" | bc)
|
||||
|
||||
echo "instanceId = $INSTANCE_ID and port = $PORT and role is frontend (in frontend file)"
|
||||
|
||||
export INSTANCE_ID=$INSTANCE_ID
|
||||
export BBB_HTML5_ROLE=frontend
|
||||
export ROOT_URL=http://127.0.0.1/html5client
|
||||
export MONGO_OPLOG_URL=mongodb://127.0.1.1/local
|
||||
export MONGO_URL=mongodb://127.0.1.1/meteor
|
||||
export NODE_ENV=production
|
||||
export NODE_VERSION=node-v12.16.1-linux-x64
|
||||
export SERVER_WEBSOCKET_COMPRESSION=0
|
||||
export BIND_IP=127.0.0.1
|
||||
PORT=$PORT /usr/share/$NODE_VERSION/bin/node --max-old-space-size=2048 --max_semi_space_size=128 main.js
|
@ -1,33 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Start parallel nodejs processes for bbb-html5. Number varies on restrictions file bbb-html5-with-roles.conf
|
||||
|
||||
source /usr/share/meteor/bundle/bbb-html5-with-roles.conf
|
||||
|
||||
if [ -f /etc/bigbluebutton/bbb-html5-with-roles.conf ]; then
|
||||
source /etc/bigbluebutton/bbb-html5-with-roles.conf
|
||||
fi
|
||||
|
||||
MIN_NUMBER_OF_BACKEND_PROCESSES=1
|
||||
MAX_NUMBER_OF_BACKEND_PROCESSES=4
|
||||
|
||||
MIN_NUMBER_OF_FRONTEND_PROCESSES=0 # 0 means each nodejs process handles both front and backend roles
|
||||
MAX_NUMBER_OF_FRONTEND_PROCESSES=8
|
||||
|
||||
|
||||
# Start backend nodejs processes
|
||||
if ((NUMBER_OF_BACKEND_NODEJS_PROCESSES >= MIN_NUMBER_OF_BACKEND_PROCESSES && NUMBER_OF_BACKEND_NODEJS_PROCESSES <= MAX_NUMBER_OF_BACKEND_PROCESSES)); then
|
||||
for ((i = 1 ; i <= NUMBER_OF_BACKEND_NODEJS_PROCESSES ; i++)); do
|
||||
systemctl start bbb-html5-backend@$i
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Start frontend nodejs processes
|
||||
if ((NUMBER_OF_FRONTEND_NODEJS_PROCESSES >= MIN_NUMBER_OF_FRONTEND_PROCESSES && NUMBER_OF_FRONTEND_NODEJS_PROCESSES <= MAX_NUMBER_OF_FRONTEND_PROCESSES)); then
|
||||
if ((NUMBER_OF_FRONTEND_NODEJS_PROCESSES == 0)); then
|
||||
echo 'Need to modify /etc/bigbluebutton/nginx/bbb-html5.nginx to ensure backend IPs are used'
|
||||
fi
|
||||
for ((i = 1 ; i <= NUMBER_OF_FRONTEND_NODEJS_PROCESSES ; i++)); do
|
||||
systemctl start bbb-html5-frontend@$i
|
||||
done
|
||||
fi
|
@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Bridge whitelist, needed for dynamically importing bridges (as modules).
|
||||
*
|
||||
* The code is intentionally unreachable, but its trigger Meteor's static
|
||||
* analysis, which makes bridge module available to build process.
|
||||
*
|
||||
* For new bridges, we must append an import statement here.
|
||||
*
|
||||
* More information here:
|
||||
*https://docs.meteor.com/packages/dynamic-import.html
|
||||
*/
|
||||
|
||||
throw new Error();
|
||||
|
||||
/* eslint-disable no-unreachable */
|
||||
// BRIDGES LIST
|
@ -11,6 +11,7 @@ import getFromMeetingSettings from '/imports/ui/services/meeting-settings';
|
||||
|
||||
const SFU_URL = Meteor.settings.public.kurento.wsUrl;
|
||||
const DEFAULT_LISTENONLY_MEDIA_SERVER = Meteor.settings.public.kurento.listenOnlyMediaServer;
|
||||
const SIGNAL_CANDIDATES = Meteor.settings.public.kurento.signalCandidates;
|
||||
const MEDIA = Meteor.settings.public.media;
|
||||
const MEDIA_TAG = MEDIA.mediaTag.replace(/#/g, '');
|
||||
const GLOBAL_AUDIO_PREFIX = 'GLOBAL_AUDIO_';
|
||||
@ -265,6 +266,7 @@ export default class KurentoAudioBridge extends BaseAudioBridge {
|
||||
iceServers,
|
||||
offering: OFFERING,
|
||||
mediaServer: getMediaServerAdapter(),
|
||||
signalCandidates: SIGNAL_CANDIDATES,
|
||||
};
|
||||
|
||||
this.broker = new ListenOnlyBroker(
|
||||
@ -303,3 +305,5 @@ export default class KurentoAudioBridge extends BaseAudioBridge {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = KurentoAudioBridge;
|
||||
|
@ -98,6 +98,7 @@ class SIPSession {
|
||||
this._hangupFlag = false;
|
||||
this._reconnecting = false;
|
||||
this._currentSessionState = null;
|
||||
this._ignoreCallState = false;
|
||||
}
|
||||
|
||||
get inputStream() {
|
||||
@ -226,6 +227,24 @@ class SIPSession {
|
||||
this._outputDeviceId = deviceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* This _ignoreCallState flag is set to true when we want to ignore SIP's
|
||||
* call state retrieved directly from FreeSWITCH ESL, when doing some checks
|
||||
* (for example , when checking if call stopped).
|
||||
* We need to ignore this , for example, when moderator is in
|
||||
* breakout audio transfer ("Join Audio" button in breakout panel): in this
|
||||
* case , we will monitor moderator's lifecycle in audio conference by
|
||||
* using the SIP state taken from SIP.js only (ignoring the ESL's call state).
|
||||
* @param {boolean} value true to ignore call state, false otherwise.
|
||||
*/
|
||||
set ignoreCallState(value) {
|
||||
this._ignoreCallState = value;
|
||||
}
|
||||
|
||||
get ignoreCallState() {
|
||||
return this._ignoreCallState;
|
||||
}
|
||||
|
||||
joinAudio({
|
||||
isListenOnly,
|
||||
extension,
|
||||
@ -236,6 +255,8 @@ class SIPSession {
|
||||
return new Promise((resolve, reject) => {
|
||||
const callExtension = extension ? `${extension}${this.userData.voiceBridge}` : this.userData.voiceBridge;
|
||||
|
||||
this.ignoreCallState = false;
|
||||
|
||||
const callback = (message) => {
|
||||
// There will sometimes we erroneous errors put out like timeouts and improper shutdowns,
|
||||
// but only the first error ever matters
|
||||
@ -1068,7 +1089,9 @@ class SIPSession {
|
||||
};
|
||||
|
||||
const checkIfCallStopped = (message) => {
|
||||
if (fsReady || !sessionTerminated) return null;
|
||||
if ((!this.ignoreCallState && fsReady) || !sessionTerminated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!message && !!this.userRequestedHangup) {
|
||||
return this.callback({
|
||||
@ -1350,6 +1373,20 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
return this.activeSession ? this.activeSession.inputStream : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for SIPSession's ignoreCallState flag
|
||||
* @param {boolean} value
|
||||
*/
|
||||
set ignoreCallState(value) {
|
||||
if (this.activeSession) {
|
||||
this.activeSession.ignoreCallState = value;
|
||||
}
|
||||
}
|
||||
|
||||
get ignoreCallState() {
|
||||
return this.activeSession ? this.activeSession.ignoreCallState : false;
|
||||
}
|
||||
|
||||
joinAudio({ isListenOnly, extension, validIceCandidates }, managerCallback) {
|
||||
const hasFallbackDomain = typeof IPV4_FALLBACK_DOMAIN === 'string' && IPV4_FALLBACK_DOMAIN !== '';
|
||||
|
||||
@ -1495,3 +1532,5 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
return this.activeSession.updateAudioConstraints(constraints);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SIPBridge;
|
||||
|
@ -16,9 +16,8 @@ export default function handleBreakoutJoinURL({ body }) {
|
||||
};
|
||||
|
||||
const modifier = {
|
||||
$push: {
|
||||
users: {
|
||||
userId,
|
||||
$set: {
|
||||
[`url_${userId}`]: {
|
||||
redirectToHtml5JoinURL,
|
||||
insertedTime: new Date().getTime(),
|
||||
},
|
||||
@ -26,13 +25,27 @@ export default function handleBreakoutJoinURL({ body }) {
|
||||
};
|
||||
|
||||
try {
|
||||
const { insertedId, numberAffected } = Breakouts.upsert(selector, modifier);
|
||||
const ATTEMPT_EVERY_MS = 1000;
|
||||
|
||||
if (insertedId) {
|
||||
Logger.info(`Added breakout id=${breakoutId}`);
|
||||
} else if (numberAffected) {
|
||||
let numberAffected = 0;
|
||||
|
||||
const updateBreakout = Meteor.bindEnvironment(() => {
|
||||
numberAffected = Breakouts.update(selector, modifier);
|
||||
});
|
||||
|
||||
const updateBreakoutPromise = new Promise((resolve) => {
|
||||
const updateBreakoutInterval = setInterval(() => {
|
||||
updateBreakout();
|
||||
|
||||
if (numberAffected) {
|
||||
resolve(clearInterval(updateBreakoutInterval));
|
||||
}
|
||||
}, ATTEMPT_EVERY_MS);
|
||||
});
|
||||
|
||||
updateBreakoutPromise.then(() => {
|
||||
Logger.info(`Upserted breakout id=${breakoutId}`);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
Logger.error(`Adding breakout to collection: ${err}`);
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ export default function handleBreakoutRoomStarted({ body }, meetingId) {
|
||||
const modifier = {
|
||||
$set: Object.assign(
|
||||
{
|
||||
users: [],
|
||||
joinedUsers: [],
|
||||
},
|
||||
{ timeRemaining: DEFAULT_TIME_REMAINING },
|
||||
|
@ -3,10 +3,11 @@ import Breakouts from '/imports/api/breakouts';
|
||||
import Users from '/imports/api/users';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import AuthTokenValidation, { ValidationStates } from '/imports/api/auth-token-validation';
|
||||
import { publicationSafeGuard } from '/imports/api/common/server/helpers';
|
||||
|
||||
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
|
||||
|
||||
function breakouts(role) {
|
||||
function breakouts() {
|
||||
const tokenValidation = AuthTokenValidation.findOne({ connectionId: this.connection.id });
|
||||
|
||||
if (!tokenValidation || tokenValidation.validationStatus !== ValidationStates.VALIDATED) {
|
||||
@ -25,7 +26,19 @@ function breakouts(role) {
|
||||
{ breakoutId: meetingId },
|
||||
],
|
||||
};
|
||||
// Monitor this publication and stop it when user is not a moderator anymore
|
||||
const comparisonFunc = () => {
|
||||
const user = Users.findOne({ userId, meetingId }, { fields: { role: 1, userId: 1 } });
|
||||
const condition = user.role === ROLE_MODERATOR;
|
||||
|
||||
if (!condition) {
|
||||
Logger.info(`conditions aren't filled anymore in publication ${this._name}:
|
||||
user.role === ROLE_MODERATOR :${condition}, user.role: ${user.role} ROLE_MODERATOR: ${ROLE_MODERATOR}`);
|
||||
}
|
||||
|
||||
return condition;
|
||||
};
|
||||
publicationSafeGuard(comparisonFunc, this);
|
||||
return Breakouts.find(presenterSelector);
|
||||
}
|
||||
|
||||
@ -37,7 +50,7 @@ function breakouts(role) {
|
||||
},
|
||||
{
|
||||
parentMeetingId: meetingId,
|
||||
'users.userId': userId,
|
||||
[`url_${userId}`]: { $exists: true },
|
||||
},
|
||||
{
|
||||
breakoutId: meetingId,
|
||||
@ -47,12 +60,7 @@ function breakouts(role) {
|
||||
|
||||
const fields = {
|
||||
fields: {
|
||||
users: {
|
||||
$elemMatch: {
|
||||
// do not allow users to obtain 'redirectToHtml5JoinURL' for others
|
||||
userId,
|
||||
},
|
||||
},
|
||||
[`url_${userId}`]: 1,
|
||||
breakoutId: 1,
|
||||
externalId: 1,
|
||||
freeJoin: 1,
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import takeOwnership from '/imports/api/captions/server/methods/takeOwnership';
|
||||
import appendText from '/imports/api/captions/server/methods/appendText';
|
||||
import getPadId from '/imports/api/captions/server/methods/getPadId';
|
||||
|
||||
Meteor.methods({
|
||||
takeOwnership,
|
||||
appendText,
|
||||
getPadId,
|
||||
});
|
||||
|
@ -2,21 +2,22 @@ import axios from 'axios';
|
||||
import { check } from 'meteor/check';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import Captions from '/imports/api/captions';
|
||||
import { CAPTIONS_TOKEN } from '/imports/api/captions/server/helpers';
|
||||
import { appendTextURL } from '/imports/api/common/server/etherpad';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
|
||||
export default function appendText(text, locale) {
|
||||
try {
|
||||
const { meetingId } = extractCredentials(this.userId);
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
check(text, String);
|
||||
check(locale, String);
|
||||
|
||||
const captions = Captions.findOne({
|
||||
meetingId,
|
||||
padId: { $regex: `${CAPTIONS_TOKEN}${locale}$` },
|
||||
locale,
|
||||
ownerId: requesterUserId,
|
||||
});
|
||||
|
||||
if (!captions) {
|
||||
|
@ -35,7 +35,7 @@ export default function createCaptions(meetingId, instanceId) {
|
||||
const locales = response.data;
|
||||
locales.forEach((locale) => {
|
||||
const padId = withInstaceId(instanceId, generatePadId(meetingId, locale.locale));
|
||||
addCaption(meetingId, padId, locale);
|
||||
addCaption(meetingId, padId, locale.locale, locale.name);
|
||||
padIds.push(padId);
|
||||
});
|
||||
addCaptionsPads(meetingId, padIds);
|
||||
|
@ -26,20 +26,22 @@ export default function editCaptions(padId, data) {
|
||||
meetingId,
|
||||
ownerId,
|
||||
locale,
|
||||
name,
|
||||
length,
|
||||
} = pad;
|
||||
|
||||
check(meetingId, String);
|
||||
check(ownerId, String);
|
||||
check(locale, { locale: String, name: String });
|
||||
check(locale, String);
|
||||
check(name, String);
|
||||
check(length, Number);
|
||||
|
||||
const index = getIndex(data, length);
|
||||
|
||||
const payload = {
|
||||
startIndex: index,
|
||||
localeCode: locale.locale,
|
||||
locale: locale.name,
|
||||
localeCode: locale,
|
||||
locale: name,
|
||||
endIndex: index,
|
||||
text: data,
|
||||
};
|
||||
|
@ -0,0 +1,49 @@
|
||||
import Captions from '/imports/api/captions';
|
||||
import Users from '/imports/api/users';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
|
||||
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
|
||||
|
||||
const hasPadAccess = (meetingId, userId) => {
|
||||
const user = Users.findOne(
|
||||
{ meetingId, userId },
|
||||
{ fields: { role: 1 }},
|
||||
);
|
||||
|
||||
if (!user) return false;
|
||||
|
||||
if (user.role === ROLE_MODERATOR) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export default function getPadId(locale) {
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
const caption = Captions.findOne(
|
||||
{ meetingId, locale },
|
||||
{
|
||||
fields: {
|
||||
padId: 1,
|
||||
ownerId: 1,
|
||||
readOnlyPadId: 1,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (caption) {
|
||||
if (hasPadAccess(meetingId, requesterUserId)) {
|
||||
if (requesterUserId === caption.ownerId) return caption.padId;
|
||||
|
||||
return caption.readOnlyPadId;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (err) {
|
||||
return null;;
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@ import { check } from 'meteor/check';
|
||||
import Captions from '/imports/api/captions';
|
||||
import updateOwnerId from '/imports/api/captions/server/modifiers/updateOwnerId';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
import { CAPTIONS_TOKEN } from '/imports/api/captions/server/helpers';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default function takeOwnership(locale) {
|
||||
@ -10,12 +9,10 @@ export default function takeOwnership(locale) {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
check(locale, String);
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
|
||||
const pad = Captions.findOne({ meetingId, padId: { $regex: `${CAPTIONS_TOKEN}${locale}$` } });
|
||||
|
||||
if (pad) {
|
||||
updateOwnerId(meetingId, requesterUserId, pad.padId);
|
||||
}
|
||||
updateOwnerId(meetingId, requesterUserId, locale);
|
||||
} catch (err) {
|
||||
Logger.error(`Exception while invoking method takeOwnership ${err.stack}`);
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import Logger from '/imports/startup/server/logger';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { check } from 'meteor/check';
|
||||
|
||||
export default function updateOwner(meetingId, userId, padId) { // TODO
|
||||
export default function updateOwner(meetingId, userId, locale) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'UpdateCaptionOwnerPubMsg';
|
||||
@ -12,23 +12,19 @@ export default function updateOwner(meetingId, userId, padId) { // TODO
|
||||
try {
|
||||
check(meetingId, String);
|
||||
check(userId, String);
|
||||
check(padId, String);
|
||||
check(locale, String);
|
||||
|
||||
const pad = Captions.findOne({ meetingId, padId });
|
||||
const pad = Captions.findOne({ meetingId, locale });
|
||||
|
||||
if (!pad) {
|
||||
Logger.error(`Editing captions owner: ${padId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { locale } = pad;
|
||||
|
||||
check(locale, { locale: String, name: String });
|
||||
|
||||
const payload = {
|
||||
ownerId: userId,
|
||||
locale: locale.name,
|
||||
localeCode: locale.locale,
|
||||
locale: pad.name,
|
||||
localeCode: pad.locale,
|
||||
};
|
||||
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, userId, payload);
|
||||
|
@ -2,13 +2,11 @@ import { check } from 'meteor/check';
|
||||
import Captions from '/imports/api/captions';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default function addCaption(meetingId, padId, locale) {
|
||||
export default function addCaption(meetingId, padId, locale, name) {
|
||||
check(meetingId, String);
|
||||
check(padId, String);
|
||||
check(locale, {
|
||||
locale: String,
|
||||
name: String,
|
||||
});
|
||||
check(locale, String);
|
||||
check(name, String);
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
@ -19,6 +17,7 @@ export default function addCaption(meetingId, padId, locale) {
|
||||
meetingId,
|
||||
padId,
|
||||
locale,
|
||||
name,
|
||||
ownerId: '',
|
||||
readOnlyPadId: '',
|
||||
data: '',
|
||||
@ -30,9 +29,9 @@ export default function addCaption(meetingId, padId, locale) {
|
||||
const { insertedId, numberAffected } = Captions.upsert(selector, modifier);
|
||||
|
||||
if (insertedId) {
|
||||
Logger.verbose('Captions: added locale', { locale: locale.locale, meetingId });
|
||||
Logger.verbose('Captions: added locale', { locale, meetingId });
|
||||
} else if (numberAffected) {
|
||||
Logger.verbose('Captions: upserted locale', { locale: locale.locale, meetingId });
|
||||
Logger.verbose('Captions: upserted locale', { locale, meetingId });
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Adding caption to collection: ${err}`);
|
||||
|
@ -3,14 +3,14 @@ import Logger from '/imports/startup/server/logger';
|
||||
import updateOwner from '/imports/api/captions/server/methods/updateOwner';
|
||||
import { check } from 'meteor/check';
|
||||
|
||||
export default function updateOwnerId(meetingId, userId, padId) {
|
||||
export default function updateOwnerId(meetingId, userId, locale) {
|
||||
check(meetingId, String);
|
||||
check(userId, String);
|
||||
check(padId, String);
|
||||
check(locale, String);
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
padId,
|
||||
locale,
|
||||
};
|
||||
|
||||
const modifier = {
|
||||
@ -23,8 +23,8 @@ export default function updateOwnerId(meetingId, userId, padId) {
|
||||
const numberAffected = Captions.update(selector, modifier, { multi: true });
|
||||
|
||||
if (numberAffected) {
|
||||
updateOwner(meetingId, userId, padId);
|
||||
Logger.verbose('Captions: updated caption', { padId, ownerId: userId });
|
||||
updateOwner(meetingId, userId, locale);
|
||||
Logger.verbose('Captions: updated caption', { locale, ownerId: userId });
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error('Captions: error while updating pad', { err });
|
||||
|
@ -14,7 +14,14 @@ function captions() {
|
||||
const { meetingId, userId } = tokenValidation;
|
||||
Logger.debug('Publishing Captions', { meetingId, requestedBy: userId });
|
||||
|
||||
return Captions.find({ meetingId });
|
||||
const options = {
|
||||
fields: {
|
||||
padId: 0,
|
||||
readOnlyPadId: 0,
|
||||
},
|
||||
};
|
||||
|
||||
return Captions.find({ meetingId }, options);
|
||||
}
|
||||
|
||||
function publish(...args) {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import Users from '/imports/api/users';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
const MSG_DIRECT_TYPE = 'DIRECT';
|
||||
const NODE_USER = 'nodeJSapp';
|
||||
@ -21,7 +22,7 @@ export const indexOf = [].indexOf || function (item) {
|
||||
return -1;
|
||||
};
|
||||
|
||||
export const processForHTML5ServerOnly = fn => (message, ...args) => {
|
||||
export const processForHTML5ServerOnly = (fn) => (message, ...args) => {
|
||||
const { envelope } = message;
|
||||
const { routing } = envelope;
|
||||
const { msgType, meetingId, userId } = routing;
|
||||
@ -45,3 +46,23 @@ export const extractCredentials = (credentials) => {
|
||||
const requesterUserId = credentialsArray[1];
|
||||
return { meetingId, requesterUserId };
|
||||
};
|
||||
|
||||
// Creates a background job to periodically check the result of the provided function.
|
||||
// The provided function is publication-specific and must check the "survival condition" of the publication.
|
||||
export const publicationSafeGuard = function (fn, self) {
|
||||
let stopped = false;
|
||||
const periodicCheck = function () {
|
||||
if (stopped) return;
|
||||
if (!fn()) {
|
||||
self.added(self._name, 'publication-stop-marker', { id: 'publication-stop-marker', stopped: true });
|
||||
self.stop();
|
||||
} else Meteor.setTimeout(periodicCheck, 1000);
|
||||
};
|
||||
|
||||
self.onStop(() => {
|
||||
stopped = true;
|
||||
Logger.info(`Publication ${self._name} has stopped in server side`);
|
||||
});
|
||||
|
||||
periodicCheck();
|
||||
};
|
||||
|
@ -8,4 +8,9 @@ if (Meteor.isServer) {
|
||||
UsersTyping._ensureIndex({ meetingId: 1, isTypingTo: 1 });
|
||||
}
|
||||
|
||||
// As we store chat in context, skip adding to mini mongo
|
||||
if (Meteor.isClient) {
|
||||
GroupChatMsg.onAdded = () => false;
|
||||
}
|
||||
|
||||
export { GroupChatMsg, UsersTyping };
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user