Merge branch 'v2.5.x-release' into default-nginx-config

This commit is contained in:
Anton Georgiev 2022-05-13 10:23:38 -04:00 committed by GitHub
commit fd5d3cbfac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
306 changed files with 4739 additions and 4504 deletions

View File

@ -1,2 +0,0 @@
Dockerfile

View File

@ -48,4 +48,5 @@ lib_managed/
.cache
bin/
src/main/resources/
.bsp/

View File

@ -1,24 +0,0 @@
FROM bbb-common-message AS builder
ARG COMMON_VERSION=0.0.1-SNAPSHOT
COPY . /source
RUN cd /source \
&& find -name build.sbt -exec sed -i "s|\(.*org.bigbluebutton.*bbb-common-message[^\"]*\"[ ]*%[ ]*\)\"[^\"]*\"\(.*\)|\1\"$COMMON_VERSION\"\2|g" {} \; \
&& sbt compile
RUN apt-get update \
&& apt-get -y install fakeroot
RUN cd /source \
&& sbt debian:packageBin
# FROM ubuntu:16.04
FROM openjdk:8-jre-slim-stretch
COPY --from=builder /source/target/*.deb /root/
RUN dpkg -i /root/*.deb
CMD ["/usr/share/bbb-apps-akka/bin/bbb-apps-akka"]

View File

@ -15,12 +15,13 @@ trait PresentationConversionCompletedSysPubMsgHdlr {
): MeetingState2x = {
val meetingId = liveMeeting.props.meetingProp.intId
val temporaryPresentationId = msg.body.presentation.temporaryPresentationId
val newState = for {
pod <- PresentationPodsApp.getPresentationPod(state, msg.body.podId)
pres <- pod.getPresentation(msg.body.presentation.id)
} yield {
val presVO = PresentationPodsApp.translatePresentationToPresentationVO(pres)
val presVO = PresentationPodsApp.translatePresentationToPresentationVO(pres, temporaryPresentationId)
PresentationSender.broadcastPresentationConversionCompletedEvtMsg(
bus,

View File

@ -58,7 +58,7 @@ object PresentationPodsApp {
)
}
PresentationVO(p.id, p.name, p.current,
PresentationVO(p.id, "", p.name, p.current,
pages.toVector, p.downloadable, p.removable)
}
@ -74,7 +74,7 @@ object PresentationPodsApp {
state.update(podManager)
}
def translatePresentationToPresentationVO(pres: PresentationInPod): PresentationVO = {
def translatePresentationToPresentationVO(pres: PresentationInPod, temporaryPresentationId: String): PresentationVO = {
val pages = pres.pages.values.map { page =>
PageVO(
id = page.id,
@ -90,7 +90,7 @@ object PresentationPodsApp {
heightRatio = page.heightRatio
)
}
PresentationVO(pres.id, pres.name, pres.current, pages.toVector, pres.downloadable, pres.removable)
PresentationVO(pres.id, temporaryPresentationId, pres.name, pres.current, pages.toVector, pres.downloadable, pres.removable)
}
def setCurrentPresentationInPod(state: MeetingState2x, podId: String, nextCurrentPresId: String): Option[PresentationPod] = {

View File

@ -19,7 +19,7 @@ trait PresentationUploadTokenReqMsgHdlr extends RightsManagementTrait {
val envelope = BbbCoreEnvelope(PresentationUploadTokenPassRespMsg.NAME, routing)
val header = BbbClientMsgHeader(PresentationUploadTokenPassRespMsg.NAME, liveMeeting.props.meetingProp.intId, msg.header.userId)
val body = PresentationUploadTokenPassRespMsgBody(msg.body.podId, token, msg.body.filename)
val body = PresentationUploadTokenPassRespMsgBody(msg.body.podId, token, msg.body.filename, msg.body.tmpPresId)
val event = PresentationUploadTokenPassRespMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
bus.outGW.send(msgEvent)

View File

@ -50,7 +50,6 @@ trait SelectRandomViewerReqMsgHdlr extends RightsManagementTrait {
Users2x.setUserExempted(liveMeeting.users2x, pickedUser, true)
}
}
val userIds = users.map { case (v) => v.intId }
broadcastEvent(msg, userIds, pickedUser)
}

View File

@ -15,12 +15,12 @@ trait UserJoinMeetingAfterReconnectReqMsgHdlr extends HandlerHelpers with UserJo
def handleUserJoinMeetingAfterReconnectReqMsg(msg: UserJoinMeetingAfterReconnectReqMsg, state: MeetingState2x): MeetingState2x = {
log.info("Received user joined after reconnecting. user {} meetingId={}", msg.body.userId, msg.header.meetingId)
Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId) match {
case Some(reconnectingUser) =>
if (reconnectingUser.userLeftFlag.left) {
log.info("Resetting flag that user left meeting. user {}", msg.body.userId)
// User has reconnected. Just reset it's flag. ralam Oct 23, 2018
sendUserLeftFlagUpdatedEvtMsg(outGW, liveMeeting, msg.body.userId, false)
Users2x.resetUserLeftFlag(liveMeeting.users2x, msg.body.userId)
}
state

View File

@ -2,9 +2,9 @@ package org.bigbluebutton.core.apps.users
import org.bigbluebutton.common2.msgs.UserJoinMeetingReqMsg
import org.bigbluebutton.core.apps.breakout.BreakoutHdlrHelpers
import org.bigbluebutton.core.models.{ Users2x, VoiceUsers }
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting, MeetingActor, OutMsgRouter }
import org.bigbluebutton.core.models.{Users2x, VoiceUsers}
import org.bigbluebutton.core.running.{HandlerHelpers, LiveMeeting, MeetingActor, OutMsgRouter}
trait UserJoinMeetingReqMsgHdlr extends HandlerHelpers {
this: MeetingActor =>
@ -20,8 +20,10 @@ trait UserJoinMeetingReqMsgHdlr extends HandlerHelpers {
if (reconnectingUser.userLeftFlag.left) {
log.info("Resetting flag that user left meeting. user {}", msg.body.userId)
// User has reconnected. Just reset it's flag. ralam Oct 23, 2018
sendUserLeftFlagUpdatedEvtMsg(outGW, liveMeeting, msg.body.userId, false)
Users2x.resetUserLeftFlag(liveMeeting.users2x, msg.body.userId)
}
state
case None =>
val newState = userJoinMeeting(outGW, msg.body.authToken, msg.body.clientType, liveMeeting, state)

View File

@ -3,9 +3,9 @@ package org.bigbluebutton.core.apps.users
import org.bigbluebutton.common2.msgs.UserLeaveReqMsg
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.models.{ RegisteredUsers, Users2x }
import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter }
import org.bigbluebutton.core.running.{ HandlerHelpers, MeetingActor, OutMsgRouter }
trait UserLeaveReqMsgHdlr {
trait UserLeaveReqMsgHdlr extends HandlerHelpers {
this: MeetingActor =>
val outGW: OutMsgRouter
@ -19,6 +19,8 @@ trait UserLeaveReqMsgHdlr {
// Just flag that user has left as the user might be reconnecting.
// An audit will remove this user if it hasn't rejoined after a certain period of time.
// ralam oct 23, 2018
sendUserLeftFlagUpdatedEvtMsg(outGW, liveMeeting, msg.body.userId, true)
Users2x.setUserLeftFlag(liveMeeting.users2x, msg.body.userId)
}
if (msg.body.loggedOut) {

View File

@ -2,6 +2,7 @@ package org.bigbluebutton.core.models
import com.softwaremill.quicklens._
import org.bigbluebutton.core.util.TimeUtil
import org.bigbluebutton.core2.message.senders.MsgBuilder
object Users2x {
def findWithIntId(users: Users2x, intId: String): Option[UserState] = {

View File

@ -0,0 +1,34 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.core.record.events
class MeetingConfigurationEvent extends StarterConfigurationEvent {
import MeetingConfigurationEvent._
setEvent("MeetingConfigurationEvent")
def setWebcamsOnlyForModerator(webcamsOnlyForModerator: Boolean) {
eventMap.put(WEBCAMS_ONLY_FOR_MODERATOR, webcamsOnlyForModerator.toString)
}
}
object MeetingConfigurationEvent {
val WEBCAMS_ONLY_FOR_MODERATOR = "webcamsOnlyForModerator"
}

View File

@ -0,0 +1,24 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.core.record.events
trait StarterConfigurationEvent extends RecordEvent {
setModule("CONFIG")
}

View File

@ -25,6 +25,20 @@ trait HandlerHelpers extends SystemConfiguration {
}
}
def sendUserLeftFlagUpdatedEvtMsg(
outGW: OutMsgRouter,
liveMeeting: LiveMeeting,
intId: String,
leftFlag: Boolean
): Unit = {
for {
u <- Users2x.findWithIntId(liveMeeting.users2x, intId)
} yield {
val userLeftFlagMeetingEvent = MsgBuilder.buildUserLeftFlagUpdatedEvtMsg(liveMeeting.props.meetingProp.intId, u.intId, leftFlag)
outGW.send(userLeftFlagMeetingEvent)
}
}
def userJoinMeeting(outGW: OutMsgRouter, authToken: String, clientType: String,
liveMeeting: LiveMeeting, state: MeetingState2x): MeetingState2x = {

View File

@ -286,6 +286,16 @@ object MsgBuilder {
BbbCommonEnvCoreMsg(envelope, event)
}
def buildUserLeftFlagUpdatedEvtMsg(meetingId: String, userId: String, userLeftFlag: Boolean): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId)
val envelope = BbbCoreEnvelope(UserLeftFlagUpdatedEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(UserLeftFlagUpdatedEvtMsg.NAME, meetingId, userId)
val body = UserLeftFlagUpdatedEvtMsgBody(userId, userLeftFlag)
val event = UserLeftFlagUpdatedEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
def buildUserInactivityInspectMsg(meetingId: String, userId: String, responseDelay: Long): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, userId)
val envelope = BbbCoreEnvelope(UserInactivityInspectMsg.NAME, routing)

View File

@ -116,6 +116,7 @@ class RedisRecorderActor(
case m: RecordStatusResetSysMsg => handleRecordStatusResetSysMsg(m)
case m: WebcamsOnlyForModeratorChangedEvtMsg => handleWebcamsOnlyForModeratorChangedEvtMsg(m)
case m: MeetingEndingEvtMsg => handleEndAndKickAllSysMsg(m)
case m: MeetingCreatedEvtMsg => handleStarterConfigurations(m)
// Recording
case m: RecordingChapterBreakSysMsg => handleRecordingChapterBreakSysMsg(m)
@ -622,4 +623,10 @@ class RedisRecorderActor(
log.error("recording database is not available.")
}
private def handleStarterConfigurations(msg: MeetingCreatedEvtMsg): Unit = {
val ev = new MeetingConfigurationEvent()
ev.setWebcamsOnlyForModerator(msg.body.props.usersProp.webcamsOnlyForModerator)
record(msg.body.props.meetingProp.intId, ev.toMap().asJava)
}
}

View File

@ -1,26 +0,0 @@
FROM bbb-fsesl-client AS builder
ARG COMMON_VERSION=0.0.1-SNAPSHOT
COPY . /source
RUN cd /source \
&& find -name build.sbt -exec sed -i "s|\(.*org.bigbluebutton.*bbb-common-message[^\"]*\"[ ]*%[ ]*\)\"[^\"]*\"\(.*\)|\1\"$COMMON_VERSION\"\2|g" {} \; \
&& find -name build.sbt -exec sed -i "s|\(.*org.bigbluebutton.*bbb-fsesl-client[^\"]*\"[ ]*%[ ]*\)\"[^\"]*\"\(.*\)|\1\"$COMMON_VERSION\"\2|g" {} \; \
&& sbt compile
RUN apt-get update \
&& apt-get -y install fakeroot
RUN cd /source \
&& sbt debian:packageBin
FROM openjdk:8-jre-slim-stretch
COPY --from=builder /source/target/*.deb /root/
RUN dpkg -i /root/*.deb
COPY wait-for-it.sh /usr/local/bin/
CMD ["/usr/share/bbb-fsesl-akka/bin/bbb-fsesl-akka"]

View File

@ -53,4 +53,5 @@ akka-patterns-store/
lib_managed/
.cache
bin/
.bsp/

View File

@ -1,13 +0,0 @@
FROM sbt:0.13.8
ARG COMMON_VERSION
COPY . /bbb-common-message
RUN cd /bbb-common-message \
&& sed -i "s|\(version := \)\".*|\1\"$COMMON_VERSION\"|g" build.sbt \
&& echo 'publishTo := Some(Resolver.file("file", new File(Path.userHome.absolutePath+"/.m2/repository")))' | tee -a build.sbt \
&& sbt compile \
&& sbt publish \
&& sbt publishLocal

View File

@ -1,6 +1,6 @@
package org.bigbluebutton.common2.domain
case class PresentationVO(id: String, name: String, current: Boolean = false,
case class PresentationVO(id: String, temporaryPresentationId: String, name: String, current: Boolean = false,
pages: Vector[PageVO], downloadable: Boolean, removable: Boolean)
case class PageVO(id: String, num: Int, thumbUri: String = "", swfUri: String,

View File

@ -13,7 +13,7 @@ case class RemovePresentationPodPubMsgBody(podId: String)
object PresentationUploadTokenReqMsg { val NAME = "PresentationUploadTokenReqMsg" }
case class PresentationUploadTokenReqMsg(header: BbbClientMsgHeader, body: PresentationUploadTokenReqMsgBody) extends StandardMsg
case class PresentationUploadTokenReqMsgBody(podId: String, filename: String)
case class PresentationUploadTokenReqMsgBody(podId: String, filename: String, tmpPresId: String)
object GetAllPresentationPodsReqMsg { val NAME = "GetAllPresentationPodsReqMsg" }
case class GetAllPresentationPodsReqMsg(header: BbbClientMsgHeader, body: GetAllPresentationPodsReqMsgBody) extends StandardMsg
@ -113,13 +113,14 @@ case class PresentationConversionRequestReceivedSysMsg(
body: PresentationConversionRequestReceivedSysMsgBody
) extends StandardMsg
case class PresentationConversionRequestReceivedSysMsgBody(
podId: String,
presentationId: String,
current: Boolean,
presName: String,
downloadable: Boolean,
removable: Boolean,
authzToken: String
podId: String,
presentationId: String,
temporaryPresentationId: String,
current: Boolean,
presName: String,
downloadable: Boolean,
removable: Boolean,
authzToken: String
)
object PresentationPageConversionStartedSysMsg { val NAME = "PresentationPageConversionStartedSysMsg" }
@ -181,7 +182,7 @@ case class PdfConversionInvalidErrorEvtMsgBody(podId: String, messageKey: String
object PresentationUploadTokenPassRespMsg { val NAME = "PresentationUploadTokenPassRespMsg" }
case class PresentationUploadTokenPassRespMsg(header: BbbClientMsgHeader, body: PresentationUploadTokenPassRespMsgBody) extends StandardMsg
case class PresentationUploadTokenPassRespMsgBody(podId: String, authzToken: String, filename: String)
case class PresentationUploadTokenPassRespMsgBody(podId: String, authzToken: String, filename: String, tmpPresId: String)
object PresentationUploadTokenFailRespMsg { val NAME = "PresentationUploadTokenFailRespMsg" }
case class PresentationUploadTokenFailRespMsg(header: BbbClientMsgHeader, body: PresentationUploadTokenFailRespMsgBody) extends StandardMsg

View File

@ -70,6 +70,20 @@ case class UserLeftMeetingEvtMsg(
) extends BbbCoreMsg
case class UserLeftMeetingEvtMsgBody(intId: String, eject: Boolean, ejectedBy: String, reason: String, reasonCode: String)
object UserLeftFlagUpdatedEvtMsg {
val NAME = "UserLeftFlagUpdatedEvtMsg"
def apply(meetingId: String, userId: String, body: UserLeftFlagUpdatedEvtMsgBody): UserLeftFlagUpdatedEvtMsg = {
val header = BbbClientMsgHeader(UserLeftFlagUpdatedEvtMsg.NAME, meetingId, userId)
UserLeftFlagUpdatedEvtMsg(header, body)
}
}
case class UserLeftFlagUpdatedEvtMsg(
header: BbbClientMsgHeader,
body: UserLeftFlagUpdatedEvtMsgBody
) extends BbbCoreMsg
case class UserLeftFlagUpdatedEvtMsgBody(intId: String, userLeftFlag: Boolean)
object UserJoinedMeetingEvtMsg {
val NAME = "UserJoinedMeetingEvtMsg"
def apply(meetingId: String, userId: String, body: UserJoinedMeetingEvtMsgBody): UserJoinedMeetingEvtMsg = {

View File

@ -53,4 +53,5 @@ akka-patterns-store/
lib_managed/
.cache
bin/
.bsp/

View File

@ -1,13 +0,0 @@
FROM bbb-common-message
ARG COMMON_VERSION
COPY . /bbb-common-web
RUN cd /bbb-common-web \
&& sed -i "s|\(version := \)\".*|\1\"$COMMON_VERSION\"|g" build.sbt \
&& find -name build.sbt -exec sed -i "s|\(.*org.bigbluebutton.*bbb-common-message[^\"]*\"[ ]*%[ ]*\)\"[^\"]*\"\(.*\)|\1\"$COMMON_VERSION\"\2|g" {} \; \
&& echo 'publishTo := Some(Resolver.file("file", new File(Path.userHome.absolutePath+"/.m2/repository")))' | tee -a build.sbt \
&& sbt compile \
&& sbt publish \
&& sbt publishLocal

View File

@ -60,6 +60,7 @@ import org.bigbluebutton.api.messaging.converters.messages.DeletedRecordingMessa
import org.bigbluebutton.api.messaging.messages.*;
import org.bigbluebutton.api2.IBbbWebApiGWApp;
import org.bigbluebutton.api2.domain.UploadedTrack;
import org.bigbluebutton.common2.msgs.MeetingCreatedEvtMsg;
import org.bigbluebutton.common2.redis.RedisStorageService;
import org.bigbluebutton.presentation.PresentationUrlDownloadService;
import org.bigbluebutton.presentation.imp.SwfSlidesGenerationProgressNotifier;

View File

@ -817,11 +817,11 @@ public class ParamsProcessorUtil {
return DigestUtils.sha1Hex(extMeetingId);
}
public String processPassword(String pass) {
return StringUtils.isEmpty(pass) ? RandomStringUtils.randomAlphanumeric(8) : pass;
}
public String processPassword(String pass) {
return StringUtils.isEmpty(pass) ? RandomStringUtils.randomAlphanumeric(8) : pass;
}
public boolean hasChecksumAndQueryString(String checksum, String queryString) {
public boolean hasChecksumAndQueryString(String checksum, String queryString) {
return (! StringUtils.isEmpty(checksum) && StringUtils.isEmpty(queryString));
}

View File

@ -2,6 +2,7 @@ package org.bigbluebutton.api;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -45,7 +46,8 @@ public final class Util {
public static String generatePresentationId(String presFilename) {
long timestamp = System.currentTimeMillis();
return DigestUtils.sha1Hex(presFilename) + "-" + timestamp;
String uuid = UUID.randomUUID().toString();
return DigestUtils.sha1Hex(presFilename + uuid) + "-" + timestamp;
}
public static String createNewFilename(String presId, String fileExt) {

View File

@ -49,8 +49,8 @@ public class Meeting {
private boolean forciblyEnded = false;
private String telVoice;
private String webVoice;
private String moderatorPass;
private String viewerPass;
private String moderatorPass = "";
private String viewerPass = "";
private int learningDashboardCleanupDelayInMinutes;
private String learningDashboardAccessToken;
private ArrayList<String> disabledFeatures;
@ -115,9 +115,17 @@ public class Meeting {
name = builder.name;
extMeetingId = builder.externalId;
intMeetingId = builder.internalId;
viewerPass = builder.viewerPass;
moderatorPass = builder.moderatorPass;
disabledFeatures = builder.disabledFeatures;
if (builder.viewerPass == null){
viewerPass = "";
} else {
viewerPass = builder.viewerPass;
}
if (builder.moderatorPass == null){
moderatorPass = "";
} else {
moderatorPass = builder.moderatorPass;
}
learningDashboardCleanupDelayInMinutes = builder.learningDashboardCleanupDelayInMinutes;
learningDashboardAccessToken = builder.learningDashboardAccessToken;
maxUsers = builder.maxUsers;

View File

@ -1,22 +0,0 @@
package org.bigbluebutton.api.model.constraint;
import org.bigbluebutton.api.model.validator.ModeratorPasswordValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Constraint(validatedBy = ModeratorPasswordValidator.class)
@Target(TYPE)
@Retention(RUNTIME)
public @interface ModeratorPasswordConstraint {
String key() default "invalidPassword";
String message() default "The supplied moderator password is incorrect";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -29,7 +29,6 @@ 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

@ -38,7 +38,6 @@ public class JoinMeeting extends RequestWithChecksum<JoinMeeting.Params> {
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

@ -1,6 +1,3 @@
package org.bigbluebutton.api.model.shared;
import org.bigbluebutton.api.model.constraint.ModeratorPasswordConstraint;
@ModeratorPasswordConstraint
public class ModeratorPassword extends Password {}

View File

@ -7,7 +7,6 @@ public abstract class Password {
@NotEmpty(message = "You must provide the meeting ID")
protected String meetingID;
@NotEmpty(message = "You must provide the password for the call")
protected String password;
public String getMeetingID() {

View File

@ -36,18 +36,10 @@ public class JoinPasswordValidator implements ConstraintValidator<JoinPasswordCo
String attendeePassword = meeting.getViewerPassword();
String providedPassword = joinPassword.getPassword();
if(providedPassword == null) {
return false;
}
log.info("Moderator password: {}", moderatorPassword);
log.info("Attendee password: {}", attendeePassword);
log.info("Provided password: {}", providedPassword);
if(!providedPassword.equals(moderatorPassword) && !providedPassword.equals(attendeePassword)) {
return false;
}
return true;
}
}

View File

@ -1,52 +0,0 @@
package org.bigbluebutton.api.model.validator;
import org.bigbluebutton.api.domain.Meeting;
import org.bigbluebutton.api.model.constraint.ModeratorPasswordConstraint;
import org.bigbluebutton.api.model.shared.ModeratorPassword;
import org.bigbluebutton.api.service.ServiceUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class ModeratorPasswordValidator implements ConstraintValidator<ModeratorPasswordConstraint, ModeratorPassword> {
private static Logger log = LoggerFactory.getLogger(ModeratorPasswordValidator.class);
@Override
public void initialize(ModeratorPasswordConstraint constraintAnnotation) {}
@Override
public boolean isValid(ModeratorPassword moderatorPassword, ConstraintValidatorContext context) {
log.info("Validating password {} for meeting with ID {}",
moderatorPassword.getPassword(), moderatorPassword.getMeetingID());
if(moderatorPassword.getMeetingID() == null) {
return false;
}
Meeting meeting = ServiceUtils.findMeetingFromMeetingID(moderatorPassword.getMeetingID());
if(meeting == null) {
return false;
}
String actualPassword = meeting.getModeratorPassword();
String providedPassword = moderatorPassword.getPassword();
if(providedPassword == null) {
return false;
}
log.info("Actual password: {}", actualPassword);
log.info("Provided password: {}", providedPassword);
if(!providedPassword.equals(actualPassword)) {
return false;
}
return true;
}
}

View File

@ -19,15 +19,11 @@ public class PasswordValidator implements ConstraintValidator<PasswordConstraint
@Override
public boolean isValid(String password, ConstraintValidatorContext context) {
log.info("Validating password [{}]", password);
if(password == null || password.equals("")) {
log.info("Provided password is either null or an empty string");
return true;
}
if(password.length() < 2 || password.length() > 64) {
log.info("Passwords must be between 2 and 64 characters in length");
return false;
if (password != null && !password.isEmpty()){
if (password.length() < 2 || password.length() > 64) {
log.info("Passwords must be between 2 and 64 characters in length");
return false;
}
}
return true;

View File

@ -148,6 +148,7 @@ public class DocumentConversionServiceImp implements DocumentConversionService {
pres.getPodId(),
pres.getMeetingId(),
pres.getId(),
pres.getTemporaryPresentationId(),
pres.getName(),
pres.getAuthzToken(),
pres.isDownloadable(),

View File

@ -26,6 +26,7 @@ public final class UploadedPresentation {
private final String podId;
private final String meetingId;
private final String id;
private final String temporaryPresentationId;
private final String name;
private final boolean uploadFailed;
private final ArrayList<String> uploadFailReason;
@ -44,6 +45,7 @@ public final class UploadedPresentation {
public UploadedPresentation(String podId,
String meetingId,
String id,
String temporaryPresentationId,
String name,
String baseUrl,
Boolean current,
@ -53,6 +55,7 @@ public final class UploadedPresentation {
this.podId = podId;
this.meetingId = meetingId;
this.id = id;
this.temporaryPresentationId = temporaryPresentationId;
this.name = name;
this.baseUrl = baseUrl;
this.isDownloadable = false;
@ -62,6 +65,19 @@ public final class UploadedPresentation {
this.uploadFailReason = uploadFailReason;
}
public UploadedPresentation(String podId,
String meetingId,
String id,
String name,
String baseUrl,
Boolean current,
String authzToken,
Boolean uploadFailed,
ArrayList<String> uploadFailReason) {
this(podId, meetingId, id, "", name, baseUrl,
current, authzToken, uploadFailed, uploadFailReason);
}
public File getUploadedFile() {
return uploadedFile;
}
@ -82,6 +98,10 @@ public final class UploadedPresentation {
return id;
}
public String getTemporaryPresentationId() {
return temporaryPresentationId;
}
public String getName() {
return name;
}

View File

@ -85,7 +85,7 @@ public class SwfSlidesGenerationProgressNotifier {
}
DocPageCompletedProgress progress = new DocPageCompletedProgress(pres.getPodId(), pres.getMeetingId(),
pres.getId(), pres.getId(),
pres.getId(), pres.getTemporaryPresentationId(), pres.getId(),
pres.getName(), "notUsedYet", "notUsedYet",
pres.isDownloadable(), pres.isRemovable(), ConversionMessageConstants.CONVERSION_COMPLETED_KEY,
pres.getNumberOfPages(), generateBasePresUrl(pres), pres.isCurrent());

View File

@ -4,6 +4,7 @@ public class DocConversionRequestReceived implements IDocConversionMsg {
public final String podId;
public final String meetingId;
public final String presId;
public final String temporaryPresentationId;
public final String filename;
public final String authzToken;
public final Boolean downloadable;
@ -13,6 +14,7 @@ public class DocConversionRequestReceived implements IDocConversionMsg {
public DocConversionRequestReceived(String podId,
String meetingId,
String presId,
String temporaryPresentationId,
String filename,
String authzToken,
Boolean downloadable,
@ -21,6 +23,7 @@ public class DocConversionRequestReceived implements IDocConversionMsg {
this.podId = podId;
this.meetingId = meetingId;
this.presId = presId;
this.temporaryPresentationId = temporaryPresentationId;
this.filename = filename;
this.authzToken = authzToken;
this.downloadable = downloadable;

View File

@ -4,6 +4,7 @@ public class DocPageCompletedProgress implements IDocConversionMsg {
public final String podId;
public final String meetingId;
public final String presId;
public final String temporaryPresentationId;
public final String presInstance;
public final String filename;
public final String uploaderId;
@ -15,13 +16,14 @@ public class DocPageCompletedProgress implements IDocConversionMsg {
public final String presBaseUrl;
public final Boolean current;
public DocPageCompletedProgress(String podId, String meetingId, String presId, String presInstance,
public DocPageCompletedProgress(String podId, String meetingId, String presId, String temporaryPresentationId, String presInstance,
String filename, String uploaderId, String authzToken,
Boolean downloadable, Boolean removable, String key,
Integer numPages, String presBaseUrl, Boolean current) {
this.podId = podId;
this.meetingId = meetingId;
this.presId = presId;
this.temporaryPresentationId = temporaryPresentationId;
this.presInstance = presInstance;
this.filename = filename;
this.uploaderId = uploaderId;

View File

@ -157,7 +157,7 @@ object MsgBuilder {
val header = BbbClientMsgHeader(PresentationConversionCompletedSysPubMsg.NAME, msg.meetingId, msg.authzToken)
val pages = generatePresentationPages(msg.presId, msg.numPages.intValue(), msg.presBaseUrl)
val presentation = PresentationVO(msg.presId, msg.filename,
val presentation = PresentationVO(msg.presId, msg.temporaryPresentationId, msg.filename,
current = msg.current.booleanValue(), pages.values.toVector, msg.downloadable.booleanValue(), msg.removable.booleanValue())
val body = PresentationConversionCompletedSysPubMsgBody(podId = msg.podId, messageKey = msg.key,
@ -228,6 +228,7 @@ object MsgBuilder {
val body = PresentationConversionRequestReceivedSysMsgBody(
podId = msg.podId,
presentationId = msg.presId,
temporaryPresentationId = msg.temporaryPresentationId,
current = msg.current,
presName = msg.filename,
downloadable = msg.downloadable,

View File

@ -1,13 +0,0 @@
FROM bbb-common-message
ARG COMMON_VERSION
COPY . /bbb-fsesl-client
RUN cd /bbb-fsesl-client \
&& sed -i "s|\(version := \)\".*|\1\"$COMMON_VERSION\"|g" build.sbt \
&& find -name build.sbt -exec sed -i "s|\(.*org.bigbluebutton.*bbb-common-message[^\"]*\"[ ]*%[ ]*\)\"[^\"]*\"\(.*\)|\1\"$COMMON_VERSION\"\2|g" {} \; \
&& echo 'publishTo := Some(Resolver.file("file", new File(Path.userHome.absolutePath+"/.m2/repository")))' | tee -a build.sbt \
&& sbt compile \
&& sbt publish \
&& sbt publishLocal

File diff suppressed because it is too large Load Diff

View File

@ -49,7 +49,7 @@ class App extends React.Component {
downloadButton.setAttribute('disabled', 'true');
downloadButton.style.cursor = 'not-allowed';
link.setAttribute('href', `data:application/octet-stream,${encodeURIComponent(data)}`);
link.setAttribute('href', `data:text/csv;charset=UTF-8,${encodeURIComponent(data)}`);
link.setAttribute('download', filename);
link.style.display = 'none';
document.body.appendChild(link);
@ -87,14 +87,19 @@ class App extends React.Component {
const cDecoded = decodeURIComponent(document.cookie);
const cArr = cDecoded.split('; ');
cArr.forEach((val) => {
if (val.indexOf(`${cookieName}=`) === 0) learningDashboardAccessToken = val.substring((`${cookieName}=`).length);
if (val.indexOf(`${cookieName}=`) === 0) {
learningDashboardAccessToken = val.substring((`${cookieName}=`).length);
}
});
// Extend AccessToken lifetime by 7d (in each access)
if (learningDashboardAccessToken !== '') {
const cookieExpiresDate = new Date();
cookieExpiresDate.setTime(cookieExpiresDate.getTime() + (3600000 * 24 * 7));
document.cookie = `ld-${meetingId}=${learningDashboardAccessToken}; expires=${cookieExpiresDate.toGMTString()}; path=/;SameSite=None;Secure`;
const value = `ld-${meetingId}=${learningDashboardAccessToken};`;
const expire = `expires=${cookieExpiresDate.toGMTString()};`;
const args = 'path=/;SameSite=None;Secure';
document.cookie = `${value} ${expire} ${args}`;
}
}

View File

@ -34,7 +34,12 @@ function Card(props) {
}
return (
<div className={`flex items-start justify-between p-3 bg-white rounded shadow border-l-4 ${cardClass}`}>
<div
className={
'flex items-start justify-between p-3 bg-white rounded shadow border-l-4'
+ ` ${cardClass}`
}
>
<div className="w-70">
<p className="text-lg font-semibold text-gray-700">
{ number }

View File

@ -104,6 +104,61 @@ class StatusTable extends React.Component {
const isRTL = document.dir === 'rtl';
function makeLineThrough(userPeriod, period) {
const { registeredOn, leftOn } = userPeriod;
const boundaryLeft = period.start;
const boundaryRight = period.end;
const interval = period.end - period.start;
let roundedLeft = registeredOn >= boundaryLeft
&& registeredOn <= boundaryRight ? 'rounded-l' : '';
let roundedRight = leftOn >= boundaryLeft
&& leftOn <= boundaryRight ? 'rounded-r' : '';
let offsetLeft = 0;
let offsetRight = 0;
if (registeredOn >= boundaryLeft && registeredOn <= boundaryRight) {
offsetLeft = ((registeredOn - boundaryLeft) * 100) / interval;
}
if (leftOn >= boundaryLeft && leftOn <= boundaryRight) {
offsetRight = ((boundaryRight - leftOn) * 100) / interval;
}
let width = '';
if (offsetLeft === 0 && offsetRight >= 99) {
width = 'w-1.5';
}
if (offsetRight === 0 && offsetLeft >= 99) {
width = 'w-1.5';
}
if (offsetLeft && offsetRight) {
const variation = offsetLeft - offsetRight;
if (variation > -1 && variation < 1) {
width = 'w-1.5';
}
}
if (isRTL) {
const aux = roundedRight;
if (roundedLeft !== '') roundedRight = 'rounded-r';
else roundedRight = '';
if (aux !== '') roundedLeft = 'rounded-l';
else roundedLeft = '';
}
const redress = '(0.375rem / 2)';
return (
<div
className={
'h-1.5 bg-gray-200 absolute inset-x-0 z-10'
+ ` ${width} ${roundedLeft} ${roundedRight}`
}
style={{
top: `calc(50% - ${redress})`,
left: `${isRTL ? offsetRight : offsetLeft}%`,
right: `${isRTL ? offsetLeft : offsetRight}%`,
}}
/>
);
}
return (
<table className="w-full">
<thead>
@ -134,6 +189,8 @@ class StatusTable extends React.Component {
{ periods.map((period) => {
const { slide, start, end } = period;
const padding = isRTL ? 'paddingLeft' : 'paddingRight';
const URLPrefix = `/bigbluebutton/presentation/${meetingId}/${meetingId}`;
const { presentationId, pageNum } = slide || {};
return (
<td
style={{
@ -147,13 +204,13 @@ class StatusTable extends React.Component {
aria-label={tsToHHmmss(start - periods[0].start)}
>
<a
href={`/bigbluebutton/presentation/${meetingId}/${meetingId}/${slide.presentationId}/svg/${slide.pageNum}`}
href={`${URLPrefix}/${presentationId}/svg/${pageNum}`}
className="block border-2 border-gray-300"
target="_blank"
rel="noreferrer"
>
<img
src={`/bigbluebutton/presentation/${meetingId}/${meetingId}/${slide.presentationId}/thumbnail/${slide.pageNum}`}
src={`${URLPrefix}/${presentationId}/thumbnail/${pageNum}`}
alt={intl.formatMessage({
id: 'app.learningDashboard.statusTimelineTable.thumbnail',
defaultMessage: 'Presentation thumbnail',
@ -216,57 +273,9 @@ class StatusTable extends React.Component {
{ (registeredOn >= boundaryLeft && registeredOn <= boundaryRight)
|| (leftOn >= boundaryLeft && leftOn <= boundaryRight)
|| (boundaryLeft > registeredOn && boundaryRight < leftOn)
|| (boundaryLeft >= registeredOn && leftOn === 0) ? (
(function makeLineThrough() {
let roundedLeft = registeredOn >= boundaryLeft
&& registeredOn <= boundaryRight ? 'rounded-l' : '';
let roundedRight = leftOn >= boundaryLeft
&& leftOn <= boundaryRight ? 'rounded-r' : '';
let offsetLeft = 0;
let offsetRight = 0;
if (registeredOn >= boundaryLeft
&& registeredOn <= boundaryRight) {
offsetLeft = ((registeredOn - boundaryLeft) * 100)
/ interval;
}
if (leftOn >= boundaryLeft && leftOn <= boundaryRight) {
offsetRight = ((boundaryRight - leftOn) * 100) / interval;
}
let width = '';
if (offsetLeft === 0 && offsetRight >= 99) {
width = 'w-1.5';
}
if (offsetRight === 0 && offsetLeft >= 99) {
width = 'w-1.5';
}
if (offsetLeft && offsetRight) {
const variation = offsetLeft - offsetRight;
if (variation > -1 && variation < 1) {
width = 'w-1.5';
}
}
if (isRTL) {
const aux = roundedRight;
if (roundedLeft !== '') roundedRight = 'rounded-r';
else roundedRight = '';
if (aux !== '') roundedLeft = 'rounded-l';
else roundedLeft = '';
}
const redress = '(0.375rem / 2)';
return (
<div
className={`h-1.5 ${width} bg-gray-200 absolute inset-x-0 z-10 ${roundedLeft} ${roundedRight}`}
style={{
top: `calc(50% - ${redress})`,
left: `${isRTL ? offsetRight : offsetLeft}%`,
right: `${isRTL ? offsetLeft : offsetRight}%`,
}}
/>
);
})()
) : null }
|| (boundaryLeft >= registeredOn && leftOn === 0)
? makeLineThrough(userPeriod, period)
: null }
{ userEmojisInPeriod.map((emoji) => {
const offset = ((emoji.sentOn - period.start) * 100)
/ (interval);
@ -285,7 +294,12 @@ class StatusTable extends React.Component {
defaultMessage: emojiConfigs[emoji.name].defaultMessage,
})}
>
<i className={`${emojiConfigs[emoji.name].icon} text-sm bbb-icon-timeline`} />
<i
className={
'text-sm bbb-icon-timeline'
+ ` ${emojiConfigs[emoji.name].icon}`
}
/>
</div>
);
}) }

View File

@ -176,6 +176,11 @@ const UserDatailsComponent = (props) => {
if (hasDraw) mostCommonAnswer = null;
}
const capitalizeFirstLetter = (text) => (
String.fromCharCode(text.charCodeAt(0) - 32)
+ text.substring(1)
);
return (
<div className="p-6 flex flex-row justify-between items-center">
<div className="min-w-[40%] text-ellipsis">{question}</div>
@ -211,11 +216,11 @@ const UserDatailsComponent = (props) => {
<div
className="min-w-[40%] text-ellipsis text-center overflow-hidden"
title={mostCommonAnswer
? `${String.fromCharCode(mostCommonAnswer.charCodeAt(0) - 32)}${mostCommonAnswer.substring(1)}`
? capitalizeFirstLetter(mostCommonAnswer)
: null}
>
{ mostCommonAnswer
? `${String.fromCharCode(mostCommonAnswer.charCodeAt(0) - 32)}${mostCommonAnswer.substring(1)}`
? capitalizeFirstLetter(mostCommonAnswer)
: intl.formatMessage({
id: 'app.learningDashboard.usersTable.notAvailable',
defaultMessage: 'N/A',
@ -233,7 +238,7 @@ const UserDatailsComponent = (props) => {
<div className="min-w-[20%] text-ellipsis overflow-hidden">{category}</div>
<div className="min-w-[60%] grow text-center text-sm">
<div className="mb-2">
{ (function () {
{ (function getAverage() {
if (average >= 0 && category === 'Talk Time') return tsToHHmmss(average);
if (average >= 0 && category !== 'Talk Time') return <FormattedNumber value={average} minimumFractionDigits="0" maximumFractionDigits="1" />;
return <FormattedMessage id="app.learningDashboard.usersTable.notAvailable" defaultMessage="N/A" />;

View File

@ -1 +1 @@
git clone --branch v1.0.2 --depth 1 https://github.com/bigbluebutton/bbb-pads bbb-pads
git clone --branch v1.2.0 --depth 1 https://github.com/bigbluebutton/bbb-pads bbb-pads

View File

@ -1 +1 @@
git clone --branch v3.4.0 --depth 1 https://github.com/bigbluebutton/bbb-playback bbb-playback
git clone --branch v4.0.0 --depth 1 https://github.com/bigbluebutton/bbb-playback bbb-playback

View File

@ -3,7 +3,7 @@
<condition field="${bbb_authorized}" expression="true" break="on-false"/>
<condition field="${sip_via_protocol}" expression="^wss?$"/>
<condition field="destination_number" expression="^(\d{5,11})$">
<action application="jitterbuffer" data="60:120"/>
<action application="jitterbuffer" data="120"/>
<action application="answer"/>
<action application="conference" data="$1@cdquality"/>
</condition>
@ -11,7 +11,7 @@
<extension name="bbb_conferences">
<condition field="${bbb_authorized}" expression="true" break="on-false"/>
<condition field="destination_number" expression="^(\d{5,11})$">
<action application="jitterbuffer" data="60:120"/>
<action application="jitterbuffer" data="120"/>
<action application="answer"/>
<action application="conference" data="$1@cdquality"/>
</condition>

View File

@ -2,7 +2,7 @@
<extension name="ECHO_TO_CONFERENCE">
<condition field="${bbb_from_echo}" expression="true" break="on-false"/>
<condition field="destination_number" expression="^(ECHO_TO_CONFERENCE)$">
<action application="jitterbuffer" data="60:120"/>
<action application="jitterbuffer" data="120"/>
<action application="answer"/>
<action application="conference" data="${vbridge}@cdquality"/>
</condition>

View File

@ -1 +1 @@
git clone --branch v2.8.0-alpha.1 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu
git clone --branch v2.8.1 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu

View File

@ -1 +1 @@
BIGBLUEBUTTON_RELEASE=2.5.0-beta.1
BIGBLUEBUTTON_RELEASE=2.5.0-rc.2

View File

@ -658,8 +658,10 @@ fi
if [[ $SECRET ]]; then
need_root
if get_properties_value securitySalt "$BBB_WEB_ETC_CONFIG" > /dev/null ; then
change_var_salt "$BBB_WEB_ETC_CONFIG" securitySalt "$SECRET"
echo "Assigning secret in $BBB_WEB_ETC_CONFIG"
if [ -f "$BBB_WEB_ETC_CONFIG" ] && grep "^securitySalt" "$BBB_WEB_ETC_CONFIG" > /dev/null ; then
change_var_value "$BBB_WEB_ETC_CONFIG" securitySalt "$SECRET"
else
echo "securitySalt=$SECRET" >> "$BBB_WEB_ETC_CONFIG"
fi
@ -1296,19 +1298,6 @@ check_state() {
done
fi
stunServerAddress=$(cat /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini | sed -n '/^stunServerAddress/{s/.*=//;p}')
stunServerPort=$(cat /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini | sed -n '/^stunServerPort/{s/.*=//;p}')
if [ ! -z "$stunServerAddress" ]; then
if stunclient --mode full --localport 30000 $stunServerAddress $stunServerPort | grep -q "fail\|Unable\ to\ resolve"; then
echo
echo "#"
echo "# Warning: Failed to verify STUN server at $stunServerAddress:$stunServerPort with command"
echo "#"
echo "# stunclient --mode full --localport 30000 $stunServerAddress $stunServerPort"
echo "#"
fi
fi
BBB_LOG="/var/log/bigbluebutton"
if [ "$(stat -c "%U %G" $BBB_LOG)" != "bigbluebutton bigbluebutton" ]; then
echo
@ -1476,14 +1465,6 @@ if [ $CHECK ]; then
done
fi
stunServerAddress=$(cat /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini | sed -n '/^stunServerAddress/{s/.*=//;p}')
stunServerPort=$(cat /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini | sed -n '/^stunServerPort/{s/.*=//;p}')
if [ ! -z "$stunServerAddress" ]; then
echo
echo "/etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini (STUN Server)"
echo " stun: $stunServerAddress:$stunServerPort"
fi
check_state
echo
@ -1633,7 +1614,12 @@ if [ -n "$HOST" ]; then
if [ -f "$BBB_WEB_ETC_CONFIG" ] && grep "bigbluebutton.web.serverURL" "$BBB_WEB_ETC_CONFIG" > /dev/null ; then
change_var_value "$BBB_WEB_ETC_CONFIG" bigbluebutton.web.serverURL "$PROTOCOL://$HOST"
else
echo "bigbluebutton.web.serverURL=$PROTOCOL://$HOST" > "$BBB_WEB_ETC_CONFIG"
echo "bigbluebutton.web.serverURL=$PROTOCOL://$HOST" >> "$BBB_WEB_ETC_CONFIG"
fi
# Populate /etc/bigbluebutton/bbb-web.properites with the shared secret
if ! grep -q "^securitySalt" "$BBB_WEB_ETC_CONFIG"; then
echo "securitySalt=$(get_bbb_web_config_value securitySalt)" >> "$BBB_WEB_ETC_CONFIG"
fi

View File

@ -1,2 +0,0 @@
Dockerfile
Dockerfile.dev

View File

@ -8,10 +8,10 @@ mobile-experience@1.1.0
mongo@1.14.6
reactive-var@1.0.11
standard-minifier-css@1.7.4
standard-minifier-css@1.8.1
standard-minifier-js@2.8.0
es5-shim@4.8.0
ecmascript@0.16.1
ecmascript@0.16.2
shell-server@0.5.0
static-html@1.3.2

View File

@ -1 +1 @@
METEOR@2.6.1
METEOR@2.7.1

View File

@ -1,6 +1,6 @@
allow-deny@1.1.1
autoupdate@1.8.0
babel-compiler@7.8.1
babel-compiler@7.9.0
babel-runtime@1.5.0
base64@1.0.12
binary-heap@1.0.11
@ -16,11 +16,11 @@ ddp-common@1.4.0
ddp-server@2.5.0
diff-sequence@1.1.1
dynamic-import@0.7.2
ecmascript@0.16.1
ecmascript@0.16.2
ecmascript-runtime@0.8.0
ecmascript-runtime-client@0.12.1
ecmascript-runtime-server@0.11.0
ejson@1.1.1
ejson@1.1.2
es5-shim@4.8.0
fetch@0.1.1
geojson-utils@1.0.10
@ -39,13 +39,13 @@ meteortesting:browser-tests@1.3.5
meteortesting:mocha@2.0.3
meteortesting:mocha-core@8.1.2
minifier-css@1.6.0
minifier-js@2.7.3
minifier-js@2.7.4
minimongo@1.8.0
mobile-experience@1.1.0
mobile-status-bar@1.1.0
modern-browsers@0.1.7
modules@0.18.0
modules-runtime@0.12.0
modules-runtime@0.13.0
mongo@1.14.6
mongo-decimal@0.1.2
mongo-dev-server@1.1.0
@ -54,7 +54,7 @@ npm-mongo@4.3.1
ordered-dict@1.1.0
promise@0.12.0
random@1.2.0
react-fast-refresh@0.2.2
react-fast-refresh@0.2.3
react-meteor-data@2.4.0
reactive-dict@1.3.0
reactive-var@1.0.11
@ -66,12 +66,12 @@ session@1.2.0
shell-server@0.5.0
socket-stream-client@0.4.0
spacebars-compiler@1.3.0
standard-minifier-css@1.7.4
standard-minifier-css@1.8.1
standard-minifier-js@2.8.0
static-html@1.3.2
templating-tools@1.2.1
tracker@1.2.0
typescript@4.4.1
typescript@4.5.4
underscore@1.0.10
url@1.3.2
webapp@1.13.1

View File

@ -1,38 +0,0 @@
FROM node:8
RUN set -x \
&& curl -sL https://install.meteor.com | sed s/--progress-bar/-sL/g | /bin/sh \
&& useradd -m -G users -s /bin/bash meteor
RUN apt-get update && apt-get -y install jq
COPY . /source
RUN cd /source \
&& mv docker-entrypoint.sh /usr/local/bin/ \
&& chown -R meteor:meteor . \
&& mkdir /app \
&& chown -R meteor:meteor /app
USER meteor
RUN cd /source \
&& meteor npm install \
&& meteor build --directory /app
ENV NODE_ENV production
RUN cd /app/bundle/programs/server \
&& npm install \
&& npm cache clear --force
WORKDIR /app/bundle
ENV MONGO_URL=mongodb://mongo:27017/html5client \
PORT=3000 \
ROOT_URL=http://localhost:3000 \
METEOR_SETTINGS_MODIFIER=.
EXPOSE 3000
CMD ["docker-entrypoint.sh"]

View File

@ -1,24 +0,0 @@
FROM node:8
COPY . /source
RUN set -x \
&& curl -sL https://install.meteor.com | sed s/--progress-bar/-sL/g | /bin/sh \
&& useradd -m -G users -s /bin/bash meteor \
&& chown -R meteor:meteor /source
USER meteor
RUN cd /source \
&& meteor npm install
WORKDIR /source
ENV MONGO_URL=mongodb://mongo:27017/html5client \
PORT=3000 \
ROOT_URL=http://localhost:3000
EXPOSE 3000
CMD ["npm", "start"]

View File

@ -1,5 +0,0 @@
#!/bin/bash -e
export METEOR_SETTINGS=` jq "${METEOR_SETTINGS_MODIFIER}" ./programs/server/assets/app/config/settings.yml `
exec node main.js

View File

@ -1,4 +1,9 @@
import { PrometheusAgent, METRIC_NAMES } from '/imports/startup/server/prom-metrics/index.js'
// Round-trip time helper
export default function voidConnection() {
export default function voidConnection(previousRtt) {
if (previousRtt) {
PrometheusAgent.observe(METRIC_NAMES.METEOR_RTT, previousRtt/1000);
}
return 0;
}

View File

@ -139,10 +139,13 @@ export default function addMeeting(meeting) {
const sanitizeTextInChat = original => SanitizeHTML(original, {
allowedTags: ['a', 'b', 'br', 'i', 'img', 'li', 'small', 'span', 'strong', 'u', 'ul'],
allowedAttributes: {
a: ['href', 'name', 'target'],
a: ['href', 'target'],
img: ['src', 'width', 'height'],
},
allowedSchemes: ['https'],
allowedSchemesByTag: {
a: ['https', 'mailto', 'tel']
}
});
const sanitizedWelcomeText = sanitizeTextInChat(welcomeMsg);
@ -153,14 +156,18 @@ export default function addMeeting(meeting) {
const insertBlankTarget = (s, i) => `${s.substr(0, i)} target="_blank"${s.substr(i)}`;
const linkWithoutTarget = new RegExp('<a href="(.*?)">', 'g');
linkWithoutTarget.test(welcomeMsg);
if (linkWithoutTarget.lastIndex > 0) {
welcomeMsg = insertBlankTarget(
welcomeMsg,
linkWithoutTarget.lastIndex - 1,
);
}
do {
linkWithoutTarget.test(welcomeMsg);
if (linkWithoutTarget.lastIndex > 0) {
welcomeMsg = insertBlankTarget(
welcomeMsg,
linkWithoutTarget.lastIndex - 1,
);
linkWithoutTarget.lastIndex = linkWithoutTarget.lastIndex - 1;
}
} while (linkWithoutTarget.lastIndex > 0);
newMeeting.welcomeProp.welcomeMsg = welcomeMsg;

View File

@ -1,23 +1,26 @@
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import PresentationUploadToken from '/imports/api/presentation-upload-token';
import Presentations from '/imports/api/presentations';
export default function handlePresentationUploadTokenPass({ body, header }, meetingId) {
check(body, Object);
const { userId } = header;
const { podId, authzToken, filename } = body;
const { podId, authzToken, filename, tmpPresId } = body;
check(userId, String);
check(podId, String);
check(authzToken, String);
check(filename, String);
check(tmpPresId, String)
const selector = {
meetingId,
podId,
userId,
filename,
tmpPresId,
};
const modifier = {
@ -26,6 +29,7 @@ export default function handlePresentationUploadTokenPass({ body, header }, meet
userId,
filename,
authzToken,
tmpPresId,
failed: false,
used: false,
};

View File

@ -3,7 +3,7 @@ import { check } from 'meteor/check';
import { extractCredentials } from '/imports/api/common/server/helpers';
import Logger from '/imports/startup/server/logger';
export default function requestPresentationUploadToken(podId, filename) {
export default function requestPresentationUploadToken(podId, filename, tmpPresId) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'PresentationUploadTokenReqMsg';
@ -15,10 +15,12 @@ export default function requestPresentationUploadToken(podId, filename) {
check(requesterUserId, String);
check(podId, String);
check(filename, String);
check(tmpPresId, String);
const payload = {
podId,
filename,
tmpPresId
};
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);

View File

@ -4,7 +4,7 @@ import PresentationUploadToken from '/imports/api/presentation-upload-token';
import Logger from '/imports/startup/server/logger';
import AuthTokenValidation, { ValidationStates } from '/imports/api/auth-token-validation';
function presentationUploadToken(podId, filename) {
function presentationUploadToken(podId, filename, tmpPresId) {
const tokenValidation = AuthTokenValidation.findOne({ connectionId: this.connection.id });
if (!tokenValidation || tokenValidation.validationStatus !== ValidationStates.VALIDATED) {
@ -16,12 +16,14 @@ function presentationUploadToken(podId, filename) {
check(podId, String);
check(filename, String);
check(tmpPresId, String);
const selector = {
meetingId,
podId,
userId,
filename,
tmpPresId,
};
Logger.debug('Publishing PresentationUploadToken', { meetingId, userId });

View File

@ -34,6 +34,7 @@ export default function addPresentation(meetingId, podId, presentation) {
id: String,
name: String,
current: Boolean,
temporaryPresentationId: String,
pages: [
{
id: String,

View File

@ -26,10 +26,10 @@ export default function addUserPersistentData(user) {
locked: Boolean,
avatar: String,
clientType: String,
left: Boolean,
effectiveConnectionType: null,
});
const {
intId,
extId,

View File

@ -1,6 +1,7 @@
import RedisPubSub from '/imports/startup/server/redis';
import handleRemoveUser from './handlers/removeUser';
import handleUserJoined from './handlers/userJoined';
import handleUserLeftFlagUpdated from './handlers/userLeftFlagUpdated';
import handleValidateAuthToken from './handlers/validateAuthToken';
import handlePresenterAssigned from './handlers/presenterAssigned';
import handleEmojiStatus from './handlers/emojiStatus';
@ -14,5 +15,6 @@ RedisPubSub.on('UserLeftMeetingEvtMsg', handleRemoveUser);
RedisPubSub.on('ValidateAuthTokenRespMsg', handleValidateAuthToken);
RedisPubSub.on('UserEmojiChangedEvtMsg', handleEmojiStatus);
RedisPubSub.on('UserRoleChangedEvtMsg', handleChangeRole);
RedisPubSub.on('UserLeftFlagUpdatedEvtMsg', handleUserLeftFlagUpdated);
RedisPubSub.on('UserPinStateChangedEvtMsg', handleUserPinChanged);
RedisPubSub.on('UserInactivityInspectMsg', handleUserInactivityInspect);

View File

@ -0,0 +1,13 @@
import { check } from 'meteor/check';
import userLeftFlag from '../modifiers/userLeftFlagUpdated';
export default function handleUserLeftFlag({ body }, meetingId) {
const user = body;
check(user, {
intId: String,
userLeftFlag: Boolean,
});
userLeftFlag(meetingId, user.intId, user.userLeftFlag);
}

View File

@ -62,6 +62,7 @@ export default function addUser(meetingId, userData) {
inactivityCheck: false,
responseDelay: 0,
loggedOut: false,
left: false,
...flat(user),
};

View File

@ -19,6 +19,7 @@ export default function createDummyUser(meetingId, userId, authToken) {
authToken,
clientType: 'HTML5',
validated: null,
left: false,
};
try {

View File

@ -0,0 +1,24 @@
import Logger from '/imports/startup/server/logger';
import Users from '/imports/api/users';
export default function userLeftFlagUpdated(meetingId, userId, left) {
const selector = {
meetingId,
userId,
};
const modifier = {
$set: {
left,
},
};
try {
const numberAffected = Users.update(selector, modifier);
if (numberAffected) {
Logger.info(`Updated user ${userId} with left flag as ${left}`);
}
} catch (err) {
Logger.error(`Changed user role: ${err}`);
}
}

View File

@ -60,6 +60,7 @@ function users() {
{ meetingId },
],
intId: { $exists: true },
left: false,
};
const User = Users.findOne({ userId, meetingId }, { fields: { role: 1 } });

View File

@ -4,7 +4,6 @@ import Langmap from 'langmap';
import fs from 'fs';
import Users from '/imports/api/users';
import './settings';
import { lookup as lookupUserAgent } from 'useragent';
import { check } from 'meteor/check';
import Logger from './logger';
import Redis from './redis';
@ -144,7 +143,8 @@ Meteor.startup(() => {
Meteor.onMessage(event => {
const { method } = event;
if (method) {
PrometheusAgent.increment(METRIC_NAMES.METEOR_METHODS, { methodName: method });
const methodName = method.includes('stream-cursor') ? 'stream-cursor' : method;
PrometheusAgent.increment(METRIC_NAMES.METEOR_METHODS, { methodName });
}
});
@ -303,20 +303,6 @@ WebApp.connectHandlers.use('/feedback', (req, res) => {
}));
});
WebApp.connectHandlers.use('/useragent', (req, res) => {
const userAgent = req.headers['user-agent'];
let response = 'No user agent found in header';
if (userAgent) {
response = lookupUserAgent(userAgent).toString();
}
Logger.info(`The requesting user agent is ${response}`);
// res.setHeader('Content-Type', 'application/json');
res.writeHead(200);
res.end(response);
});
WebApp.connectHandlers.use('/guestWait', (req, res) => {
if (!guestWaitHtml) {
try {

View File

@ -1,5 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { createLogger, format, transports } from 'winston';
import WinstonPromTransport from './prom-metrics/winstonPromTransport';
const LOG_CONFIG = Meteor?.settings?.private?.serverLog || {};
const { level } = LOG_CONFIG;
@ -20,6 +21,10 @@ const Logger = createLogger({
handleExceptions: true,
level,
}),
// export error logs to prometheus
new WinstonPromTransport({
level: 'error',
}),
],
});

View File

@ -1,34 +1,60 @@
const {
Counter,
Gauge,
Histogram
} = require('prom-client');
const METRICS_PREFIX = 'html5_'
const METRIC_NAMES = {
METEOR_METHODS: 'meteorMethods',
METEOR_ERRORS_TOTAL: 'meteorErrorsTotal',
METEOR_RTT: 'meteorRtt',
REDIS_MESSAGE_QUEUE: 'redisMessageQueue',
REDIS_PAYLOAD_SIZE: 'redisPayloadSize',
REDIS_PROCESSING_TIME: 'redisProcessingTime'
}
const buildFrontendMetrics = () => {
return {
[METRIC_NAMES.METEOR_METHODS]: new Counter({
name: `${METRICS_PREFIX}meteor_methods`,
help: 'Total number of meteor methods processed in html5',
labelNames: ['methodName', 'role', 'instanceId'],
}),
}
}
const buildBackendMetrics = () => {
// TODO add relevant backend metrics
return {}
}
let METRICS;
const buildMetrics = () => {
if (METRICS == null) {
const isFrontend = (!process.env.BBB_HTML5_ROLE || process.env.BBB_HTML5_ROLE === 'frontend');
const isBackend = (!process.env.BBB_HTML5_ROLE || process.env.BBB_HTML5_ROLE === 'backend');
if (isFrontend) METRICS = buildFrontendMetrics();
if (isBackend) METRICS = { ...METRICS, ...buildBackendMetrics()}
METRICS = {
[METRIC_NAMES.METEOR_METHODS]: new Counter({
name: `${METRICS_PREFIX}meteor_methods`,
help: 'Total number of meteor methods processed in html5',
labelNames: ['methodName', 'role', 'instanceId'],
}),
[METRIC_NAMES.METEOR_ERRORS_TOTAL]: new Counter({
name: `${METRICS_PREFIX}meteor_errors_total`,
help: 'Total number of errors logs in meteor',
labelNames: ['errorMessage', 'role', 'instanceId'],
}),
[METRIC_NAMES.METEOR_RTT]: new Histogram({
name: `${METRICS_PREFIX}meteor_rtt_seconds`,
help: 'Round-trip time of meteor client-server connections in seconds',
buckets: [0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.75, 1, 1.5, 2, 2.5, 5],
labelNames: ['role', 'instanceId'],
}),
[METRIC_NAMES.REDIS_MESSAGE_QUEUE]: new Gauge({
name: `${METRICS_PREFIX}redis_message_queue`,
help: 'Message queue size in redis',
labelNames: ['meetingId', 'role', 'instanceId'],
}),
[METRIC_NAMES.REDIS_PAYLOAD_SIZE]: new Histogram({
name: `${METRICS_PREFIX}redis_payload_size`,
help: 'Redis events payload size',
labelNames: ['eventName', 'role', 'instanceId'],
}),
[METRIC_NAMES.REDIS_PROCESSING_TIME]: new Histogram({
name: `${METRICS_PREFIX}redis_processing_time`,
help: 'Redis events processing time in milliseconds',
labelNames: ['eventName', 'role', 'instanceId'],
}),
}
}
return METRICS;

View File

@ -81,6 +81,16 @@ class PrometheusScrapeAgent {
metric.set(labelsObject, value)
}
}
observe(metricName, value, labelsObject) {
if (!this.started) return;
const metric = this.metrics[metricName];
if (metric) {
labelsObject = { ...labelsObject, ...this.roleAndInstanceLabels };
metric.observe(labelsObject, value)
}
}
}
export default PrometheusScrapeAgent;

View File

@ -0,0 +1,19 @@
const Transport = require('winston-transport');
import { PrometheusAgent, METRIC_NAMES } from './index.js'
module.exports = class WinstonPromTransport extends Transport {
constructor(opts) {
super(opts);
}
log(info, callback) {
setImmediate(() => {
this.emit('logged', info);
});
PrometheusAgent.increment(METRIC_NAMES.METEOR_ERRORS_TOTAL, { errorMessage: info.message });
callback();
}
};

View File

@ -5,11 +5,13 @@ import { check } from 'meteor/check';
import Logger from './logger';
import Metrics from './metrics';
import queue from 'queue';
import { PrometheusAgent, METRIC_NAMES } from './prom-metrics/index.js'
// Fake meetingId used for messages that have no meetingId
const NO_MEETING_ID = '_';
const { queueMetrics } = Meteor.settings.private.redis.metrics;
const { collectRedisMetrics: PROM_METRICS_ENABLED } = Meteor.settings.private.prometheus;
const makeEnvelope = (channel, eventName, header, body, routing) => {
const envelope = {
@ -78,6 +80,16 @@ class MeetingMessageQueue {
}
const queueLength = this.queue.length;
if (PROM_METRICS_ENABLED) {
const dataLength = JSON.stringify(data).length;
const currentTimestamp = Date.now();
const processTime = currentTimestamp - beginHandleTimestamp;
PrometheusAgent.observe(METRIC_NAMES.REDIS_PROCESSING_TIME, processTime, { eventName });
PrometheusAgent.observe(METRIC_NAMES.REDIS_PAYLOAD_SIZE, dataLength, { eventName });
meetingId && PrometheusAgent.set(METRIC_NAMES.REDIS_MESSAGE_QUEUE, queueLength, { meetingId });
}
if (queueLength > 100) {
Logger.warn(`Redis: MeetingMessageQueue for meetingId=${meetingId} has queue size=${queueLength} `);
}

View File

@ -470,6 +470,7 @@ class BreakoutRoom extends PureComponent {
.filter((user) => !stateUsersId.includes(user.userId))
.map((user) => ({
userId: user.userId,
extId: user.extId,
userName: user.name,
isModerator: user.role === ROLE_MODERATOR,
room: 0,
@ -611,7 +612,8 @@ class BreakoutRoom extends PureComponent {
}
populateWithLastBreakouts(lastBreakouts) {
const { getBreakoutUserWasIn, users, intl } = this.props;
const { getBreakoutUserWasIn, intl } = this.props;
const { users } = this.state;
const changedNames = [];
lastBreakouts.forEach((breakout) => {

View File

@ -44,6 +44,7 @@ import Settings from '/imports/ui/services/settings';
import LayoutService from '/imports/ui/components/layout/service';
import { registerTitleView } from '/imports/utils/dom-utils';
import GlobalStyles from '/imports/ui/stylesheets/styled-components/globalStyles';
import MediaService from '/imports/ui/components/media/service';
const MOBILE_MEDIA = 'only screen and (max-width: 40em)';
const APP_CONFIG = Meteor.settings.public.app;
@ -156,6 +157,9 @@ class App extends Component {
settingsLayout,
isRTL,
hidePresentation,
autoSwapLayout,
shouldShowScreenshare,
shouldShowExternalVideo,
} = this.props;
const { browserName } = browserInfo;
const { osName } = deviceInfo;
@ -167,11 +171,18 @@ class App extends Component {
value: isRTL,
});
const presentationOpen = !(autoSwapLayout || hidePresentation)
|| shouldShowExternalVideo || shouldShowScreenshare;
layoutContextDispatch({
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
value: !hidePresentation,
value: presentationOpen,
});
if (!presentationOpen && !MediaService.getSwapLayout()) {
MediaService.setSwapLayout(layoutContextDispatch);
}
Modal.setAppElement('#app');
const fontSize = isMobile() ? MOBILE_FONT_SIZE : DESKTOP_FONT_SIZE;

View File

@ -230,6 +230,7 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
Meteor.settings.public.presentation.restoreOnUpdate,
),
hidePresentation: getFromUserSettings('bbb_hide_presentation', LAYOUT_CONFIG.hidePresentation),
autoSwapLayout: getFromUserSettings('bbb_auto_swap_layout', LAYOUT_CONFIG.autoSwapLayout),
hideActionsBar: getFromUserSettings('bbb_hide_actions_bar', false),
isModalOpen: !!getModal(),
};

View File

@ -41,7 +41,7 @@ const handleLeaveAudio = () => {
Storage.setItem('getEchoTest', true);
}
Service.exitAudio();
Service.forceExitAudio();
logger.info({
logCode: 'audiocontrols_leave_audio',
extraInfo: { logType: 'user_action' },

View File

@ -267,28 +267,7 @@ class AudioModal extends Component {
disableActions: false,
});
}).catch((err) => {
const { type } = err;
switch (type) {
case 'MEDIA_ERROR':
this.setState({
content: 'help',
errCode: 0,
disableActions: false,
});
break;
case 'CONNECTION_ERROR':
this.setState({
errCode: 0,
disableActions: false,
});
break;
default:
this.setState({
errCode: 0,
disableActions: false,
});
break;
}
this.handleJoinMicrophoneError(err);
});
}
@ -342,7 +321,29 @@ class AudioModal extends Component {
this.setState({
disableActions: false,
});
}).catch(this.handleGoToAudioOptions);
}).catch((err) => {
this.handleJoinMicrophoneError(err);
});
}
handleJoinMicrophoneError(err) {
const { type } = err;
switch (type) {
case 'MEDIA_ERROR':
this.setState({
content: 'help',
errCode: 0,
disableActions: false,
});
break;
case 'CONNECTION_ERROR':
default:
this.setState({
errCode: 0,
disableActions: false,
});
break;
}
}
setContent(content) {

View File

@ -14,6 +14,8 @@ import { screenshareHasEnded } from '/imports/ui/components/screenshare/service'
import AudioManager from '/imports/ui/services/audio-manager';
import Settings from '/imports/ui/services/settings';
import BreakoutDropdown from '/imports/ui/components/breakout-room/breakout-dropdown/component';
import Users from '/imports/api/users';
import Auth from '/imports/ui/services/auth';
const intlMessages = defineMessages({
breakoutTitle: {
@ -282,7 +284,8 @@ class BreakoutRoom extends PureComponent {
amIPresenter,
intl,
isUserInBreakoutRoom,
exitAudio,
forceExitAudio,
rejoinAudio,
setBreakoutAudioTransferStatus,
getBreakoutAudioTransferStatus,
} = this.props;
@ -349,7 +352,7 @@ class BreakoutRoom extends PureComponent {
this.getBreakoutURL(breakoutId);
// leave main room's audio,
// and stops video and screenshare when joining a breakout room
exitAudio();
forceExitAudio();
logger.info({
logCode: 'breakoutroom_join',
extraInfo: { logType: 'user_action' },
@ -357,6 +360,31 @@ class BreakoutRoom extends PureComponent {
VideoService.storeDeviceIds();
VideoService.exitVideo();
if (amIPresenter) screenshareHasEnded();
Tracker.autorun((c) => {
const selector = {
meetingId: breakoutId,
};
const query = Users.find(selector, {
fields: {
loggedOut: 1,
extId: 1,
},
});
const observeLogOut = (user) => {
if (user?.loggedOut && user?.extId?.startsWith(Auth.userID)) {
rejoinAudio();
c.stop();
}
}
query.observe({
added: observeLogOut,
changed: observeLogOut,
});
});
}}
disabled={disable}
/>

View File

@ -7,6 +7,11 @@ import Service from './service';
import { layoutDispatch } from '../layout/context';
import Auth from '/imports/ui/services/auth';
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
import {
didUserSelectedMicrophone,
didUserSelectedListenOnly,
} from '/imports/ui/components/audio/audio-modal/service';
import { makeCall } from '/imports/ui/services/api';
const BreakoutContainer = (props) => {
const layoutContextDispatch = layoutDispatch();
@ -45,6 +50,30 @@ export default withTracker((props) => {
getBreakoutAudioTransferStatus,
} = AudioService;
const logUserCouldNotRejoinAudio = () => {
logger.warn({
logCode: 'mainroom_audio_rejoin',
extraInfo: { logType: 'user_action' },
}, 'leaving breakout room couldn\'t rejoin audio in the main room');
};
const rejoinAudio = () => {
if (didUserSelectedMicrophone()) {
AudioManager.joinMicrophone().then(() => {
makeCall('toggleVoice', null, true).catch(() => {
AudioManager.forceExitAudio();
logUserCouldNotRejoinAudio();
});
}).catch(() => {
logUserCouldNotRejoinAudio();
});
} else if (didUserSelectedListenOnly()) {
AudioManager.joinListenOnly().catch(() => {
logUserCouldNotRejoinAudio();
});
}
};
return {
...props,
breakoutRooms,
@ -61,7 +90,8 @@ export default withTracker((props) => {
amIModerator: amIModerator(),
isMeteorConnected,
isUserInBreakoutRoom,
exitAudio: () => AudioManager.exitAudio(),
forceExitAudio: () => AudioManager.forceExitAudio(),
rejoinAudio,
isReconnecting,
setBreakoutAudioTransferStatus,
getBreakoutAudioTransferStatus,

View File

@ -68,8 +68,6 @@ const ChatAlert = (props) => {
unreadMessagesByChat,
intl,
layoutContextDispatch,
chatsTracker,
notify,
} = props;
const [unreadMessagesCount, setUnreadMessagesCount] = useState(0);
@ -105,35 +103,6 @@ const ChatAlert = (props) => {
}
}, [pushAlertEnabled]);
useEffect(() => {
const keys = Object.keys(chatsTracker);
keys.forEach((key) => {
if (chatsTracker[key]?.shouldNotify) {
if (audioAlertEnabled) {
AudioService.playAlertSound(`${Meteor.settings.public.app.cdn
+ Meteor.settings.public.app.basename
+ Meteor.settings.public.app.instanceId}`
+ '/resources/sounds/notify.mp3');
}
if (pushAlertEnabled) {
notify(
key === 'MAIN-PUBLIC-GROUP-CHAT'
? intl.formatMessage(intlMessages.publicChatMsg)
: intl.formatMessage(intlMessages.privateChatMsg),
'info',
'chat',
{ autoClose: 3000 },
<div>
<div style={{ fontWeight: 700 }}>{chatsTracker[key].lastSender}</div>
<div dangerouslySetInnerHTML={{ __html: chatsTracker[key].content }} />
</div>,
true,
);
}
}
});
}, [chatsTracker]);
useEffect(() => {
if (pushAlertEnabled) {
const alertsObject = unreadMessagesByChat;

View File

@ -1,7 +1,5 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import logger from '/imports/startup/client/logger';
import Auth from '/imports/ui/services/auth';
import ChatAlert from './component';
import { layoutSelect, layoutSelectInput, layoutDispatch } from '../../layout/context';
import { PANELS } from '../../layout/enums';
@ -18,15 +16,6 @@ const propTypes = {
pushAlertEnabled: PropTypes.bool.isRequired,
};
// custom hook for getting previous value
function usePrevious(value) {
const ref = React.useRef();
React.useEffect(() => {
ref.current = value;
});
return ref.current;
}
const ChatAlertContainer = (props) => {
const idChatOpen = layoutSelect((i) => i.idChatOpen);
const sidebarContent = layoutSelectInput((i) => i.sidebarContent);
@ -65,47 +54,9 @@ const ChatAlertContainer = (props) => {
})
: null;
const chatsTracker = {};
if (usingChatContext.chats) {
const chatsActive = Object.entries(usingChatContext.chats);
chatsActive.forEach((c) => {
try {
if (c[0] === idChat || (c[0] === 'MAIN-PUBLIC-GROUP-CHAT' && idChat === 'public')) {
chatsTracker[c[0]] = {};
if (c[1]?.posJoinMessages || c[1]?.messageGroups) {
const m = Object.entries(c[1]?.posJoinMessages || c[1]?.messageGroups);
const sameUserCount = m.filter((message) => message[1]?.sender === Auth.userID).length;
if (m[m.length - 1] && m[m.length - 1][1]?.sender !== Auth.userID) {
chatsTracker[c[0]].lastSender = users[Auth.meetingID][c[1]?.lastSender]?.name;
chatsTracker[c[0]].content = m[m.length - 1][1]?.message;
chatsTracker[c[0]].count = m?.length - sameUserCount;
}
}
}
} catch (e) {
logger.error({
logCode: 'chat_alert_component_error',
}, 'Error : ', e.error);
}
});
const prevTracker = usePrevious(chatsTracker);
if (prevTracker) {
const keys = Object.keys(prevTracker);
keys.forEach((key) => {
if (chatsTracker[key]?.count > (prevTracker[key]?.count || 0)) {
chatsTracker[key].shouldNotify = true;
}
});
}
}
return (
<ChatAlert
{...props}
chatsTracker={chatsTracker}
layoutContextDispatch={layoutContextDispatch}
unreadMessagesCountByChat={unreadMessagesCountByChat}
unreadMessagesByChat={unreadMessagesByChat}

View File

@ -66,10 +66,10 @@ class TimeWindowChatItem extends PureComponent {
}
componentDidUpdate(prevProps, prevState) {
const { height, forceCacheUpdate, systemMessage, index } = this.props;
const { height, forceCacheUpdate, index } = this.props;
const elementHeight = this.itemRef ? this.itemRef.clientHeight : null;
if (systemMessage && elementHeight && height !== 'auto' && elementHeight !== height && this.state.forcedUpdateCount < 10) {
if (elementHeight && height !== 'auto' && elementHeight !== height && this.state.forcedUpdateCount < 10) {
// forceCacheUpdate() internally calls forceUpdate(), so we need a stop flag
// and cannot rely on shouldComponentUpdate() and other comparisons.
forceCacheUpdate(index);
@ -158,7 +158,10 @@ class TimeWindowChatItem extends PureComponent {
const emphasizedText = messageFromModerator && CHAT_EMPHASIZE_TEXT && chatId === CHAT_PUBLIC_ID;
return (
<Styled.Item key={`time-window-${messageKey}`}>
<Styled.Item
key={`time-window-${messageKey}`}
ref={element => this.itemRef = element}
>
<Styled.Wrapper isSystemSender={isSystemSender}>
<Styled.AvatarWrapper>
<UserAvatar
@ -280,11 +283,7 @@ class TimeWindowChatItem extends PureComponent {
return this.renderSystemMessage();
}
return (
<Styled.Item>
{this.renderMessageItem()}
</Styled.Item>
);
return this.renderMessageItem();
}
}

View File

@ -101,6 +101,7 @@ const Meta = styled.div`
flex: 1;
flex-flow: row;
line-height: 1.35;
align-items: baseline;
`;
const Name = styled.div`

View File

@ -33,6 +33,15 @@ class BBBMenu extends React.Component {
this.handleClose = this.handleClose.bind(this);
}
componentDidUpdate() {
const { anchorEl } = this.state;
if (this.props.open === false && anchorEl) {
this.setState({ anchorEl: null });
} else if (this.props.open === true && !anchorEl) {
this.setState({ anchorEl: this.anchorElRef });
}
}
handleClick(event) {
this.setState({ anchorEl: event.currentTarget });
};
@ -129,6 +138,7 @@ class BBBMenu extends React.Component {
this.handleClick(e);
}}
accessKey={this.props?.accessKey}
ref={(ref) => this.anchorElRef = ref}
>
{trigger}
</div>

View File

@ -71,7 +71,9 @@ class ConfirmationModal extends Component {
</Styled.Title>
</Styled.Header>
<Styled.Description>
<span dangerouslySetInnerHTML={{ __html: description }} />
<Styled.DescriptionText>
{description}
</Styled.DescriptionText>
{ hasCheckbox ? (
<label htmlFor="confirmationCheckbox" key="confirmation-checkbox">
<Styled.Checkbox

View File

@ -55,6 +55,10 @@ const Description = styled.div`
margin-bottom: ${jumboPaddingY};
`;
const DescriptionText = styled.span`
white-space: pre-line;
`;
const Checkbox = styled.input`
position: relative;
top: 0.134rem;
@ -85,6 +89,7 @@ export default {
Header,
Title,
Description,
DescriptionText,
Checkbox,
Footer,
ConfirmationButton,

View File

@ -69,6 +69,7 @@ class Switch extends Toggle {
hasFocus={hasFocus}
disabled={disabled}
animations={animations}
isRTL={document.getElementsByTagName('html')[0].dir === 'rtl'}
/>
<Styled.ScreenreaderInput

View File

@ -1,4 +1,4 @@
import styled from 'styled-components';
import styled, { css } from 'styled-components';
import { borderSize } from '/imports/ui/stylesheets/styled-components/general';
import { colorDanger, colorSuccess } from '/imports/ui/stylesheets/styled-components/palette';
@ -121,7 +121,7 @@ const ToggleTrackX = styled.div`
const ToggleThumb = styled.div`
position: absolute;
top: 1px;
left: 1px;
left: ${({ isRTL }) => isRTL ? '2.6rem' : '1px'};
width: 1.35rem;
height: 1.35rem;
border-radius: 50%;
@ -129,16 +129,12 @@ const ToggleThumb = styled.div`
box-sizing: border-box;
box-shadow: 2px 0px 10px -1px rgba(0,0,0,0.4);
[dir="rtl"] & {
left: 2.6rem;
}
${({ animations }) => animations && `
transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0ms;
`}
${({ checked }) => checked && `
left: 2.1rem;
${({ checked }) => checked && css`
left: ${({ isRTL }) => isRTL ? '1px' : '2.1rem' };
box-shadow: -2px 0px 10px -1px rgba(0,0,0,0.4);
`}

View File

@ -29,6 +29,7 @@ const intlMessages = defineMessages({
});
let stats = -1;
let lastRtt = null;
const statsDep = new Tracker.Dependency();
let statsTimeout = null;
@ -111,11 +112,12 @@ const addConnectionStatus = (level, type, value) => {
const fetchRoundTripTime = () => {
const t0 = Date.now();
makeCall('voidConnection').then(() => {
makeCall('voidConnection', lastRtt).then(() => {
const tf = Date.now();
const rtt = tf - t0;
const event = new CustomEvent('socketstats', { detail: { rtt } });
window.dispatchEvent(event);
lastRtt = rtt;
});
};

View File

@ -46,7 +46,8 @@ class EndMeetingComponent extends PureComponent {
: intl.formatMessage(intlMessages.endMeetingNoUserDescription);
if (warnAboutUnsavedContentOnMeetingEnd) {
description += `<p>${intl.formatMessage(intlMessages.contentWarning)}</p>`;
// the double breakline it to put one empty line between the descriptions
description += `\n\n${intl.formatMessage(intlMessages.contentWarning)}`;
}
return (

View File

@ -13,6 +13,7 @@ import deviceInfo from '/imports/utils/deviceInfo';
import logger from '/imports/startup/client/logger';
import Subtitles from './subtitles/component';
import VolumeSlider from './volume-slider/component';
import ReloadButton from '/imports/ui/components/reload-button/component';
import FullscreenButtonContainer from '/imports/ui/components/common/fullscreen-button/container';
@ -34,6 +35,12 @@ const intlMessages = defineMessages({
fullscreenLabel: {
id: 'app.externalVideo.fullscreenLabel',
},
subtitlesOn: {
id: 'app.externalVideo.subtitlesOn',
},
subtitlesOff: {
id: 'app.externalVideo.subtitlesOff',
},
});
const SYNC_INTERVAL_SECONDS = 5;
@ -69,18 +76,22 @@ class VideoPlayer extends Component {
this.throttleTimeout = null;
this.state = {
subtitlesOn: false,
muted: false,
playing: false,
autoPlayBlocked: false,
volume: 1,
playbackRate: 1,
key: 0,
played:0,
loaded:0,
};
this.hideVolume = {
Vimeo: true,
Facebook: true,
ArcPlayer: true,
//YouTube: true,
};
this.opts = {
@ -113,6 +124,7 @@ class VideoPlayer extends Component {
rel: 0,
ecver: 2,
controls: isPresenter ? 1 : 0,
cc_lang_pref: document.getElementsByTagName('html')[0].lang.substring(0, 2),
},
},
peertube: {
@ -145,6 +157,7 @@ class VideoPlayer extends Component {
this.getMuted = this.getMuted.bind(this);
this.setPlaybackRate = this.setPlaybackRate.bind(this);
this.onBeforeUnload = this.onBeforeUnload.bind(this);
this.toggleSubtitle = this.toggleSubtitle.bind(this);
this.mobileHoverSetTimeout = null;
}
@ -234,6 +247,20 @@ class VideoPlayer extends Component {
}
}
toggleSubtitle() {
this.setState((state) => {
return { subtitlesOn: !state.subtitlesOn };
}, () => {
const { subtitlesOn } = this.state;
const { isPresenter } = this.props;
if (!isPresenter && subtitlesOn) {
this?.player?.getInternalPlayer()?.setOption('captions', 'reload', true);
} else {
this?.player?.getInternalPlayer()?.unloadModule('captions');
}
});
}
handleOnReady() {
const { hasPlayedBefore, playerIsReady } = this;
@ -301,12 +328,16 @@ class VideoPlayer extends Component {
}
}
handleOnProgress() {
handleOnProgress(data) {
const { mutedByEchoTest } = this.state;
const volume = this.getCurrentVolume();
const muted = this.getMuted();
const { played, loaded } = data;
this.setState({played, loaded});
if (!mutedByEchoTest) {
this.setState({ volume, muted });
}
@ -380,10 +411,10 @@ class VideoPlayer extends Component {
}
getMuted() {
const { mutedByEchoTest } = this.state;
const { mutedByEchoTest, muted } = this.state;
const intPlayer = this.player && this.player.getInternalPlayer();
return intPlayer && intPlayer.isMuted && intPlayer.isMuted() && !mutedByEchoTest;
return (intPlayer && intPlayer.isMuted && intPlayer.isMuted?.() && !mutedByEchoTest) || muted;
}
autoPlayBlockDetected() {
@ -549,7 +580,7 @@ class VideoPlayer extends Component {
const {
playing, playbackRate, mutedByEchoTest, autoPlayBlocked,
volume, muted, key, showHoverToolBar,
volume, muted, key, showHoverToolBar, played, loaded, subtitlesOn
} = this.state;
// This looks weird, but I need to get this nested player
@ -578,6 +609,7 @@ class VideoPlayer extends Component {
width,
pointerEvents: isResizing ? 'none' : 'inherit',
display: isMinimized && 'none',
background: 'var(--color-black)',
}}
>
<Styled.VideoPlayerWrapper
@ -617,10 +649,7 @@ class VideoPlayer extends Component {
!isPresenter
? [
(
<Styled.HoverToolbar
toolbarStyle={toolbarStyle}
key="hover-toolbar-external-video"
>
<Styled.HoverToolbar key="hover-toolbar-external-video">
<VolumeSlider
hideVolume={this.hideVolume[playerName]}
volume={volume}
@ -628,12 +657,32 @@ class VideoPlayer extends Component {
onMuted={this.handleOnMuted}
onVolumeChanged={this.handleVolumeChanged}
/>
<ReloadButton
handleReload={this.handleReload}
label={intl.formatMessage(intlMessages.refreshLabel)}
/>
<Styled.ButtonsWrapper>
<ReloadButton
handleReload={this.handleReload}
label={intl.formatMessage(intlMessages.refreshLabel)}
/>
{playerName === 'YouTube' && (
<Subtitles
toggleSubtitle={this.toggleSubtitle}
label={subtitlesOn
? intl.formatMessage(intlMessages.subtitlesOn)
: intl.formatMessage(intlMessages.subtitlesOff)
}
/>
)}
</Styled.ButtonsWrapper>
{this.renderFullscreenButton()}
<Styled.ProgressBar>
<Styled.Loaded
style={{ width: loaded * 100 + '%' }}
>
<Styled.Played
style={{ width: played * 100 / loaded + '%'}}
/>
</Styled.Loaded>
</Styled.ProgressBar>
</Styled.HoverToolbar>
),
(deviceInfo.isMobile && playing) && (

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