Merge pull request #13485 from antobinary/merge-24-dev

chore: Merge 2.4.x into 'develop'
This commit is contained in:
Anton Georgiev 2021-10-15 17:23:14 -04:00 committed by GitHub
commit 53a160d616
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
295 changed files with 3068 additions and 2468 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-09-30
image: gitlab.senfcall.de:5050/senfcall-public/docker-bbb-build:v2021-10-12
# 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

@ -419,7 +419,12 @@ public class ParamsProcessorUtil {
}
}
boolean learningDashboardEn = learningDashboardEnabled;
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
@ -431,7 +436,7 @@ public class ParamsProcessorUtil {
}
}
int learningDashboardCleanupMins = learningDashboardCleanupDelayInMinutes;
learningDashboardCleanupMins = learningDashboardCleanupDelayInMinutes;
if (!StringUtils.isEmpty(params.get(ApiParams.LEARNING_DASHBOARD_CLEANUP_DELAY_IN_MINUTES))) {
try {
learningDashboardCleanupMins = Integer.parseInt(params
@ -442,6 +447,8 @@ public class ParamsProcessorUtil {
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,8 +2,6 @@ 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;
@ -16,6 +14,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
@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,12 +2,12 @@ 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;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import java.util.Map;
public class EndMeeting extends RequestWithChecksum<EndMeeting.Params> {

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,17 +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(message = "You must provide either the moderator or attendee password")
@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

@ -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);
@ -121,7 +140,10 @@ class UsersTable extends React.Component {
<th className="px-4 py-3 text-center">
<FormattedMessage id="app.learningDashboard.usersTable.colRaiseHands" defaultMessage="Raise Hand" />
</th>
<th className="px-4 py-3 text-center">
<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,7 +155,12 @@ 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
@ -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;

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

@ -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
#
kurento_dir=/var/kurento/
mediasoup_dir=/var/mediasoup/
remove_stale_sfu_raw_files() {
for app in recordings screenshare; do
app_dir=/var/kurento/$app
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 '{}' +
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

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

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

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

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 !== '';

View File

@ -15,26 +15,12 @@ export default function handleBreakoutJoinURL({ body }) {
breakoutId,
};
// only keep each users' last invitation
const newUsers = [];
const currentBreakout = Breakouts.findOne({ breakoutId }, { fields: { users: 1 } });
currentBreakout.users.forEach((item) => {
if (item.userId !== userId) {
newUsers.push(item);
}
});
newUsers.push({
userId,
redirectToHtml5JoinURL,
insertedTime: new Date().getTime(),
});
const modifier = {
$set: {
users: newUsers,
[`url_${userId}`]: {
redirectToHtml5JoinURL,
insertedTime: new Date().getTime(),
},
},
};

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

@ -50,7 +50,7 @@ function breakouts() {
},
{
parentMeetingId: meetingId,
'users.userId': userId,
[`url_${userId}`]: { $exists: true },
},
{
breakoutId: meetingId,
@ -60,12 +60,7 @@ function breakouts() {
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,2 +1,3 @@
import './publishers';
import './methods';
import './eventHandlers';

View File

@ -0,0 +1,6 @@
import { Meteor } from 'meteor/meteor';
import getNoteId from './methods/getNoteId';
Meteor.methods({
getNoteId,
});

View File

@ -0,0 +1,65 @@
import Note from '/imports/api/note';
import Users from '/imports/api/users';
import Meetings from '/imports/api/meetings';
import { extractCredentials } from '/imports/api/common/server/helpers';
const ROLE_VIEWER = Meteor.settings.public.user.role_viewer;
const hasNoteAccess = (meetingId, userId) => {
const user = Users.findOne(
{ meetingId, userId },
{
fields: {
role: 1,
locked: 1,
},
}
);
if (!user) return false;
if (user.role === ROLE_VIEWER && user.locked) {
const meeting = Meetings.findOne(
{ meetingId },
{ fields: { 'lockSettingsProps.disableNote': 1 } }
);
if (!meeting) return false;
const { lockSettingsProps } = meeting;
if (lockSettingsProps) {
if (lockSettingsProps.disableNote) return false;
} else {
return false;
}
}
return true;
};
export default function getNoteId() {
try {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
const note = Note.findOne(
{ meetingId },
{
fields: {
noteId: 1,
readOnlyNoteId: 1,
},
}
);
if (note) {
if (hasNoteAccess(meetingId, requesterUserId)) {
return note.noteId;
}
return note.readOnlyNoteId;
}
return null;
} catch (err) {
return null;
}
}

View File

@ -15,7 +15,14 @@ function note() {
Logger.info(`Publishing Note for ${meetingId} ${userId}`);
return Note.find({ meetingId });
const options = {
fields: {
noteId: 0,
readOnlyNoteId: 0,
},
};
return Note.find({ meetingId }, options);
}
function publish(...args) {

View File

@ -8,6 +8,7 @@ import { SCREENSHARING_ERRORS } from './errors';
const SFU_CONFIG = Meteor.settings.public.kurento;
const SFU_URL = SFU_CONFIG.wsUrl;
const OFFERING = SFU_CONFIG.screenshare.subscriberOffering;
const SIGNAL_CANDIDATES = Meteor.settings.public.kurento.signalCandidates;
const BRIDGE_NAME = 'kurento'
const SCREENSHARE_VIDEO_TAG = 'screenshareVideo';
@ -225,6 +226,7 @@ export default class KurentoScreenshareBridge {
hasAudio,
offering: OFFERING,
mediaServer: BridgeService.getMediaServerAdapter(),
signalCandidates: SIGNAL_CANDIDATES,
};
this.broker = new ScreenshareBroker(
@ -284,6 +286,7 @@ export default class KurentoScreenshareBridge {
bitrate: BridgeService.BASE_BITRATE,
offering: true,
mediaServer: BridgeService.getMediaServerAdapter(),
signalCandidates: SIGNAL_CANDIDATES,
};
this.broker = new ScreenshareBroker(

View File

@ -29,8 +29,6 @@ export default function handlePresenterAssigned({ body }, meetingId) {
presenter: true,
};
const prevPresenter = Users.findOne(selector);
const defaultPodSelector = {
meetingId,
podId: 'DEFAULT_PRESENTATION_POD',
@ -44,28 +42,23 @@ export default function handlePresenterAssigned({ body }, meetingId) {
presenterId,
};
// no previous presenters
// The below code is responsible for set Meeting presenter to be default pod presenter as well.
// It's been handled here because right now akka-apps don't handle all cases scenarios.
if (!prevPresenter) {
const { currentPresenterId } = currentDefaultPod;
if (currentPresenterId === '') {
return setPresenterInPodReqMsg(setPresenterPayload);
}
const oldPresenter = Users.findOne({ meetingId, userId: currentPresenterId });
if (oldPresenter?.userId !== currentPresenterId) {
return setPresenterInPodReqMsg(setPresenterPayload);
}
return true;
}
if (currentDefaultPod && currentDefaultPod.currentPresenterId !== presenterId) {
setPresenterInPodReqMsg(setPresenterPayload);
}
const prevPresenter = Users.findOne(selector);
if (prevPresenter) {
changePresenter(false, prevPresenter.userId, meetingId, assignedBy);
}
/**
* In the cases where the first moderator joins the meeting or
* the current presenter left the meeting, akka-apps doesn't assign the new presenter
* to the default presentation pod. This step is done manually here.
*/
if (currentDefaultPod.currentPresenterId !== presenterId) {
const presenterToBeAssigned = Users.findOne({ userId: presenterId });
if (!presenterToBeAssigned) setPresenterPayload.presenterId = '';
setPresenterInPodReqMsg(setPresenterPayload);
}
}

View File

@ -124,6 +124,7 @@ class ActionsDropdown extends PureComponent {
stopExternalVideoShare,
mountModal,
layoutContextDispatch,
hidePresentation,
} = this.props;
const {
@ -138,7 +139,7 @@ class ActionsDropdown extends PureComponent {
const actions = [];
if (amIPresenter) {
if (amIPresenter && !hidePresentation) {
actions.push({
icon: "presentation",
dataTest: "uploadPresentation",
@ -183,7 +184,7 @@ class ActionsDropdown extends PureComponent {
if (amIPresenter && allowExternalVideo) {
actions.push({
icon: "video",
icon: !isSharingVideo ? "external-video" : "external-video_off",
label: !isSharingVideo ? intl.formatMessage(intlMessages.startExternalVideoLabel)
: intl.formatMessage(intlMessages.stopExternalVideoLabel),
key: "external-video",

View File

@ -5,6 +5,7 @@ import PresentationUploaderService from '/imports/ui/components/presentation/pre
import PresentationPodService from '/imports/ui/components/presentation-pod/service';
import ActionsDropdown from './component';
import { layoutSelectInput, layoutDispatch } from '../../layout/context';
import getFromUserSettings from '/imports/ui/services/users-settings';
const ActionsDropdownContainer = (props) => {
const sidebarContent = layoutSelectInput((i) => i.sidebarContent);
@ -22,6 +23,8 @@ const ActionsDropdownContainer = (props) => {
);
};
const LAYOUT_CONFIG = Meteor.settings.public.layout;
export default withTracker(() => {
const presentations = Presentations.find({ 'conversion.done': true }).fetch();
return ({
@ -29,5 +32,6 @@ export default withTracker(() => {
isDropdownOpen: Session.get('dropdownOpen'),
setPresentation: PresentationUploaderService.setPresentation,
podIds: PresentationPodService.getPresentationPodIds(),
hidePresentation: getFromUserSettings('bbb_hide_presentation', LAYOUT_CONFIG.hidePresentation),
});
})(ActionsDropdownContainer);

View File

@ -21,6 +21,7 @@ class ActionsBar extends PureComponent {
handleTakePresenter,
intl,
isSharingVideo,
hasScreenshare,
stopExternalVideoShare,
isCaptionsAvailable,
isMeteorConnected,
@ -80,15 +81,14 @@ class ActionsBar extends PureComponent {
/>
</div>
<div className={styles.right}>
{isLayoutSwapped && !isPresentationDisabled
? (
<PresentationOptionsContainer
isLayoutSwapped={isLayoutSwapped}
toggleSwapLayout={toggleSwapLayout}
layoutContextDispatch={layoutContextDispatch}
isThereCurrentPresentation={isThereCurrentPresentation}
hasPresentation={isThereCurrentPresentation}
hasExternalVideo={isSharingVideo}
hasScreenshare={hasScreenshare}
/>
)
: null}
{isRaiseHandButtonEnabled
? (
<Button

View File

@ -13,10 +13,10 @@ import UserListService from '/imports/ui/components/user-list/service';
import ExternalVideoService from '/imports/ui/components/external-video-player/service';
import CaptionsService from '/imports/ui/components/captions/service';
import { layoutSelectOutput, layoutDispatch } from '../layout/context';
import { isVideoBroadcasting } from '/imports/ui/components/screenshare/service';
import MediaService, {
getSwapLayout,
shouldEnableSwapLayout,
} from '../media/service';
const ActionsBarContainer = (props) => {
@ -51,12 +51,13 @@ export default withTracker(() => ({
amIModerator: Service.amIModerator(),
stopExternalVideoShare: ExternalVideoService.stopWatching,
enableVideo: getFromUserSettings('bbb_enable_video', Meteor.settings.public.kurento.enableVideo),
isLayoutSwapped: getSwapLayout() && shouldEnableSwapLayout(),
isLayoutSwapped: getSwapLayout(),
toggleSwapLayout: MediaService.toggleSwapLayout,
handleTakePresenter: Service.takePresenterRole,
currentSlidHasContent: PresentationService.currentSlidHasContent(),
parseCurrentSlideContent: PresentationService.parseCurrentSlideContent,
isSharingVideo: Service.isSharingVideo(),
hasScreenshare: isVideoBroadcasting(),
isCaptionsAvailable: CaptionsService.isCaptionsAvailable(),
isMeteorConnected: Meteor.status().connected,
isPollingEnabled: POLLING_ENABLED,

View File

@ -227,7 +227,8 @@ class BreakoutRoom extends PureComponent {
componentDidUpdate(prevProps, prevstate) {
if (this.listOfUsers) {
for (let i = 0; i < this.listOfUsers.children.length; i += 1) {
const roomList = this.listOfUsers.children[i].getElementsByTagName('div')[0];
const roomWrapperChildren = this.listOfUsers.children[i].getElementsByTagName('div');
const roomList = roomWrapperChildren[roomWrapperChildren.length > 1 ? 1 : 0];
roomList.addEventListener('keydown', this.handleMoveEvent, true);
}
}

View File

@ -2,6 +2,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import Button from '/imports/ui/components/button/component';
import MediaService from '/imports/ui/components/media/service';
import cx from 'classnames';
import { styles } from '../styles';
const propTypes = {
intl: PropTypes.shape({
@ -11,6 +14,14 @@ const propTypes = {
};
const intlMessages = defineMessages({
minimizePresentationLabel: {
id: 'app.actionsBar.actionsDropdown.minimizePresentationLabel',
description: '',
},
minimizePresentationDesc: {
id: 'app.actionsBar.actionsDropdown.restorePresentationDesc',
description: '',
},
restorePresentationLabel: {
id: 'app.actionsBar.actionsDropdown.restorePresentationLabel',
description: 'Restore Presentation option label',
@ -23,24 +34,41 @@ const intlMessages = defineMessages({
const PresentationOptionsContainer = ({
intl,
isLayoutSwapped,
toggleSwapLayout,
isThereCurrentPresentation,
layoutContextDispatch,
}) => (
hasPresentation,
hasExternalVideo,
hasScreenshare,
}) => {
let buttonType = 'presentation';
if (hasExternalVideo) {
// hack until we have an external-video icon
buttonType = 'external-video';
} else if (hasScreenshare) {
buttonType = 'desktop';
}
const isThereCurrentPresentation = hasExternalVideo || hasScreenshare || hasPresentation;
return (
<Button
icon="presentation"
data-test="restorePresentationButton"
label={intl.formatMessage(intlMessages.restorePresentationLabel)}
description={intl.formatMessage(intlMessages.restorePresentationDesc)}
color="primary"
className={cx(styles.button, !isLayoutSwapped || styles.btn)}
icon={`${buttonType}${isLayoutSwapped ? '_off' : ''}`}
label={intl.formatMessage(isLayoutSwapped ? intlMessages.restorePresentationLabel : intlMessages.minimizePresentationLabel)}
aria-label={intl.formatMessage(isLayoutSwapped ? intlMessages.restorePresentationLabel : intlMessages.minimizePresentationLabel)}
aria-describedby={intl.formatMessage(isLayoutSwapped ? intlMessages.restorePresentationDesc : intlMessages.minimizePresentationDesc)}
description={intl.formatMessage(isLayoutSwapped ? intlMessages.restorePresentationDesc : intlMessages.minimizePresentationDesc)}
color={!isLayoutSwapped ? "primary" : "default"}
hideLabel
circle
size="lg"
onClick={() => toggleSwapLayout(layoutContextDispatch)}
id="restore-presentation"
ghost={isLayoutSwapped}
disabled={!isThereCurrentPresentation}
/>
);
};
PresentationOptionsContainer.propTypes = propTypes;
export default injectIntl(PresentationOptionsContainer);

View File

@ -149,6 +149,7 @@ class App extends Component {
meetingLayout,
settingsLayout,
isRTL,
hidePresentation,
} = this.props;
const { browserName } = browserInfo;
const { osName } = deviceInfo;
@ -160,6 +161,11 @@ class App extends Component {
value: isRTL,
});
layoutContextDispatch({
type: ACTIONS.SET_PRESENTATION_IS_OPEN,
value: !hidePresentation,
});
MediaService.setSwapLayout(layoutContextDispatch);
Modal.setAppElement('#app');

View File

@ -167,6 +167,7 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
}).fetch();
const AppSettings = Settings.application;
const { selectedLayout } = AppSettings;
const { viewScreenshare } = Settings.dataSaving;
const shouldShowExternalVideo = MediaService.shouldShowExternalVideo();
const shouldShowScreenshare = MediaService.shouldShowScreenshare()
@ -177,6 +178,8 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
customStyleUrl = CUSTOM_STYLE_URL;
}
const LAYOUT_CONFIG = Meteor.settings.public.layout;
return {
captions: CaptionsService.isCaptionsActive() ? <CaptionsContainer /> : null,
fontSize: getFontSize(),
@ -195,8 +198,8 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
currentUserId: currentUser?.userId,
isPresenter: currentUser?.presenter,
meetingLayout: layout,
settingsLayout: AppSettings.selectedLayout,
pushLayoutToEveryone: AppSettings.pushLayoutToEveryone,
settingsLayout: selectedLayout?.replace('Push', ''),
pushLayoutToEveryone: selectedLayout?.includes('Push'),
audioAlertEnabled: AppSettings.chatAudioAlerts,
pushAlertEnabled: AppSettings.chatPushAlerts,
shouldShowScreenshare,
@ -207,6 +210,7 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
'bbb_force_restore_presentation_on_new_events',
Meteor.settings.public.presentation.restoreOnUpdate,
),
hidePresentation: getFromUserSettings('bbb_hide_presentation', LAYOUT_CONFIG.hidePresentation),
};
})(AppContainer)));

View File

@ -192,7 +192,7 @@ class BreakoutJoinConfirmation extends Component {
))
}
</select>
{ waiting ? <span>{intl.formatMessage(intlMessages.generatingURL)}</span> : null}
{ waiting ? <span data-test="labelGeneratingURL">{intl.formatMessage(intlMessages.generatingURL)}</span> : null}
</div>
);
}

View File

@ -15,9 +15,9 @@ const BreakoutJoinConfirmationContrainer = (props) => (
const getURL = (breakoutId) => {
const currentUserId = Auth.userID;
const getBreakout = Breakouts.findOne({ breakoutId }, { fields: { users: 1 } });
const user = getBreakout ? getBreakout.users?.find((u) => u.userId === currentUserId) : '';
if (user) return user.redirectToHtml5JoinURL;
const breakout = Breakouts.findOne({ breakoutId }, { fields: { [`url_${currentUserId}`]: 1 } });
const breakoutUrlData = (breakout && breakout[`url_${currentUserId}`]) ? breakout[`url_${currentUserId}`] : null;
if (breakoutUrlData) return breakoutUrlData.redirectToHtml5JoinURL;
return '';
};

View File

@ -43,18 +43,14 @@ const intlMessages = defineMessages({
id: 'app.createBreakoutRoom.returnAudio',
description: 'label for option to return audio',
},
generateURL: {
id: 'app.createBreakoutRoom.generateURL',
askToJoin: {
id: 'app.createBreakoutRoom.askToJoin',
description: 'label for generate breakout room url',
},
generatingURL: {
id: 'app.createBreakoutRoom.generatingURL',
description: 'label for generating breakout room url',
},
generatedURL: {
id: 'app.createBreakoutRoom.generatedURL',
description: 'label for generated breakout room url',
},
endAllBreakouts: {
id: 'app.createBreakoutRoom.endAllBreakouts',
description: 'Button label to end all breakout rooms',
@ -125,7 +121,7 @@ class BreakoutRoom extends PureComponent {
componentDidUpdate() {
const {
breakoutRoomUser,
getBreakoutRoomUrl,
setBreakoutAudioTransferStatus,
isMicrophoneUser,
isReconnecting,
@ -144,10 +140,11 @@ class BreakoutRoom extends PureComponent {
}
if (waiting && !generated) {
const breakoutUser = breakoutRoomUser(requestedBreakoutId);
const breakoutUrlData = getBreakoutRoomUrl(requestedBreakoutId);
if (!breakoutUser) return false;
if (breakoutUser.redirectToHtml5JoinURL !== '') {
if (!breakoutUrlData) return false;
if (breakoutUrlData.redirectToHtml5JoinURL !== '') {
window.open(breakoutUrlData.redirectToHtml5JoinURL, '_blank');
_.delay(() => this.setState({ generated: true, waiting: false }), 1000);
}
}
@ -164,10 +161,10 @@ class BreakoutRoom extends PureComponent {
getBreakoutURL(breakoutId) {
Session.set('lastBreakoutOpened', breakoutId);
const { requestJoinURL, breakoutRoomUser } = this.props;
const { requestJoinURL, getBreakoutRoomUrl } = this.props;
const { waiting } = this.state;
const hasUser = breakoutRoomUser(breakoutId);
if (!hasUser && !waiting) {
const breakoutRoomUrlData = getBreakoutRoomUrl(breakoutId);
if (!breakoutRoomUrlData && !waiting) {
this.setState(
{
waiting: true,
@ -178,28 +175,28 @@ class BreakoutRoom extends PureComponent {
);
}
if (hasUser) {
window.open(hasUser.redirectToHtml5JoinURL, '_blank');
if (breakoutRoomUrlData) {
window.open(breakoutRoomUrlData.redirectToHtml5JoinURL, '_blank');
this.setState({ waiting: false, generated: false });
}
return null;
}
getBreakoutLabel(breakoutId) {
const { intl, breakoutRoomUser } = this.props;
const { intl, getBreakoutRoomUrl } = this.props;
const { requestedBreakoutId, generated } = this.state;
const hasUser = breakoutRoomUser(breakoutId);
const breakoutRoomUrlData = getBreakoutRoomUrl(breakoutId);
if (generated && requestedBreakoutId === breakoutId) {
return intl.formatMessage(intlMessages.generatedURL);
}
if (hasUser) {
return intl.formatMessage(intlMessages.breakoutJoin);
}
return intl.formatMessage(intlMessages.generateURL);
if (breakoutRoomUrlData) {
return intl.formatMessage(intlMessages.breakoutJoin);
}
return intl.formatMessage(intlMessages.askToJoin);
}
clearJoinedAudioOnly() {
@ -490,7 +487,7 @@ class BreakoutRoom extends PureComponent {
messageDuration={intlMessages.breakoutDuration}
breakoutRoom={breakoutRooms[0]}
/>
{!visibleExtendTimeForm
{amIModerator && !visibleExtendTimeForm
? (
<Button
onClick={this.showExtendTimeForm}
@ -539,6 +536,7 @@ class BreakoutRoom extends PureComponent {
size="lg"
label={intl.formatMessage(intlMessages.endAllBreakouts)}
className={styles.endButton}
data-test="endBreakoutRoomsButton"
onClick={() => {
this.closePanel();
endAllBreakouts();

View File

@ -19,7 +19,7 @@ export default withTracker((props) => {
extendBreakoutsTime,
isExtendTimeHigherThanMeetingRemaining,
findBreakouts,
breakoutRoomUser,
getBreakoutRoomUrl,
transferUserToMeeting,
transferToBreakout,
meetingId,
@ -43,7 +43,7 @@ export default withTracker((props) => {
requestJoinURL,
extendBreakoutsTime,
isExtendTimeHigherThanMeetingRemaining,
breakoutRoomUser,
getBreakoutRoomUrl,
transferUserToMeeting,
transferToBreakout,
isMicrophoneUser,

View File

@ -9,10 +9,9 @@ const BREAKOUT_MODAL_DELAY = 200;
const propTypes = {
mountModal: PropTypes.func.isRequired,
currentBreakoutUser: PropTypes.shape({
currentBreakoutUrlData: PropTypes.shape({
insertedTime: PropTypes.number.isRequired,
}),
getBreakoutByUser: PropTypes.func.isRequired,
breakoutUserIsIn: PropTypes.shape({
sequence: PropTypes.number.isRequired,
}),
@ -22,7 +21,7 @@ const propTypes = {
};
const defaultProps = {
currentBreakoutUser: undefined,
currentBreakoutUrlData: undefined,
breakoutUserIsIn: undefined,
breakouts: [],
};
@ -55,8 +54,8 @@ class BreakoutRoomInvitation extends Component {
checkBreakouts(oldProps) {
const {
breakouts,
currentBreakoutUser,
getBreakoutByUser,
currentBreakoutUrlData,
getBreakoutByUrlData,
breakoutUserIsIn,
} = this.props;
@ -67,19 +66,19 @@ class BreakoutRoomInvitation extends Component {
const hasBreakouts = breakouts.length > 0;
if (hasBreakouts && !breakoutUserIsIn && BreakoutService.checkInviteModerators()) {
// Have to check for freeJoin breakouts first because currentBreakoutUser will
// Have to check for freeJoin breakouts first because currentBreakoutUrlData will
// populate after a room has been joined
const breakoutRoom = getBreakoutByUser(currentBreakoutUser);
const freeJoinBreakout = breakouts.find(breakout => breakout.freeJoin);
const breakoutRoom = getBreakoutByUrlData(currentBreakoutUrlData);
const freeJoinBreakout = breakouts.find((breakout) => breakout.freeJoin);
if (freeJoinBreakout) {
if (!didSendBreakoutInvite) {
this.inviteUserToBreakout(breakoutRoom || freeJoinBreakout);
this.setState({ didSendBreakoutInvite: true });
}
} else if (currentBreakoutUser) {
const currentInsertedTime = currentBreakoutUser.insertedTime;
const oldCurrentUser = oldProps.currentBreakoutUser || {};
const oldInsertedTime = oldCurrentUser.insertedTime;
} else if (currentBreakoutUrlData) {
const currentInsertedTime = currentBreakoutUrlData.insertedTime;
const oldCurrentUrlData = oldProps.currentBreakoutUrlData || {};
const oldInsertedTime = oldCurrentUrlData.insertedTime;
if (currentInsertedTime !== oldInsertedTime) {
const breakoutId = Session.get('lastBreakoutOpened');
if (breakoutRoom.breakoutId !== breakoutId) {

View File

@ -15,7 +15,7 @@ const BreakoutRoomInvitationContainer = ({ isMeetingBreakout, ...props }) => {
export default withTracker(() => ({
isMeetingBreakout: AppService.meetingIsBreakout(),
breakouts: BreakoutService.getBreakoutsNoTime(),
getBreakoutByUser: BreakoutService.getBreakoutByUser,
currentBreakoutUser: BreakoutService.getBreakoutUserByUserId(Auth.userID),
getBreakoutByUrlData: BreakoutService.getBreakoutByUrlData,
currentBreakoutUrlData: BreakoutService.getBreakoutUrlByUserId(Auth.userID),
breakoutUserIsIn: BreakoutService.getBreakoutUserIsIn(Auth.userID),
}))(BreakoutRoomInvitationContainer);

View File

@ -10,22 +10,30 @@ import fp from 'lodash/fp';
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
const findBreakouts = () => {
const BreakoutRooms = Breakouts.find({
const BreakoutRooms = Breakouts.find(
{
parentMeetingId: Auth.meetingID,
}, {
},
{
sort: {
sequence: 1,
},
}).fetch();
}
).fetch();
return BreakoutRooms;
};
const breakoutRoomUser = (breakoutId) => {
const getBreakoutRoomUrl = (breakoutId) => {
const breakoutRooms = findBreakouts();
const breakoutRoom = breakoutRooms.filter(breakout => breakout.breakoutId === breakoutId).shift();
const breakoutUser = breakoutRoom.users?.filter(user => user.userId === Auth.userID).shift();
return breakoutUser;
const breakoutRoom = breakoutRooms
.filter((breakout) => breakout.breakoutId === breakoutId)
.shift();
const breakoutUrlData =
breakoutRoom && breakoutRoom[`url_${Auth.userID}`]
? breakoutRoom[`url_${Auth.userID}`]
: null;
return breakoutUrlData;
};
const endAllBreakouts = () => {
@ -47,8 +55,9 @@ const isExtendTimeHigherThanMeetingRemaining = (extendTimeInMinutes) => {
if (timeRemaining) {
const breakoutRooms = findBreakouts();
const breakoutRoomsTimeRemaining = (breakoutRooms[0]).timeRemaining;
const newBreakoutRoomsRemainingTime = breakoutRoomsTimeRemaining + (extendTimeInMinutes * 60);
const breakoutRoomsTimeRemaining = breakoutRooms[0].timeRemaining;
const newBreakoutRoomsRemainingTime =
breakoutRoomsTimeRemaining + extendTimeInMinutes * 60;
// Keep margin of 5 seconds for breakout rooms end before parent meeting
const meetingTimeRemainingWithMargin = timeRemaining - 5;
@ -71,18 +80,24 @@ const extendBreakoutsTime = (extendTimeInMinutes) => {
return true;
};
const transferUserToMeeting = (fromMeetingId, toMeetingId) => makeCall('transferUser', fromMeetingId, toMeetingId);
const transferUserToMeeting = (fromMeetingId, toMeetingId) =>
makeCall('transferUser', fromMeetingId, toMeetingId);
const transferToBreakout = (breakoutId) => {
const breakoutRooms = findBreakouts();
const breakoutRoom = breakoutRooms.filter(breakout => breakout.breakoutId === breakoutId).shift();
const breakoutMeeting = Meetings.findOne({
const breakoutRoom = breakoutRooms
.filter((breakout) => breakout.breakoutId === breakoutId)
.shift();
const breakoutMeeting = Meetings.findOne(
{
$and: [
{ 'breakoutProps.sequence': breakoutRoom.sequence },
{ 'breakoutProps.parentId': breakoutRoom.parentMeetingId },
{ 'meetingProp.isBreakout': true },
],
}, { fields: { meetingId: 1 } });
},
{ fields: { meetingId: 1 } }
);
transferUserToMeeting(Auth.meetingID, breakoutMeeting.meetingId);
};
@ -94,48 +109,56 @@ const amIModerator = () => {
const checkInviteModerators = () => {
const BREAKOUTS_CONFIG = Meteor.settings.public.app.breakouts;
return !((amIModerator() && !BREAKOUTS_CONFIG.sendInvitationToIncludedModerators));
return !(
amIModerator() && !BREAKOUTS_CONFIG.sendInvitationToIncludedModerators
);
};
const getBreakoutByUserId = userId => Breakouts.find(
{ 'users.userId': userId },
{ fields: { timeRemaining: 0 } },
const getBreakoutByUserId = (userId) =>
Breakouts.find(
{ [`url_${userId}`]: { $exists: true } },
{ fields: { timeRemaining: 0 } }
).fetch();
const getBreakoutByUser = user => Breakouts.findOne({ users: user });
const getBreakoutByUrlData = (breakoutUrlData) =>
Breakouts.findOne({ [`url_${Auth.userID}`]: breakoutUrlData });
const getUsersFromBreakouts = breakoutsArray => breakoutsArray
.map(breakout => breakout.users)
.reduce((acc, usersArray) => [...acc, ...usersArray], []);
const getUrlFromBreakouts = (userId) => (breakoutsArray) =>
breakoutsArray
.map((breakout) => breakout[`url_${userId}`])
.reduce((acc, urlDataArray) => acc.concat(urlDataArray), []);
const filterUserURLs = userId => breakoutUsersArray => breakoutUsersArray
.filter(user => user.userId === userId);
const getLastURLInserted = (breakoutURLArray) =>
breakoutURLArray.sort((a, b) => a.insertedTime - b.insertedTime).pop();
const getLastURLInserted = breakoutURLArray => breakoutURLArray
.sort((a, b) => a.insertedTime - b.insertedTime).pop();
const getBreakoutUserByUserId = userId => fp.pipe(
const getBreakoutUrlByUserId = (userId) =>
fp.pipe(
getBreakoutByUserId,
getUsersFromBreakouts,
filterUserURLs(userId),
getLastURLInserted,
getUrlFromBreakouts(userId),
getLastURLInserted
)(userId);
const getBreakouts = () => Breakouts.find({}, { sort: { sequence: 1 } }).fetch();
const getBreakoutsNoTime = () => Breakouts.find(
const getBreakouts = () =>
Breakouts.find({}, { sort: { sequence: 1 } }).fetch();
const getBreakoutsNoTime = () =>
Breakouts.find(
{},
{
sort: { sequence: 1 },
fields: { timeRemaining: 0 },
},
}
).fetch();
const getBreakoutUserIsIn = userId => Breakouts.findOne({ 'joinedUsers.userId': new RegExp(`^${userId}`) }, { fields: { sequence: 1 } });
const getBreakoutUserIsIn = (userId) =>
Breakouts.findOne(
{ 'joinedUsers.userId': new RegExp(`^${userId}`) },
{ fields: { sequence: 1 } }
);
const isUserInBreakoutRoom = (joinedUsers) => {
const userId = Auth.userID;
return !!joinedUsers.find(user => user.userId.startsWith(userId));
return !!joinedUsers.find((user) => user.userId.startsWith(userId));
};
export default {
@ -144,16 +167,15 @@ export default {
extendBreakoutsTime,
isExtendTimeHigherThanMeetingRemaining,
requestJoinURL,
breakoutRoomUser,
getBreakoutRoomUrl,
transferUserToMeeting,
transferToBreakout,
meetingId: () => Auth.meetingID,
amIModerator,
getBreakoutUserByUserId,
getBreakoutByUser,
getBreakoutUrlByUserId,
getBreakoutByUrlData,
getBreakouts,
getBreakoutsNoTime,
getBreakoutByUserId,
getBreakoutUserIsIn,
sortUsersByName: UserListService.sortUsersByName,
isUserInBreakoutRoom,

View File

@ -12,7 +12,7 @@ const SIZES = [
];
const COLORS = [
'default', 'primary', 'danger', 'warning', 'success', 'dark', 'offline',
'default', 'primary', 'danger', 'warning', 'success', 'dark', 'offline', 'muted',
];
const propTypes = {

View File

@ -34,6 +34,10 @@
--btn-offline-bg: var(--color-offline);
--btn-offline-border: var(--color-offline);
--btn-muted-color: var(--color-muted);
--btn-muted-bg: var(--color-muted-background);
--btn-muted-border: var(--color-muted-background);
--btn-border-size: var(--border-size);
--btn-border-radius: var(--border-radius);
--btn-font-weight: 600;
@ -325,6 +329,10 @@
@include button-variant(var(--btn-offline-color), var(--btn-offline-bg), var(--btn-offline-border));
}
.muted {
@include button-variant(var(--btn-muted-color), var(--btn-muted-bg), var(--btn-muted-border));
}
/* Styles
* ==========
*/
@ -362,6 +370,10 @@
&.offline {
@include button-ghost-variant(var(--btn-offline-bg), var(--btn-offline-color));
}
&.muted {
@include button-ghost-variant(var(--btn-muted-bg), var(--btn-muted-color));
}
}
.circle {

View File

@ -23,11 +23,11 @@ class Captions extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
const {
padId,
locale,
revs,
} = this.props;
if (padId === nextProps.padId) {
if (locale === nextProps.locale) {
if (revs === nextProps.revs && !nextState.clear) return false;
}
return true;
@ -124,7 +124,7 @@ class Captions extends React.Component {
export default Captions;
Captions.propTypes = {
padId: PropTypes.string.isRequired,
locale: PropTypes.string.isRequired,
revs: PropTypes.number.isRequired,
data: PropTypes.string.isRequired,
};

View File

@ -9,13 +9,13 @@ const CaptionsContainer = props => (
export default withTracker(() => {
const {
padId,
locale,
revs,
data,
} = CaptionsService.getCaptionsData();
return {
padId,
locale,
revs,
data,
};

View File

@ -58,8 +58,6 @@ const propTypes = {
locale: PropTypes.string.isRequired,
ownerId: PropTypes.string.isRequired,
currentUserId: PropTypes.string.isRequired,
padId: PropTypes.string.isRequired,
readOnlyPadId: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
@ -85,6 +83,7 @@ class Pad extends PureComponent {
this.state = {
listening: false,
url: null,
};
const { locale, intl } = props;
@ -104,7 +103,13 @@ class Pad extends PureComponent {
}
}
componentDidUpdate() {
componentDidMount() {
const { locale } = this.props;
this.updatePadURL(locale);
}
componentDidUpdate(prevProps) {
const {
locale,
ownerId,
@ -122,6 +127,16 @@ class Pad extends PureComponent {
}
this.recognition.lang = locale;
}
if (prevProps.ownerId !== ownerId || prevProps.locale !== locale) {
this.updatePadURL(locale);
}
}
updatePadURL(locale) {
PadService.getPadId(locale).then(response => {
this.setState({ url: PadService.buildPadURL(response) });
});
}
handleListen() {
@ -212,16 +227,16 @@ class Pad extends PureComponent {
const {
locale,
intl,
padId,
readOnlyPadId,
ownerId,
name,
layoutContextDispatch,
isResizing,
} = this.props;
const { listening } = this.state;
const url = PadService.getPadURL(padId, readOnlyPadId, ownerId);
const {
listening,
url,
} = this.state;
return (
<div className={styles.pad}>

View File

@ -40,19 +40,14 @@ export default withTracker(() => {
const locale = Session.get('captionsLocale');
const caption = CaptionsService.getCaptions(locale);
const {
padId,
name,
ownerId,
readOnlyPadId,
} = caption;
const { name } = caption ? caption.locale : '';
return {
locale,
name,
ownerId,
padId,
readOnlyPadId,
currentUserId: Auth.userID,
amIModerator: CaptionsService.amIModerator(),
};

View File

@ -1,4 +1,5 @@
import Auth from '/imports/ui/services/auth';
import { makeCall } from '/imports/ui/services/api';
import Settings from '/imports/ui/services/settings';
const NOTE_CONFIG = Meteor.settings.public.note;
@ -21,18 +22,19 @@ const getPadParams = () => {
return params.join('&');
};
const getPadURL = (padId, readOnlyPadId, ownerId) => {
const userId = Auth.userID;
const getPadId = (locale) => makeCall('getPadId', locale);
const buildPadURL = (padId) => {
if (padId) {
const params = getPadParams();
let url;
if (!ownerId || userId === ownerId) {
url = Auth.authenticateURL(`${NOTE_CONFIG.url}/p/${padId}?${params}`);
} else {
url = Auth.authenticateURL(`${NOTE_CONFIG.url}/p/${readOnlyPadId}?${params}`);
}
const url = Auth.authenticateURL(`${NOTE_CONFIG.url}/p/${padId}?${params}`);
return url;
}
return null;;
};
export default {
getPadURL,
getPadId,
buildPadURL,
};

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