Merge remote-tracking branch 'upstream/develop' into PR-11359

This commit is contained in:
Ramón Souza 2021-10-19 16:41:36 +00:00
commit 1cc066366f
441 changed files with 34636 additions and 8504 deletions

View File

@ -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

View File

@ -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 {

View File

@ -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)
}

View File

@ -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)
}
}
}

View File

@ -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")

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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 = "";

View File

@ -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 {};
}

View File

@ -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 {};

View File

@ -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 {};
}

View File

@ -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 {};
}

View File

@ -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 {};

View File

@ -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 {};
}

View File

@ -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 {};
}

View File

@ -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 {};

View File

@ -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 {};

View File

@ -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 {};
}

View File

@ -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 {};
}

View File

@ -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 {};
}

View File

@ -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 {};

View File

@ -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;
}
}
}

View File

@ -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 {};
}

View File

@ -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;
}

View File

@ -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 {};

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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)")

View File

@ -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) {

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,4 @@
package org.bigbluebutton.api.model.shared;
public interface ChecksumValidationGroup {
}

View File

@ -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")

View File

@ -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; }

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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; }

View File

@ -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

View File

@ -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();

View File

@ -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>();

View File

@ -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;
}

File diff suppressed because it is too large Load Diff

View File

@ -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;
}

View File

@ -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>
&nbsp;&nbsp;
{
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" />

View 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;

View File

@ -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>
&nbsp;&nbsp;
<div>
<p className="font-semibold">{user.name}</p>
</div>

View File

@ -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>
&nbsp;&nbsp;
<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
? (

View File

@ -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>
&nbsp;&nbsp;&nbsp;
<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>
&nbsp;
{ 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>
&nbsp;
{ 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>
&nbsp;
{ 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>
&nbsp;
{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`} />
&nbsp;
{ 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>
&nbsp;
{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>
&nbsp;
<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>
)}

View File

@ -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;

View File

@ -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

View File

@ -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

View 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

View File

@ -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>

View File

@ -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

View File

@ -1 +1 @@
BIGBLUEBUTTON_RELEASE=2.4-rc-1
BIGBLUEBUTTON_RELEASE=2.4-rc-2

View File

@ -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)"

View File

@ -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

View File

@ -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

View File

@ -1 +1 @@
METEOR@1.10.2
METEOR@2.3.6

View File

@ -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

View File

@ -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"/>

View File

@ -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) => {

View File

@ -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";
}

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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}`);
}

View File

@ -23,7 +23,6 @@ export default function handleBreakoutRoomStarted({ body }, meetingId) {
const modifier = {
$set: Object.assign(
{
users: [],
joinedUsers: [],
},
{ timeRemaining: DEFAULT_TIME_REMAINING },

View File

@ -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,

View File

@ -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,
});

View File

@ -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) {

View File

@ -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);

View File

@ -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,
};

View File

@ -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;;
}
}

View File

@ -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}`);
}

View File

@ -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);

View File

@ -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}`);

View File

@ -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 });

View File

@ -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) {

View File

@ -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();
};

View File

@ -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