Merge remote-tracking branch 'upstream/v3.0.x-release' into PR_19785

This commit is contained in:
Tainan Felipe 2024-03-20 11:59:45 -03:00
commit 3105bee74b
198 changed files with 1414 additions and 1205 deletions

View File

@ -17,7 +17,7 @@ As such, we recommend that all administrators deploy 2.7 going forward. You'll
## Reporting a Vulnerability
If you believe you have found a security vunerability in BigBlueButton please let us know directly by
If you believe you have found a security vulnerability in BigBlueButton please let us know directly by
- using GitHub's "Report a vulnerability" functionality on https://github.com/bigbluebutton/bigbluebutton/security/advisories
- or e-mailing security@bigbluebutton.org with as much detail as possible.

View File

@ -16,7 +16,7 @@ object Dependencies {
val pekkoHttpVersion = "1.0.0"
val gson = "2.8.9"
val jackson = "2.13.5"
val logback = "1.2.11"
val logback = "1.2.13"
val quicklens = "1.7.5"
val spray = "1.3.6"

View File

@ -13,6 +13,7 @@ trait SystemConfiguration {
lazy val bbbWebPort = Try(config.getInt("services.bbbWebPort")).getOrElse(8888)
lazy val bbbWebAPI = Try(config.getString("services.bbbWebAPI")).getOrElse("localhost")
lazy val bbbWebSharedSecret = Try(config.getString("services.sharedSecret")).getOrElse("changeme")
lazy val checkSumAlgorithmForBreakouts = Try(config.getString("services.checkSumAlgorithmForBreakouts")).getOrElse("sha256")
lazy val bbbWebModeratorPassword = Try(config.getString("services.moderatorPassword")).getOrElse("changeme")
lazy val bbbWebViewerPassword = Try(config.getString("services.viewerPassword")).getOrElse("changeme")
lazy val keysExpiresInSec = Try(config.getInt("redis.keyExpiry")).getOrElse(14 * 86400) // 14 days

View File

@ -4,6 +4,7 @@ import org.bigbluebutton.core.running.MeetingActor
import java.net.URLEncoder
import scala.collection.SortedSet
import org.apache.commons.codec.digest.DigestUtils
import org.bigbluebutton.SystemConfiguration
trait BreakoutApp2x extends BreakoutRoomCreatedMsgHdlr
with BreakoutRoomsListMsgHdlr
@ -26,7 +27,7 @@ trait BreakoutApp2x extends BreakoutRoomCreatedMsgHdlr
}
object BreakoutRoomsUtil {
object BreakoutRoomsUtil extends SystemConfiguration {
def createMeetingIds(id: String, index: Int): (String, String) = {
val timeStamp = System.currentTimeMillis()
val externalHash = DigestUtils.sha1Hex(id.concat("-").concat(timeStamp.toString()).concat("-").concat(index.toString()))
@ -48,7 +49,13 @@ object BreakoutRoomsUtil {
//checksum() -- Return a checksum based on SHA-1 digest
//
def checksum(s: String): String = {
DigestUtils.sha256Hex(s);
checkSumAlgorithmForBreakouts match {
case "sha1" => DigestUtils.sha1Hex(s);
case "sha256" => DigestUtils.sha256Hex(s);
case "sha384" => DigestUtils.sha384Hex(s);
case "sha512" => DigestUtils.sha512Hex(s);
case _ => DigestUtils.sha256Hex(s); // default
}
}
def calculateChecksum(apiCall: String, baseString: String, sharedSecret: String): String = {

View File

@ -6,6 +6,7 @@ import org.bigbluebutton.core.running.OutMsgRouter
import org.bigbluebutton.core2.MeetingStatus2x
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
import org.bigbluebutton.core.db.LayoutDAO
import org.bigbluebutton.core2.message.senders.{ MsgBuilder }
trait BroadcastLayoutMsgHdlr extends RightsManagementTrait {
this: LayoutApp2x =>
@ -60,5 +61,18 @@ trait BroadcastLayoutMsgHdlr extends RightsManagementTrait {
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
outGW.send(msgEvent)
if (body.pushLayout) {
val notifyEvent = MsgBuilder.buildNotifyUserInMeetingEvtMsg(
fromUserId,
liveMeeting.props.meetingProp.intId,
"info",
"user",
"app.layoutUpdate.label",
"Notification to when the presenter changes size of cams",
Vector()
)
outGW.send(notifyEvent)
}
}
}

View File

@ -15,7 +15,7 @@ trait SetPresenterInDefaultPodInternalMsgHdlr {
msg: SetPresenterInDefaultPodInternalMsg, state: MeetingState2x,
liveMeeting: LiveMeeting, bus: MessageBus
): MeetingState2x = {
// Swith presenter as default presenter pod has changed.
// Switch presenter as default presenter pod has changed.
log.info("Presenter pod change will trigger a presenter change")
SetPresenterInPodActionHandler.handleAction(state, liveMeeting, bus.outGW, "", PresentationPod.DEFAULT_PRESENTATION_POD, msg.presenterId)
}

View File

@ -367,7 +367,7 @@ public class Bezier {
* @param first Indice of first point in d.
* @param last Indice of last point in d.
* @param tHat1 Unit tangent vectors at start point.
* @param tHat2 Unit tanget vector at end point.
* @param tHat2 Unit tangent vector at end point.
* @param errorSquared User-defined errorSquared squared.
* @param bezierPath Path to which the bezier curve segments are added.
*/
@ -580,7 +580,7 @@ public class Bezier {
*
* @param Q Current fitted bezier curve.
* @param P Digitized point.
* @param u Parameter value vor P.
* @param u Parameter value for P.
*/
private static double newtonRaphsonRootFind(Point2D.Double[] Q, Point2D.Double P, double u) {
double numerator, denominator;
@ -661,7 +661,7 @@ public class Bezier {
* @param last Indice of last point in d.
* @param uPrime Parameter values for region .
* @param tHat1 Unit tangent vectors at start point.
* @param tHat2 Unit tanget vector at end point.
* @param tHat2 Unit tangent vector at end point.
* @return A cubic bezier curve consisting of 4 control points.
*/
private static Point2D.Double[] generateBezier(ArrayList<Point2D.Double> d, int first, int last, double[] uPrime, Point2D.Double tHat1, Point2D.Double tHat2) {

View File

@ -40,7 +40,7 @@ public class BezierPath extends ArrayList<BezierPath.Node>
private static final long serialVersionUID=1L;
/** Constant for having only control point C0 in effect. C0 is the point
* through whitch the curve passes. */
* through which the curve passes. */
public static final int C0_MASK = 0;
/** Constant for having control point C1 in effect (in addition
* to C0). C1 controls the curve going towards C0.

View File

@ -115,7 +115,7 @@ class RedisRecorderActor(
//case m: DeskShareNotifyViewersRTMP => handleDeskShareNotifyViewersRTMP(m)
// AudioCaptions
case m: TranscriptUpdatedEvtMsg => handleTranscriptUpdatedEvtMsg(m)
//case m: TranscriptUpdatedEvtMsg => handleTranscriptUpdatedEvtMsg(m) // temporarily disabling due to issue https://github.com/bigbluebutton/bigbluebutton/issues/19701
// Meeting
case m: RecordingStatusChangedEvtMsg => handleRecordingStatusChangedEvtMsg(m)
@ -284,7 +284,7 @@ class RedisRecorderActor(
}
private def getPresentationId(whiteboardId: String): String = {
// Need to split the whiteboard id into presenation id and page num as the old
// Need to split the whiteboard id into presentation id and page num as the old
// recording expects them
val strId = new StringOps(whiteboardId)
val ids = strId.split('/')
@ -536,6 +536,7 @@ class RedisRecorderActor(
}
*/
/* temporarily disabling due to issue https://github.com/bigbluebutton/bigbluebutton/issues/19701
private def handleTranscriptUpdatedEvtMsg(msg: TranscriptUpdatedEvtMsg) {
val ev = new TranscriptUpdatedRecordEvent()
ev.setMeetingId(msg.header.meetingId)
@ -544,6 +545,7 @@ class RedisRecorderActor(
record(msg.header.meetingId, ev.toMap.asJava)
}
*/
private def handleStartExternalVideoEvtMsg(msg: StartExternalVideoEvtMsg) {
val ev = new StartExternalVideoRecordEvent()

View File

@ -65,6 +65,7 @@ expire {
services {
bbbWebAPI = "https://192.168.23.33/bigbluebutton/api"
sharedSecret = "changeme"
checkSumAlgorithmForBreakouts = "sha256"
}
eventBus {

View File

@ -14,7 +14,7 @@ object Dependencies {
// Libraries
val pekkoVersion = "1.0.1"
val pekkoHttpVersion = "1.0.0"
val logback = "1.2.10"
val logback = "1.2.13"
// Apache Commons
val lang = "3.12.0"

View File

@ -143,7 +143,7 @@ case class SetRecordingStatusCmdMsg(header: BbbClientMsgHeader, body: SetRecordi
case class SetRecordingStatusCmdMsgBody(recording: Boolean, setBy: String)
/**
* Sent by user to start recording mark and ignore previsous marks
* Sent by user to start recording mark and ignore previous marks
*/
object RecordAndClearPreviousMarkersCmdMsg { val NAME = "RecordAndClearPreviousMarkersCmdMsg" }
case class RecordAndClearPreviousMarkersCmdMsg(header: BbbClientMsgHeader, body: RecordAndClearPreviousMarkersCmdMsgBody) extends StandardMsg

View File

@ -111,7 +111,7 @@ public class ApiParams {
public static final String RECORD_FULL_DURATION_MEDIA = "recordFullDurationMedia";
private ApiParams() {
throw new IllegalStateException("ApiParams is a utility class. Instanciation is forbidden.");
throw new IllegalStateException("ApiParams is a utility class. Instantiation is forbidden.");
}
}

View File

@ -124,7 +124,7 @@ public class RecordingServiceFileImpl implements RecordingService {
if (!doneFile.exists())
log.error("Failed to create {} file.", done);
} catch (IOException e) {
log.error("Exception occured when trying to create {} file", done);
log.error("Exception occurred when trying to create {} file", done);
}
} else {
log.error("{} file already exists.", done);
@ -141,7 +141,7 @@ public class RecordingServiceFileImpl implements RecordingService {
if (!doneFile.exists())
log.error("Failed to create {} file.", done);
} catch (IOException e) {
log.error("Exception occured when trying to create {} file.", done);
log.error("Exception occurred when trying to create {} file.", done);
}
} else {
log.error("{} file already exists.", done);
@ -158,7 +158,7 @@ public class RecordingServiceFileImpl implements RecordingService {
if (!doneFile.exists())
log.error("Failed to create " + done + " file.");
} catch (IOException e) {
log.error("Exception occured when trying to create {} file.", done);
log.error("Exception occurred when trying to create {} file.", done);
}
} else {
log.error(done + " file already exists.");

View File

@ -37,7 +37,7 @@ public class ResponseBuilder {
try {
cfg.setDirectoryForTemplateLoading(templatesLoc);
} catch (IOException e) {
log.error("Exception occured creating ResponseBuilder", e);
log.error("Exception occurred creating ResponseBuilder", e);
}
setUpConfiguration();
}
@ -97,12 +97,12 @@ public class ResponseBuilder {
return xmlText.toString();
}
public String buildErrors(ArrayList erros, String returnCode) {
public String buildErrors(ArrayList errors, String returnCode) {
StringWriter xmlText = new StringWriter();
Map<String, Object> data = new HashMap<String, Object>();
data.put("returnCode", returnCode);
data.put("errorsList", erros);
data.put("errorsList", errors);
processData(getTemplate("api-errors.ftlx"), data, xmlText);

View File

@ -41,6 +41,6 @@ public class ConversionMessageConstants {
public static final String CONVERSION_TIMEOUT_KEY = "CONVERSION_TIMEOUT";
private ConversionMessageConstants() {
throw new IllegalStateException("ConversionMessageConstants is a utility class. Instanciation is forbidden.");
throw new IllegalStateException("ConversionMessageConstants is a utility class. Instantiation is forbidden.");
}
}

View File

@ -81,7 +81,7 @@ public class ExternalProcessExecutor {
try {
if (!proc.waitFor(timeout.toMillis(), TimeUnit.MILLISECONDS)) {
log.warn("TIMEDOUT excuting: {}", String.join(" ", cmd));
log.warn("TIMEDOUT executing: {}", String.join(" ", cmd));
proc.destroy();
}
return !proc.isAlive() && proc.exitValue() == 0;

View File

@ -11,7 +11,7 @@ object Dependencies {
// Libraries
val netty = "3.2.10.Final"
val logback = "1.2.10"
val logback = "1.2.13"
// Test
val junit = "4.12"

View File

@ -157,7 +157,7 @@ public class Client
* Sends a FreeSWITCH API command to the server and blocks, waiting for an immediate response from the
* server.
* <p/>
* The outcome of the command from the server is retured in an {@link EslMessage} object.
* The outcome of the command from the server is returned in an {@link EslMessage} object.
*
* @param command API command to send
* @param arg command arguments
@ -454,7 +454,7 @@ public class Client
public void run() {
try {
/**
* Custom extra parsing to get conference Events for BigBlueButton / FreeSwitch intergration
* Custom extra parsing to get conference Events for BigBlueButton / FreeSwitch integration
*/
//FIXME: make the conference headers constants
if (event.getEventSubclass().equals("conference::maintenance")) {
@ -495,7 +495,7 @@ public class Client
listener.conferenceEventPlayFile(uniqueId, confName, confSize, event);
return;
} else if (eventFunc.equals("conf_api_sub_transfer") || eventFunc.equals("conference_api_sub_transfer")) {
//Member transfered to another conf...
//Member transferred to another conf...
listener.conferenceEventTransfer(uniqueId, confName, confSize, event);
return;
} else if (eventFunc.equals("conference_add_member") || eventFunc.equals("conference_member_add")) {

View File

@ -27,7 +27,7 @@ import org.slf4j.LoggerFactory;
/**
* a {@link Runnable} which sends the specified {@link ChannelEvent} upstream.
* Most users will not see this type at all because it is used by
* {@link Executor} implementors only
* {@link Executor} implementers only
*
* @author The Netty Project (netty-dev@lists.jboss.org)
* @author Trustin Lee (tlee@redhat.com)

View File

@ -26,7 +26,7 @@ import org.slf4j.LoggerFactory;
* the following:
* <pre>
&lt;extension&gt;
&lt;condition field="destination_number" expresssion="444"&gt;
&lt;condition field="destination_number" expression="444"&gt;
&lt;action application="socket" data="192.168.100.88:8084 async full"/&gt;
&lt;/condition&gt;
&lt;/extension&gt;

View File

@ -21,7 +21,7 @@ export default function buildRedisMessage(sessionVariables: Record<string, unkno
recording: input.recording
};
//TODO check if backend velidate it
//TODO check if backend validates it
// const recordObject = await RecordMeetings.findOneAsync({ meetingId });
//

View File

@ -0,0 +1,24 @@
import { RedisMessage } from '../types';
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
const eventName = `SendCursorPositionPubMsg`;
const routing = {
meetingId: sessionVariables['x-hasura-meetingid'] as String,
userId: sessionVariables['x-hasura-userid'] as String
};
const header = {
name: eventName,
meetingId: routing.meetingId,
userId: routing.userId
};
const body = {
whiteboardId: input.whiteboardId,
xPercent: input.xPercent,
yPercent: input.yPercent,
};
return { eventName, routing, header, body };
}

View File

@ -3,7 +3,7 @@ BBB_GRAPHQL_MIDDLEWARE_LISTEN_PORT=8378
BBB_GRAPHQL_MIDDLEWARE_REDIS_ADDRESS=127.0.0.1:6379
BBB_GRAPHQL_MIDDLEWARE_REDIS_PASSWORD=
BBB_GRAPHQL_MIDDLEWARE_HASURA_WS=ws://127.0.0.1:8080/v1/graphql
BBB_GRAPHQL_MIDDLEWARE_RATE_LIMIT_IN_MS=50
BBB_GRAPHQL_MIDDLEWARE_MAX_CONN_PER_SECOND=10
# If you are running a cluster proxy setup, you need to configure the Origin of
# the frontend. See https://docs.bigbluebutton.org/administration/cluster-proxy

View File

@ -2,12 +2,12 @@ package main
import (
"context"
"errors"
"fmt"
"github.com/iMDT/bbb-graphql-middleware/internal/common"
"github.com/iMDT/bbb-graphql-middleware/internal/msgpatch"
"github.com/iMDT/bbb-graphql-middleware/internal/websrv"
log "github.com/sirupsen/logrus"
"golang.org/x/time/rate"
"net/http"
"os"
"strconv"
@ -53,20 +53,23 @@ func main() {
}
//Define new Connections Rate Limit
rateLimitInMs := 50
if envRateLimitInMs := os.Getenv("BBB_GRAPHQL_MIDDLEWARE_RATE_LIMIT_IN_MS"); envRateLimitInMs != "" {
if envRateLimitInMsAsInt, err := strconv.Atoi(envRateLimitInMs); err == nil {
rateLimitInMs = envRateLimitInMsAsInt
maxConnPerSecond := 10
if envMaxConnPerSecond := os.Getenv("BBB_GRAPHQL_MIDDLEWARE_MAX_CONN_PER_SECOND"); envMaxConnPerSecond != "" {
if envMaxConnPerSecondAsInt, err := strconv.Atoi(envMaxConnPerSecond); err == nil {
maxConnPerSecond = envMaxConnPerSecondAsInt
}
}
limiterInterval := rate.NewLimiter(rate.Every(time.Duration(rateLimitInMs)*time.Millisecond), 1)
rateLimiter := common.NewCustomRateLimiter(maxConnPerSecond)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
ctx, cancel := context.WithTimeout(r.Context(), 120*time.Second)
defer cancel()
if err := limiterInterval.Wait(ctx); err != nil {
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
if err := rateLimiter.Wait(ctx); err != nil {
if !errors.Is(err, context.Canceled) {
http.Error(w, "Request cancelled or rate limit exceeded", http.StatusTooManyRequests)
}
return
}

View File

@ -0,0 +1,64 @@
package common
import (
"context"
"time"
)
type CustomRateLimiter struct {
tokens chan struct{}
requestQueue chan context.Context
}
func NewCustomRateLimiter(requestsPerSecond int) *CustomRateLimiter {
rl := &CustomRateLimiter{
tokens: make(chan struct{}, requestsPerSecond),
requestQueue: make(chan context.Context, 20000), // Adjust the size accordingly
}
go rl.refillTokens(requestsPerSecond)
go rl.processQueue()
return rl
}
func (rl *CustomRateLimiter) refillTokens(requestsPerSecond int) {
ticker := time.NewTicker(time.Second / time.Duration(requestsPerSecond))
for {
select {
case <-ticker.C:
// Try to add a token, skip if full
select {
case rl.tokens <- struct{}{}:
default:
}
}
}
}
func (rl *CustomRateLimiter) processQueue() {
for ctx := range rl.requestQueue {
select {
case <-rl.tokens:
if ctx.Err() == nil {
// Token acquired and context not cancelled, proceed
// Simulate processing by calling a dummy function
// processRequest() or similar
}
case <-ctx.Done():
// Context cancelled, skip
}
}
}
func (rl *CustomRateLimiter) Wait(ctx context.Context) error {
rl.requestQueue <- ctx
select {
case <-ctx.Done():
// Request cancelled
return ctx.Err()
case <-rl.tokens:
// Acquired token, proceed
return nil
}
}

View File

@ -37,6 +37,13 @@ func (s *SafeChannel) ReceiveChannel() <-chan interface{} {
return s.ch
}
func (s *SafeChannel) Closed() bool {
s.mux.Lock()
defer s.mux.Unlock()
return s.closed
}
func (s *SafeChannel) Close() {
s.mux.Lock()
defer s.mux.Unlock()
@ -47,6 +54,10 @@ func (s *SafeChannel) Close() {
}
}
func (s *SafeChannel) Frozen() bool {
return s.freezeFlag
}
func (s *SafeChannel) FreezeChannel() {
if !s.freezeFlag {
s.mux.Lock()

View File

@ -43,10 +43,10 @@ type BrowserConnection struct {
}
type HasuraConnection struct {
Id string // hasura connection id
Browserconn *BrowserConnection // browser connection that originated this hasura connection
Websocket *websocket.Conn // websocket used to connect to hasura
Context context.Context // hasura connection context (child of browser connection context)
ContextCancelFunc context.CancelFunc // function to cancel the hasura context (and so, the hasura connection)
MsgReceivingActiveChan *SafeChannel // indicate that it's waiting for the return of mutations before closing connection
Id string // hasura connection id
BrowserConn *BrowserConnection // browser connection that originated this hasura connection
Websocket *websocket.Conn // websocket used to connect to hasura
Context context.Context // hasura connection context (child of browser connection context)
ContextCancelFunc context.CancelFunc // function to cancel the hasura context (and so, the hasura connection)
FreezeMsgFromBrowserChan *SafeChannel // indicate that it's waiting for the return of mutations before closing connection
}

View File

@ -60,11 +60,11 @@ func HasuraClient(browserConnection *common.BrowserConnection, cookies []*http.C
defer hasuraConnectionContextCancel()
var thisConnection = common.HasuraConnection{
Id: hasuraConnectionId,
Browserconn: browserConnection,
Context: hasuraConnectionContext,
ContextCancelFunc: hasuraConnectionContextCancel,
MsgReceivingActiveChan: common.NewSafeChannel(1),
Id: hasuraConnectionId,
BrowserConn: browserConnection,
Context: hasuraConnectionContext,
ContextCancelFunc: hasuraConnectionContextCancel,
FreezeMsgFromBrowserChan: common.NewSafeChannel(1),
}
browserConnection.HasuraConnection = &thisConnection

View File

@ -15,7 +15,7 @@ import (
// HasuraConnectionReader consumes messages from Hasura connection and add send to the browser channel
func HasuraConnectionReader(hc *common.HasuraConnection, fromHasuraToBrowserChannel *common.SafeChannel, fromBrowserToHasuraChannel *common.SafeChannel, wg *sync.WaitGroup) {
log := log.WithField("_routine", "HasuraConnectionReader").WithField("browserConnectionId", hc.Browserconn.Id).WithField("hasuraConnectionId", hc.Id)
log := log.WithField("_routine", "HasuraConnectionReader").WithField("browserConnectionId", hc.BrowserConn.Id).WithField("hasuraConnectionId", hc.Id)
defer log.Debugf("finished")
log.Debugf("starting")
@ -28,9 +28,9 @@ func HasuraConnectionReader(hc *common.HasuraConnection, fromHasuraToBrowserChan
err := wsjson.Read(hc.Context, hc.Websocket, &message)
if err != nil {
if errors.Is(err, context.Canceled) {
log.Debugf("Closing ws connection as Context was cancelled!")
log.Debugf("Closing Hasura ws connection as Context was cancelled!")
} else {
log.Errorf("Error reading message from Hasura: %v", err)
log.Debugf("Error reading message from Hasura: %v", err)
}
return
}
@ -50,11 +50,11 @@ func handleMessageReceivedFromHasura(hc *common.HasuraConnection, fromHasuraToBr
//Check if subscription is still active!
if queryId != "" {
hc.Browserconn.ActiveSubscriptionsMutex.RLock()
subscription, ok := hc.Browserconn.ActiveSubscriptions[queryId]
hc.Browserconn.ActiveSubscriptionsMutex.RUnlock()
hc.BrowserConn.ActiveSubscriptionsMutex.RLock()
subscription, ok := hc.BrowserConn.ActiveSubscriptions[queryId]
hc.BrowserConn.ActiveSubscriptionsMutex.RUnlock()
if !ok {
log.Debugf("Subscription with Id %s doesn't exist anymore, skiping response.", queryId)
log.Debugf("Subscription with Id %s doesn't exist anymore, skipping response.", queryId)
return
}
@ -104,13 +104,13 @@ func handleSubscriptionMessage(hc *common.HasuraConnection, messageMap map[strin
//Store LastReceivedData Checksum
subscription.LastReceivedDataChecksum = dataChecksum
hc.Browserconn.ActiveSubscriptionsMutex.Lock()
hc.Browserconn.ActiveSubscriptions[queryId] = subscription
hc.Browserconn.ActiveSubscriptionsMutex.Unlock()
hc.BrowserConn.ActiveSubscriptionsMutex.Lock()
hc.BrowserConn.ActiveSubscriptions[queryId] = subscription
hc.BrowserConn.ActiveSubscriptionsMutex.Unlock()
//Apply msg patch when it supports it
if subscription.JsonPatchSupported {
msgpatch.PatchMessage(&messageMap, queryId, dataKey, dataAsJson, hc.Browserconn)
msgpatch.PatchMessage(&messageMap, queryId, dataKey, dataAsJson, hc.BrowserConn)
}
}
}
@ -126,17 +126,19 @@ func handleStreamingMessage(hc *common.HasuraConnection, messageMap map[string]i
if lastCursor != nil && subscription.StreamCursorCurrValue != lastCursor {
subscription.StreamCursorCurrValue = lastCursor
hc.Browserconn.ActiveSubscriptionsMutex.Lock()
hc.Browserconn.ActiveSubscriptions[queryId] = subscription
hc.Browserconn.ActiveSubscriptionsMutex.Unlock()
hc.BrowserConn.ActiveSubscriptionsMutex.Lock()
hc.BrowserConn.ActiveSubscriptions[queryId] = subscription
hc.BrowserConn.ActiveSubscriptionsMutex.Unlock()
}
}
func handleCompleteMessage(hc *common.HasuraConnection, queryId string) {
hc.Browserconn.ActiveSubscriptionsMutex.Lock()
delete(hc.Browserconn.ActiveSubscriptions, queryId)
hc.Browserconn.ActiveSubscriptionsMutex.Unlock()
log.Debugf("Subscription with Id %s finished by Hasura.", queryId)
hc.BrowserConn.ActiveSubscriptionsMutex.Lock()
queryType := hc.BrowserConn.ActiveSubscriptions[queryId].Type
operationName := hc.BrowserConn.ActiveSubscriptions[queryId].OperationName
delete(hc.BrowserConn.ActiveSubscriptions, queryId)
hc.BrowserConn.ActiveSubscriptionsMutex.Unlock()
log.Debugf("%s (%s) with Id %s finished by Hasura.", queryType, operationName, queryId)
}
func handleConnectionAckMessage(hc *common.HasuraConnection, messageMap map[string]interface{}, fromHasuraToBrowserChannel *common.SafeChannel, fromBrowserToHasuraChannel *common.SafeChannel) {
@ -145,9 +147,9 @@ func handleConnectionAckMessage(hc *common.HasuraConnection, messageMap map[stri
fromBrowserToHasuraChannel.UnfreezeChannel()
//Avoid to send `connection_ack` to the browser when it's a reconnection
if hc.Browserconn.ConnAckSentToBrowser == false {
if hc.BrowserConn.ConnAckSentToBrowser == false {
fromHasuraToBrowserChannel.Send(messageMap)
hc.Browserconn.ConnAckSentToBrowser = true
hc.BrowserConn.ConnAckSentToBrowser = true
}
go retransmiter.RetransmitSubscriptionStartMessages(hc, fromBrowserToHasuraChannel)

View File

@ -14,7 +14,7 @@ import (
func HasuraConnectionWriter(hc *common.HasuraConnection, fromBrowserToHasuraChannel *common.SafeChannel, wg *sync.WaitGroup, initMessage map[string]interface{}) {
log := log.WithField("_routine", "HasuraConnectionWriter")
browserConnection := hc.Browserconn
browserConnection := hc.BrowserConn
log = log.WithField("browserConnectionId", browserConnection.Id).WithField("hasuraConnectionId", hc.Id)
@ -38,10 +38,12 @@ RangeLoop:
select {
case <-hc.Context.Done():
break RangeLoop
case <-hc.MsgReceivingActiveChan.ReceiveChannel():
log.Debugf("freezing channel fromBrowserToHasuraChannel")
//Freeze channel once it's about to close Hasura connection
fromBrowserToHasuraChannel.FreezeChannel()
case <-hc.FreezeMsgFromBrowserChan.ReceiveChannel():
if !fromBrowserToHasuraChannel.Frozen() {
log.Debug("freezing channel fromBrowserToHasuraChannel")
//Freeze channel once it's about to close Hasura connection
fromBrowserToHasuraChannel.FreezeChannel()
}
case fromBrowserMessage := <-fromBrowserToHasuraChannel.ReceiveChannel():
{
if fromBrowserMessage == nil {

View File

@ -6,10 +6,10 @@ import (
)
func RetransmitSubscriptionStartMessages(hc *common.HasuraConnection, fromBrowserToHasuraChannel *common.SafeChannel) {
log := log.WithField("_routine", "RetransmitSubscriptionStartMessages").WithField("browserConnectionId", hc.Browserconn.Id).WithField("hasuraConnectionId", hc.Id)
log := log.WithField("_routine", "RetransmitSubscriptionStartMessages").WithField("browserConnectionId", hc.BrowserConn.Id).WithField("hasuraConnectionId", hc.Id)
hc.Browserconn.ActiveSubscriptionsMutex.RLock()
for _, subscription := range hc.Browserconn.ActiveSubscriptions {
hc.BrowserConn.ActiveSubscriptionsMutex.RLock()
for _, subscription := range hc.BrowserConn.ActiveSubscriptions {
//Not retransmitting Mutations
if subscription.Type == common.Mutation {
@ -27,5 +27,5 @@ func RetransmitSubscriptionStartMessages(hc *common.HasuraConnection, fromBrowse
}
}
}
hc.Browserconn.ActiveSubscriptionsMutex.RUnlock()
hc.BrowserConn.ActiveSubscriptionsMutex.RUnlock()
}

View File

@ -47,11 +47,11 @@ func ConnectionHandler(w http.ResponseWriter, r *http.Request) {
acceptOptions.OriginPatterns = append(acceptOptions.OriginPatterns, bbbOrigin)
}
c, err := websocket.Accept(w, r, &acceptOptions)
browserWsConn, err := websocket.Accept(w, r, &acceptOptions)
if err != nil {
log.Errorf("error: %v", err)
}
defer c.Close(websocket.StatusInternalError, "the sky is falling")
defer browserWsConn.Close(websocket.StatusInternalError, "the sky is falling")
var thisConnection = common.BrowserConnection{
Id: browserConnectionId,
@ -67,10 +67,13 @@ func ConnectionHandler(w http.ResponseWriter, r *http.Request) {
defer func() {
msgpatch.RemoveConnCacheDir(browserConnectionId)
BrowserConnectionsMutex.Lock()
sessionTokenRemoved := BrowserConnections[browserConnectionId].SessionToken
delete(BrowserConnections, browserConnectionId)
_, bcExists := BrowserConnections[browserConnectionId]
if bcExists {
sessionTokenRemoved := BrowserConnections[browserConnectionId].SessionToken
delete(BrowserConnections, browserConnectionId)
go SendUserGraphqlConnectionClosedSysMsg(sessionTokenRemoved, browserConnectionId)
}
BrowserConnectionsMutex.Unlock()
go SendUserGraphqlConnectionClosedSysMsg(sessionTokenRemoved, browserConnectionId)
log.Infof("connection removed")
}()
@ -116,14 +119,16 @@ func ConnectionHandler(w http.ResponseWriter, r *http.Request) {
wgReader.Add(1)
// Reads from browser connection, writes into fromBrowserToHasuraChannel and fromBrowserToHasuraConnectionEstablishingChannel
go reader.BrowserConnectionReader(browserConnectionId, browserConnectionContext, c, fromBrowserToHasuraChannel, fromBrowserToHasuraConnectionEstablishingChannel, []*sync.WaitGroup{&wgAll, &wgReader})
go reader.BrowserConnectionReader(browserConnectionId, browserConnectionContext, browserConnectionContextCancel, browserWsConn, fromBrowserToHasuraChannel, fromBrowserToHasuraConnectionEstablishingChannel, []*sync.WaitGroup{&wgAll, &wgReader})
go func() {
wgReader.Wait()
log.Debug("BrowserConnectionReader finished, closing Write Channel")
fromHasuraToBrowserChannel.Close()
thisConnection.Disconnected = true
}()
// Reads from fromHasuraToBrowserChannel, writes to browser connection
go writer.BrowserConnectionWriter(browserConnectionId, browserConnectionContext, c, fromHasuraToBrowserChannel, &wgAll)
go writer.BrowserConnectionWriter(browserConnectionId, browserConnectionContext, browserWsConn, fromHasuraToBrowserChannel, &wgAll)
go ConnectionInitHandler(browserConnectionId, browserConnectionContext, fromBrowserToHasuraConnectionEstablishingChannel, &wgAll)
@ -133,11 +138,75 @@ func ConnectionHandler(w http.ResponseWriter, r *http.Request) {
func InvalidateSessionTokenConnections(sessionTokenToInvalidate string) {
BrowserConnectionsMutex.RLock()
connectionsToProcess := make([]*common.BrowserConnection, 0)
for _, browserConnection := range BrowserConnections {
if browserConnection.SessionToken == sessionTokenToInvalidate {
connectionsToProcess = append(connectionsToProcess, browserConnection)
}
}
BrowserConnectionsMutex.RUnlock()
var wg sync.WaitGroup
for _, browserConnection := range connectionsToProcess {
wg.Add(1)
go func(bc *common.BrowserConnection) {
defer wg.Done()
invalidateBrowserConnection(bc, sessionTokenToInvalidate)
}(browserConnection)
}
wg.Wait()
}
func invalidateBrowserConnection(bc *common.BrowserConnection, sessionToken string) {
if bc.HasuraConnection == nil {
return // If there's no Hasura connection, there's nothing to invalidate.
}
hasuraConnectionId := bc.HasuraConnection.Id
// Send message to stop receiving new messages from the browser.
bc.HasuraConnection.FreezeMsgFromBrowserChan.Send(true)
// Wait until there are no active mutations.
for iterationCount := 0; iterationCount < 20; iterationCount++ {
activeMutationFound := false
bc.ActiveSubscriptionsMutex.RLock()
for _, subscription := range bc.ActiveSubscriptions {
if subscription.Type == common.Mutation {
activeMutationFound = true
break
}
}
bc.ActiveSubscriptionsMutex.RUnlock()
if !activeMutationFound {
break // Exit the loop if no active mutations are found.
}
time.Sleep(100 * time.Millisecond) // Wait a bit before checking again.
}
log.Debugf("Processing invalidate request for sessionToken %v (hasura connection %v)", sessionToken, hasuraConnectionId)
// Cancel the Hasura connection context to clean up resources.
if bc.HasuraConnection != nil && bc.HasuraConnection.ContextCancelFunc != nil {
bc.HasuraConnection.ContextCancelFunc()
}
log.Debugf("Processed invalidate request for sessionToken %v (hasura connection %v)", sessionToken, hasuraConnectionId)
// Send a reconnection confirmation message
go SendUserGraphqlReconnectionForcedEvtMsg(sessionToken)
}
func InvalidateSessionTokenConnectionsB(sessionTokenToInvalidate string) {
BrowserConnectionsMutex.RLock()
for _, browserConnection := range BrowserConnections {
hasuraConnectionId := browserConnection.HasuraConnection.Id
if browserConnection.SessionToken == sessionTokenToInvalidate {
if browserConnection.HasuraConnection != nil {
//Close chan to force stop receiving new messages from the browser
browserConnection.HasuraConnection.MsgReceivingActiveChan.Close()
//Send message to force stop receiving new messages from the browser
browserConnection.HasuraConnection.FreezeMsgFromBrowserChan.Send(true)
// Wait until there are no active mutations
for iterationCount := 0; iterationCount < 20; iterationCount++ {
@ -157,9 +226,11 @@ func InvalidateSessionTokenConnections(sessionTokenToInvalidate string) {
time.Sleep(100 * time.Millisecond)
}
log.Debugf("Processing invalidate request for sessionToken %v (hasura connection %v)", sessionTokenToInvalidate, browserConnection.HasuraConnection.Id)
browserConnection.HasuraConnection.ContextCancelFunc()
log.Debugf("Processed invalidate request for sessionToken %v (hasura connection %v)", sessionTokenToInvalidate, browserConnection.HasuraConnection.Id)
log.Debugf("Processing invalidate request for sessionToken %v (hasura connection %v)", sessionTokenToInvalidate, hasuraConnectionId)
if browserConnection.HasuraConnection != nil {
browserConnection.HasuraConnection.ContextCancelFunc()
}
log.Debugf("Processed invalidate request for sessionToken %v (hasura connection %v)", sessionTokenToInvalidate, hasuraConnectionId)
go SendUserGraphqlReconnectionForcedEvtMsg(browserConnection.SessionToken)
}

View File

@ -2,6 +2,7 @@ package reader
import (
"context"
"errors"
"github.com/iMDT/bbb-graphql-middleware/internal/common"
log "github.com/sirupsen/logrus"
"nhooyr.io/websocket"
@ -10,7 +11,7 @@ import (
"time"
)
func BrowserConnectionReader(browserConnectionId string, ctx context.Context, c *websocket.Conn, fromBrowserToHasuraChannel *common.SafeChannel, fromBrowserToHasuraConnectionEstablishingChannel *common.SafeChannel, waitGroups []*sync.WaitGroup) {
func BrowserConnectionReader(browserConnectionId string, ctx context.Context, ctxCancel context.CancelFunc, browserWsConn *websocket.Conn, fromBrowserToHasuraChannel *common.SafeChannel, fromBrowserToHasuraConnectionEstablishingChannel *common.SafeChannel, waitGroups []*sync.WaitGroup) {
log := log.WithField("_routine", "BrowserConnectionReader").WithField("browserConnectionId", browserConnectionId)
defer log.Debugf("finished")
log.Debugf("starting")
@ -29,14 +30,17 @@ func BrowserConnectionReader(browserConnectionId string, ctx context.Context, c
time.Sleep(100 * time.Millisecond)
}()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
defer ctxCancel()
for {
var v interface{}
err := wsjson.Read(ctx, c, &v)
err := wsjson.Read(ctx, browserWsConn, &v)
if err != nil {
log.Debugf("Browser is disconnected, skiping reading of ws message: %v", err)
if errors.Is(err, context.Canceled) {
log.Debugf("Closing Browser ws connection as Context was cancelled!")
} else {
log.Debugf("Hasura is disconnected, skipping reading of ws message: %v", err)
}
return
}

View File

@ -55,7 +55,8 @@ func StartRedisListener() {
messageCoreAsMap := messageAsMap["core"].(map[string]interface{})
messageBodyAsMap := messageCoreAsMap["body"].(map[string]interface{})
sessionTokenToInvalidate := messageBodyAsMap["sessionToken"]
log.Debugf("Received invalidate request for sessionToken %v", sessionTokenToInvalidate)
reason := messageBodyAsMap["reason"]
log.Debugf("Received invalidate request for sessionToken %v (%v)", sessionTokenToInvalidate, reason)
//Not being used yet
go InvalidateSessionTokenConnections(sessionTokenToInvalidate.(string))

View File

@ -4,13 +4,12 @@ import (
"context"
"github.com/iMDT/bbb-graphql-middleware/internal/common"
log "github.com/sirupsen/logrus"
"sync"
"nhooyr.io/websocket"
"nhooyr.io/websocket/wsjson"
"sync"
)
func BrowserConnectionWriter(browserConnectionId string, ctx context.Context, c *websocket.Conn, fromHasuraToBrowserChannel *common.SafeChannel, wg *sync.WaitGroup) {
func BrowserConnectionWriter(browserConnectionId string, ctx context.Context, browserWsConn *websocket.Conn, fromHasuraToBrowserChannel *common.SafeChannel, wg *sync.WaitGroup) {
log := log.WithField("_routine", "BrowserConnectionWriter").WithField("browserConnectionId", browserConnectionId)
defer log.Debugf("finished")
log.Debugf("starting")
@ -20,15 +19,23 @@ RangeLoop:
for {
select {
case <-ctx.Done():
log.Debug("Browser context cancelled.")
break RangeLoop
case toBrowserMessage := <-fromHasuraToBrowserChannel.ReceiveChannel():
{
if toBrowserMessage == nil {
if fromHasuraToBrowserChannel.Closed() {
break RangeLoop
}
continue
}
var toBrowserMessageAsMap = toBrowserMessage.(map[string]interface{})
log.Tracef("sending to browser: %v", toBrowserMessage)
err := wsjson.Write(ctx, c, toBrowserMessage)
err := wsjson.Write(ctx, browserWsConn, toBrowserMessage)
if err != nil {
log.Debugf("Browser is disconnected, skiping writing of ws message: %v", err)
log.Debugf("Browser is disconnected, skipping writing of ws message: %v", err)
return
}
@ -36,7 +43,7 @@ RangeLoop:
// Authentication hook unauthorized this request
if toBrowserMessageAsMap["type"] == "connection_error" {
var payloadAsString = toBrowserMessageAsMap["payload"].(string)
c.Close(websocket.StatusInternalError, payloadAsString)
browserWsConn.Close(websocket.StatusInternalError, payloadAsString)
}
}
}

View File

@ -1,2 +1,7 @@
#!/bin/bash
nodemon --exec go run cmd/bbb-graphql-middleware/main.go --signal SIGTERM
sudo systemctl stop bbb-graphql-middleware
set -a # Automatically export all variables
source ./bbb-graphql-middleware-config.env
set +a # Stop automatically exporting
go run cmd/bbb-graphql-middleware/main.go --signal SIGTERM

View File

@ -250,7 +250,7 @@ CREATE TABLE "user" (
"registeredOn" bigint,
"excludeFromDashboard" bool,
"enforceLayout" varchar(50),
--columns of user state bellow
--columns of user state below
"raiseHand" bool default false,
"raiseHandTime" timestamp with time zone,
"away" bool default false,

View File

@ -287,6 +287,14 @@ type Mutation {
): Boolean
}
type Mutation {
presentationPublishCursor(
whiteboardId: String!
xPercent: Float!
yPercent: Float!
): Boolean
}
type Mutation {
presentationRemove(
presentationId: String!

View File

@ -252,6 +252,12 @@ actions:
permissions:
- role: bbb_client
comment: presentationExport
- name: presentationPublishCursor
definition:
kind: synchronous
handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}'
permissions:
- role: bbb_client
- name: presentationRemove
definition:
kind: synchronous

View File

@ -16,7 +16,7 @@ HOST=127.0.0.1
# Set this to "-k" to allow it to work in a test environment, ie with a self signed
# certificate
UNSECURE=
INSECURE=
# This script receives three params
# Param 1: Input office file path (e.g. "/tmp/test.odt")
@ -46,6 +46,6 @@ timeoutSecs="${timeoutSecs:0:3}"
# The timeout is important.
timeout $(printf %03d $timeoutSecs)s curl $UNSECURE -F "data=@${source}" https://$HOST:9980/cool/convert-to/$convertTo > "${dest}"
timeout $(printf %03d $timeoutSecs)s curl $INSECURE -F "data=@${source}" https://$HOST:9980/cool/convert-to/$convertTo > "${dest}"
exit 0

View File

@ -18,7 +18,7 @@ conf.orig dir is what was installed by freeswitch by default
NOTE: you must double check this config if you intend to have
the freeswitch server on a public facing interface.
It defaults to localhost for the event socket inteface.
It defaults to localhost for the event socket interface.
I run my server in a test environment with
/usr/local/freeswitch/bin/freeswitch -hp -nc

View File

@ -3,7 +3,7 @@
NOTICE:
This context is usually accessed via authenticated callers on the sip profile on port 5060
or transfered callers from the public context which arrived via the sip profile on port 5080.
or transferred callers from the public context which arrived via the sip profile on port 5080.
Authenticated users will use the user_context variable on the user to determine what context
they can access. You can also add a user in the directory with the cidr= attribute acl.conf.xml

View File

@ -7,7 +7,7 @@
<params>
<!-- omit password for authless registration -->
<param name="password" value="secret"/>
<!-- What this user is allowed to acces -->
<!-- What this user is allowed to access -->
<!--<param name="http-allowed-api" value="jsapi,voicemail,status"/> -->
</params>
<variables>

View File

@ -327,7 +327,7 @@
<!--<param name="rtcp-audio-interval-msec" value="5000"/>-->
<!--<param name="rtcp-video-interval-msec" value="5000"/>-->
<!--force suscription expires to a lower value than requested-->
<!--force subscription expires to a lower value than requested-->
<!--<param name="force-subscription-expires" value="60"/>-->
<!-- add a random deviation to the expires value of the 202 Accepted -->

View File

@ -166,7 +166,7 @@
rtp_secure_media_suites
____________________________________________________________________________
Optionaly you can use rtp_secure_media_suites to dictate the suite list
Optionally you can use rtp_secure_media_suites to dictate the suite list
and only use rtp_secure_media=[optional|mandatory|false|true] without having
to dictate the suite list with the rtp_secure_media* variables.
-->
@ -175,7 +175,7 @@
codecname[@8000h|16000h|32000h[@XXi]]
XX is the frame size must be multples allowed for the codec
XX is the frame size must be multiples allowed for the codec
FreeSWITCH can support 10-120ms on some codecs.
We do not support exceeding the MTU of the RTP packet.
@ -414,7 +414,7 @@
openssl ciphers -v 'ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH'
Will show you what is available in your verion of openssl.
Will show you what is available in your version of openssl.
-->
<X-PRE-PROCESS cmd="set" data="sip_tls_ciphers=ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH"/>
@ -431,7 +431,7 @@
<X-PRE-PROCESS cmd="set" data="external_ssl_enable=false"/>
<!-- Video Settings -->
<!-- Setting the max bandwdith -->
<!-- Setting the max bandwidth -->
<X-PRE-PROCESS cmd="set" data="rtp_video_max_bandwidth_in=1mb"/>
<X-PRE-PROCESS cmd="set" data="rtp_video_max_bandwidth_out=1mb"/>

View File

@ -1 +1 @@
git clone --branch v3.0.0-beta.4 --depth 1 https://github.com/bigbluebutton/bbb-webhooks bbb-webhooks
git clone --branch v3.1.0 --depth 1 https://github.com/bigbluebutton/bbb-webhooks bbb-webhooks

View File

@ -1 +1 @@
git clone --branch v0.6.0 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-recorder bbb-webrtc-recorder
git clone --branch v0.7.0 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-recorder bbb-webrtc-recorder

View File

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

View File

@ -308,7 +308,7 @@ check_and_backup () {
fi
}
# 3 paramenter: the file, the variable name, the new value
# 3 parameter: the file, the variable name, the new value
change_var_value () {
check_and_backup $1
sed -i "s<^[[:blank:]#]*\(${2}\).*<\1=${3}<" $1
@ -1489,7 +1489,7 @@ if [ -n "$HOST" ]; then
echo "bigbluebutton.web.serverURL=$PROTOCOL://$HOST" >> "$BBB_WEB_ETC_CONFIG"
fi
# Populate /etc/bigbluebutton/bbb-web.properites with the shared secret
# Populate /etc/bigbluebutton/bbb-web.properties 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

@ -23,7 +23,7 @@
# Daniel Petri Rocha <danielpetrirocha@gmail.com>
#
# Changelog:
# 2011-08-18 FFD Inital Version
# 2011-08-18 FFD Initial Version
# 2011-11-20 FFD Added more checks for processing of recording
# 2012-01-04 GUG Add option to check for errors
# 2012-02-27 GUG Add option to delete one meeting and recording

View File

@ -122,7 +122,7 @@ remove_raw_of_published_recordings(){
remove_raw_of_published_recordings
#
# Remove untagged and unamed docker images, cleanning /var/lib/docker/overlay2
# Remove untagged and unnamed docker images, cleaning /var/lib/docker/overlay2
#
docker image prune -f

View File

@ -10,6 +10,7 @@ import LoadingScreenHOC from '/imports/ui/components/common/loading-screen/loadi
import IntlLoaderContainer from '/imports/startup/client/intlLoader';
import LocatedErrorBoundary from '/imports/ui/components/common/error-boundary/located-error-boundary/component';
import StartupDataFetch from '/imports/ui/components/connection-manager/startup-data-fetch/component';
import UserGrapQlMiniMongoAdapter from '/imports/ui/components/components-data/userGrapQlMiniMongoAdapter/component';
import VoiceUserGrapQlMiniMongoAdapter from '/imports/ui/components/components-data/voiceUserGraphQlMiniMongoAdapter/component';
const Main: React.FC = () => {
@ -23,6 +24,7 @@ const Main: React.FC = () => {
<ConnectionManager>
<PresenceManager>
<SettingsLoader />
<UserGrapQlMiniMongoAdapter />
<VoiceUserGrapQlMiniMongoAdapter />
</PresenceManager>
</ConnectionManager>

View File

@ -97,7 +97,7 @@ const doGUM = async (constraints, retryOnFailure = false) => {
const stream = await navigator.mediaDevices.getUserMedia(constraints);
return stream;
} catch (error) {
// This is probably a deviceId mistmatch. Retry with base constraints
// This is probably a deviceId mismatch. Retry with base constraints
// without an exact deviceId.
if (error.name === 'OverconstrainedError' && retryOnFailure) {
logger.warn({

View File

@ -225,7 +225,7 @@ export default class SFUAudioBridge extends BaseAudioBridge {
}
}
// Already tried reconnecting once OR the user handn't succesfully
// Already tried reconnecting once OR the user handn't successfully
// connected firsthand and retrying isn't an option. Finish the session
// and reject with the error
logger.error({

View File

@ -272,7 +272,7 @@ class SIPSession {
*
* sessionSupportRTPPayloadDtmf
* tells if browser support RFC4733 DTMF.
* Safari 13 doens't support it yet
* Safari 13 doesn't support it yet
*/
sessionSupportRTPPayloadDtmf(session) {
try {
@ -383,7 +383,7 @@ class SIPSession {
if (this.preloadedInputStream && this.preloadedInputStream.active) {
return Promise.resolve(this.preloadedInputStream);
}
// The rest of this mimicks the default factory behavior.
// The rest of this mimics the default factory behavior.
if (!constraints.audio && !constraints.video) {
return Promise.resolve(new MediaStream());
}
@ -498,7 +498,7 @@ class SIPSession {
extraInfo: {
callerIdName: this.user.callerIdName,
},
}, 'User agent succesfully reconnected');
}, 'User agent successfully reconnected');
}).catch(() => {
if (userAgentConnected) {
error = 1001;
@ -531,7 +531,7 @@ class SIPSession {
extraInfo: {
callerIdName: this.user.callerIdName,
},
}, 'User agent succesfully connected');
}, 'User agent successfully connected');
window.addEventListener('beforeunload', this.onBeforeUnload.bind(this));
@ -567,7 +567,7 @@ class SIPSession {
extraInfo: {
callerIdName: this.user.callerIdName,
},
}, 'User agent succesfully reconnected');
}, 'User agent successfully reconnected');
resolve();
}).catch(() => {
@ -579,7 +579,7 @@ class SIPSession {
callerIdName: this.user.callerIdName,
},
}, 'User agent failed to reconnect after'
+ ` ${USER_AGENT_RECONNECTION_ATTEMPTS} attemps`);
+ ` ${USER_AGENT_RECONNECTION_ATTEMPTS} attempts`);
this.callback({
status: this.baseCallStates.failed,
@ -1013,7 +1013,7 @@ class SIPSession {
}
// if session hasn't even started, we let audio-modal to handle
// any possile errors
// any possible errors
if (!this._currentSessionState) return false;

View File

@ -1 +0,0 @@
import './methods';

View File

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

View File

@ -1,10 +0,0 @@
import RedisPubSub from '/imports/startup/server/redis';
import { Meteor } from 'meteor/meteor';
export default function publishCursorUpdate(meetingId, requesterUserId, payload) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'SendCursorPositionPubMsg';
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
}

View File

@ -66,6 +66,7 @@ const currentParameters = [
'bbb_hide_nav_bar',
'bbb_change_layout',
'bbb_direct_leave_button',
'bbb_default_layout',
];
function valueParser(val) {

View File

@ -27,6 +27,7 @@ export interface Public {
}
export interface App {
instanceId: string
mobileFontSize: string
desktopFontSize: string
audioChatNotification: boolean

View File

@ -1,7 +1,5 @@
export interface Cameras {
streamId: string;
meetingId: string;
userId: string;
}
export interface PresPagesWritable {
@ -42,16 +40,36 @@ export interface Voice {
startTime: number;
}
export interface CustomParameter {
parameter: string;
value: string;
}
export interface Reaction {
reactionEmoji: string;
}
export interface UserClientSettings {
userClientSettingsJson: string;
}
export interface User {
authToken: string;
userId: string;
extId: string;
name: string;
nameSortable: string;
banned: boolean;
isModerator: boolean;
clientType: string;
disconnected: boolean;
isOnline: boolean;
isRunningEchoTest: boolean;
echoTestRunningAt: number;
ejectReason: string;
ejectReasonCode: string;
ejected: boolean;
enforceLayout: boolean;
role: string;
color: string;
avatar: string;
@ -59,10 +77,19 @@ export interface User {
presenter?: boolean;
pinned?: boolean;
guest?: boolean;
guestStatus: string;
joinErrorCode: string;
joinErrorMessage: string;
joined: boolean;
loggedOut: boolean;
mobile?: boolean;
whiteboardAccess?: boolean;
isDialIn: boolean;
voice?: Partial<Voice>;
locked: boolean;
registeredAt: number;
registeredOn: string;
hasDrawPermissionOnCurrentPage: boolean;
lastBreakoutRoom?: LastBreakoutRoom;
cameras: Array<Cameras>;
presPagesWritable: Array<PresPagesWritable>;
@ -72,4 +99,6 @@ export interface User {
away: boolean;
raiseHand: boolean;
reaction: Reaction;
customParameters: Array<CustomParameter>;
userClientSettings: UserClientSettings;
}

View File

@ -117,7 +117,7 @@ const intlMessages = defineMessages({
},
defaultViewLabel: {
id: 'app.title.defaultViewLabel',
description: 'view name apended to document title',
description: 'view name appended to document title',
},
promotedLabel: {
id: 'app.toast.promotedLabel',

View File

@ -626,7 +626,7 @@ class AudioModal extends Component {
<Styled.BrowserWarning>
<FormattedMessage
id="app.audioModal.unsupportedBrowserLabel"
description="Warning when someone joins with a browser that isnt supported"
description="Warning when someone joins with a browser that isn't supported"
values={{
0: <a href="https://www.google.com/chrome/">Chrome</a>,
1: <a href="https://getfirefox.com">Firefox</a>,

View File

@ -208,7 +208,7 @@ class AudioSettings extends React.Component {
const { outputDeviceId: currentOutputDeviceId } = this.state;
// withEcho usage (isLive arg): if local echo is enabled we need the device
// change to be performed seamlessly (which is what the isLive parameter guarantes)
// change to be performed seamlessly (which is what the isLive parameter guarantees)
changeOutputDevice(deviceId, withEcho)
.then(() => {
this.setState({

View File

@ -67,11 +67,11 @@ const intlMessages = defineMessages({
},
BrowserNotSupported: {
id: 'app.audioNotification.audioFailedError1003',
description: 'browser not supported error messsage',
description: 'browser not supported error message',
},
reconectingAsListener: {
id: 'app.audioNotificaion.reconnectingAsListenOnly',
description: 'ice negociation error messsage',
description: 'ice negotiation error message',
},
});

View File

@ -29,7 +29,7 @@ const intlMessages = defineMessages({
},
dictationStop: {
id: 'app.captions.dictationStop',
description: 'Label for stoping speech recognition',
description: 'Label for stopping speech recognition',
},
dictationOnDesc: {
id: 'app.captions.dictationOnDesc',

View File

@ -1,15 +1,32 @@
export const textToMarkdown = (message: string) => {
let parsedMessage = message || '';
parsedMessage = parsedMessage.trim();
const parsedMessage = message || '';
// replace url with markdown links
const urlRegex = /(?<!\]\()https?:\/\/([\w-]+\.)+\w{1,6}([/?=&#.]?[\w-]+)*/gm;
parsedMessage = parsedMessage.replace(urlRegex, '[$&]($&)');
// regular expression to match urls
const urlRegex = /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g;
// replace new lines with markdown new lines
parsedMessage = parsedMessage.replace(/\n\r?/g, ' \n');
// regular expression to match URLs with IP addresses
const ipUrlRegex = /\b(?:https?:\/\/)?(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?::\d{1,5})?(?:\/\S*)?\b/g;
return parsedMessage;
// regular expression to match Markdown links
const mdRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
// regular expression to match new lines
const newLineRegex = /\n\r?/g;
// append https:// to URLs that don't have it
const appendHttps = (match: string, text: string, url: string) => {
if (!/^https?:\/\//.test(url)) {
return `[${text}](https://${url})`;
}
return match;
};
return parsedMessage
.trim()
.replace(urlRegex, '[$&]($&)')
.replace(ipUrlRegex, '[$&]($&)')
.replace(mdRegex, appendHttps)
.replace(newLineRegex, ' \n');
};
export default {

View File

@ -3,6 +3,7 @@ import {
Bar, BarChart, ResponsiveContainer, XAxis, YAxis,
} from 'recharts';
import caseInsensitiveReducer from '/imports/utils/caseInsensitiveReducer';
import { defineMessages, useIntl } from 'react-intl';
import Styled from './styles';
interface ChatPollContentProps {
@ -25,6 +26,29 @@ interface Answers {
id: number;
}
const intlMessages = defineMessages({
true: {
id: 'app.poll.t',
description: 'Poll true option value',
},
false: {
id: 'app.poll.f',
description: 'Poll false option value',
},
yes: {
id: 'app.poll.y',
description: 'Poll yes option value',
},
no: {
id: 'app.poll.n',
description: 'Poll no option value',
},
abstention: {
id: 'app.poll.abstention',
description: 'Poll Abstention option value',
},
});
function assertAsMetadata(metadata: unknown): asserts metadata is Metadata {
if (typeof metadata !== 'object' || metadata === null) {
throw new Error('metadata is not an object');
@ -52,14 +76,23 @@ function assertAsMetadata(metadata: unknown): asserts metadata is Metadata {
const ChatPollContent: React.FC<ChatPollContentProps> = ({
metadata: string,
}) => {
const intl = useIntl();
const pollData = JSON.parse(string) as unknown;
assertAsMetadata(pollData);
const answers = pollData.answers.reduce(
caseInsensitiveReducer, [],
);
const answers = pollData.answers.reduce(caseInsensitiveReducer, []);
const height = answers.length * 50;
const translatedAnswers = answers.map((answer: Answers) => {
const translationKey = intlMessages[answer.key.toLowerCase() as keyof typeof intlMessages];
const pollAnswer = translationKey ? intl.formatMessage(translationKey) : answer.key;
return {
...answer,
pollAnswer,
};
});
const height = translatedAnswers.length * 50;
return (
<div data-test="chatPollMessageText">
<Styled.PollText>
@ -67,11 +100,11 @@ const ChatPollContent: React.FC<ChatPollContentProps> = ({
</Styled.PollText>
<ResponsiveContainer width="90%" height={height}>
<BarChart
data={answers}
data={translatedAnswers}
layout="vertical"
>
<XAxis type="number" />
<YAxis width={80} type="category" dataKey="key" />
<YAxis width={80} type="category" dataKey="pollAnswer" />
<Bar dataKey="numVotes" fill="#0C57A7" />
</BarChart>
</ResponsiveContainer>

View File

@ -29,7 +29,7 @@ const isBoolean = (v: unknown): boolean => {
} if (v === 'false') {
return false;
}
// if v is not difined it shouldn't be considered on comparation, so it returns true
// if v is not defined it shouldn't be considered on comparison, so it returns true
return true;
};

View File

@ -0,0 +1,37 @@
import { useSubscription } from '@apollo/client';
import React, { useEffect, useState } from 'react';
import CURRENT_USER_SUBSCRIPTION from '/imports/ui/core/graphql/queries/currentUserSubscription';
import { User } from '/imports/ui/Types/user';
import Users from '/imports/api/users';
import logger from '/imports/startup/client/logger';
interface UserCurrentResponse {
user_current: Array<User>;
}
const UserGrapQlMiniMongoAdapter: React.FC = () => {
const {
error,
data,
} = useSubscription<UserCurrentResponse>(CURRENT_USER_SUBSCRIPTION);
const [userDataSetted, setUserDataSetted] = useState(false);
useEffect(() => {
if (error) {
logger.error('Error in UserGrapQlMiniMongoAdapter', error);
}
}, [error]);
useEffect(() => {
if (data && data.user_current) {
const { userId } = data.user_current[0];
Users.upsert({ userId }, data.user_current[0]);
if (!userDataSetted) {
setUserDataSetted(true);
}
}
}, [data]);
return null;
};
export default UserGrapQlMiniMongoAdapter;

View File

@ -56,6 +56,7 @@ const ConnectionManager: React.FC<ConnectionManagerProps> = ({ children }): Reac
const subscription = new SubscriptionClient(graphqlUrl, {
reconnect: true,
timeout: 30000,
minTimeout: 30000,
connectionParams: {
headers: {
'X-Session-Token': sessionToken,

View File

@ -1,4 +1,5 @@
import React, { useEffect } from 'react';
import { Session } from 'meteor/session';
import { ErrorScreen } from '../../error-screen/component';
import LoadingScreen from '../../common/loading-screen/component';
@ -69,6 +70,10 @@ const StartupDataFetch: React.FC<StartupDataFetchProps> = ({
setSettingsFetched(true);
clearTimeout(timeoutRef.current);
setLoading(false);
}).catch(() => {
Session.set('errorMessageDescription', 'meeting_ended');
setError('Error fetching startup data');
setLoading(false);
});
}, []);
@ -79,6 +84,7 @@ const StartupDataFetch: React.FC<StartupDataFetchProps> = ({
? (
<ErrorScreen
endedReason={error}
code={403}
/>
)
: null}

View File

@ -30,7 +30,7 @@ const intlMessages = defineMessages({
},
more: {
id: 'app.connection-status.more',
description: 'More about conectivity issues',
description: 'More about connectivity issues',
},
audioLabel: {
id: 'app.settings.audioTab.label',
@ -386,7 +386,7 @@ class ConnectionStatusComponent extends PureComponent {
}
/**
* Render network data , containing information abount current upload and
* Render network data , containing information about current upload and
* download rates
* @return {Object} The component to be renderized.
*/

View File

@ -263,7 +263,7 @@ const getNetworkData = async () => {
};
/**
* Calculates both upload and download rates using data retreived from getStats
* Calculates both upload and download rates using data retrieved from getStats
* API. For upload (outbound-rtp) we use both bytesSent and timestamp fields.
* byteSent field contains the number of octets sent at the given timestamp,
* more information can be found in:

View File

@ -94,7 +94,7 @@ const reducer = (state, action) => {
}
// LAYOUT TYPE
// using to load a diferent layout manager
// using to load a different layout manager
case ACTIONS.SET_LAYOUT_TYPE: {
const { layoutType } = state.input;
if (layoutType === action.value) return state;

View File

@ -21,8 +21,9 @@ const LayoutModalComponent = (props) => {
} = props;
const [selectedLayout, setSelectedLayout] = useState(application.selectedLayout);
const [updateAllUsed, setUpdateAllUsed] = useState(false);
const BASE_NAME = window.meetingClientSettings.public.app.basename;
const BASE_NAME = window.meetingClientSettings.public.app.cdn + window.meetingClientSettings.public.app.basename;
const LAYOUTS_PATH = `${BASE_NAME}/resources/images/layouts/`;
const isKeepPushingLayoutEnabled = SettingsService.isKeepPushingLayoutEnabled();
@ -44,6 +45,14 @@ const LayoutModalComponent = (props) => {
id: 'app.layout.modal.layoutLabel',
description: 'Layout label',
},
layoutToastLabelAuto: {
id: 'app.layout.modal.layoutToastLabelAuto',
description: 'Layout toast label',
},
layoutToastLabelAutoOff: {
id: 'app.layout.modal.layoutToastLabelAutoOff',
description: 'Layout toast label',
},
layoutToastLabel: {
id: 'app.layout.modal.layoutToastLabel',
description: 'Layout toast label',
@ -83,6 +92,15 @@ const LayoutModalComponent = (props) => {
application:
{ ...application, selectedLayout, pushLayout: updateAll },
};
if ((isModerator || isPresenter) && updateAll) {
updateSettings(obj, intlMessages.layoutToastLabelAuto);
setUpdateAllUsed(true);
} else if ((isModerator || isPresenter) && !updateAll && !updateAllUsed) {
updateSettings(obj, intlMessages.layoutToastLabelAutoOff);
setUpdateAllUsed(false);
} else {
updateSettings(obj, intlMessages.layoutToastLabel);
}
updateSettings(obj, intlMessages.layoutToastLabel, setLocalSettings);
setIsOpen(false);
};

View File

@ -70,10 +70,15 @@ class PushLayoutEngine extends React.Component {
pushLayoutMeeting,
} = this.props;
const userLayout = LAYOUT_TYPE[getFromUserSettings('bbb_change_layout', false)];
Settings.application.selectedLayout = enforceLayout || userLayout || meetingLayout;
const changeLayout = LAYOUT_TYPE[getFromUserSettings('bbb_change_layout', null)];
const defaultLayout = LAYOUT_TYPE[getFromUserSettings('bbb_default_layout', null)];
let selectedLayout = Settings.application.selectedLayout;
Settings.application.selectedLayout = enforceLayout
|| changeLayout
|| defaultLayout
|| meetingLayout;
let { selectedLayout } = Settings.application;
if (isMobile()) {
selectedLayout = selectedLayout === 'custom' ? 'smart' : selectedLayout;
Settings.application.selectedLayout = selectedLayout;

View File

@ -23,7 +23,7 @@ export {
isMobile, isTablet, isTabletPortrait, isTabletLandscape, isDesktop,
};
// Array for select component to select diferent layout
// Array for select component to select different layout
const suportedLayouts = [
{
layoutKey: LAYOUT_TYPE.SMART_LAYOUT,

View File

@ -167,7 +167,7 @@ export default class Legacy extends Component {
<p className="browserWarning">
<FormattedMessage
id={messageId}
description="Warning when someone joins with a browser that isnt supported"
description="Warning when someone joins with a browser that isn't supported"
values={{
0: <a href="https://www.google.com/chrome/">Chrome</a>,
1: <a href="https://getfirefox.com">Firefox</a>,

View File

@ -11,7 +11,7 @@ const lockContextContainer = (component) => withTracker(() => {
const lockSetting = new LockStruct();
const Meeting = Meetings.findOne({ meetingId: Auth.meetingID },
{ fields: { lockSettingsProps: 1 } });
const User = Users.findOne({ userId: Auth.userID, meetingId: Auth.meetingID },
const User = Users.findOne({ userId: Auth.userID },
{ fields: { role: 1, locked: 1 } });
const userIsLocked = User ? User.locked && User.role !== ROLE_MODERATOR : true;
const lockSettings = Meeting.lockSettingsProps;

View File

@ -68,7 +68,7 @@ const intlMessage = defineMessages({
},
confirmDesc: {
id: 'app.leaveConfirmation.confirmDesc',
description: 'adds context to confim option',
description: 'adds context to confirm option',
},
sendLabel: {
id: 'app.feedback.sendFeedback',

View File

@ -93,7 +93,7 @@ class LeaveMeetingButton extends PureComponent {
this.menuItems.push(
{
key: 'list-item-logout',
dataTest: 'logoutButton',
dataTest: 'directLogoutButton',
icon: 'logout',
label: intl.formatMessage(intlMessages.leaveSessionLabel),
description: intl.formatMessage(intlMessages.leaveSessionDesc),

View File

@ -9,7 +9,7 @@ export interface IsBreakoutSubscriptionData {
meeting: Array<IsBreakoutData>;
}
// TODO: rework when useMeeting hook be avaible
// TODO: rework when useMeeting hook be available
export const MEETING_ISBREAKOUT_SUBSCRIPTION = gql`
subscription getIsBreakout {
meeting {

View File

@ -359,7 +359,7 @@ class OptionsDropdown extends PureComponent {
if (allowLogoutSetting) {
bottomItems.push({
key: 'list-item-logout',
dataTest: 'logout',
dataTest: 'optionsLogoutButton',
icon: 'logout',
label: intl.formatMessage(intlMessages.leaveSessionLabel),
description: intl.formatMessage(intlMessages.leaveSessionDesc),

View File

@ -44,6 +44,15 @@ export default injectIntl(injectCurrentUser(withTracker(({ intl, currentUser })
return UserService.UserLeftMeetingAlert(obj);
}
if (obj.messageId === 'app.layoutUpdate.label') {
const last = new Date(Session.get('lastLayoutUpdateNotification'));
const now = new Date();
if (now - last < 1000) {
return {};
}
Session.set('lastLayoutUpdateNotification', now);
}
return notify(
<FormattedMessage
id={obj.messageId}

View File

@ -62,7 +62,7 @@ const intlMessages = defineMessages({
},
customInputToggleLabel: {
id: 'app.poll.customInput.label',
description: 'poll custom input toogle button label',
description: 'poll custom input toggle button label',
},
customInputInstructionsLabel: {
id: 'app.poll.customInputInstructions.label',

View File

@ -11,7 +11,7 @@ import { POLL_PUBLISH_RESULT, POLL_CANCEL, POLL_CREATE } from './mutations';
import { ACTIONS, PANELS } from '../layout/enums';
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_id;
const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_group_id;
const PollContainer = (props) => {
const layoutContextDispatch = layoutDispatch();

View File

@ -90,6 +90,16 @@ export const PRES_ANNOTATION_SUBMIT = gql`
}
`;
export const PRESENTATION_PUBLISH_CURSOR = gql`
mutation PresentationPublishCursor($whiteboardId: String!, $xPercent: Float!, $yPercent: Float!) {
presentationPublishCursor(
whiteboardId: $whiteboardId,
xPercent: $xPercent,
yPercent: $yPercent,
)
}
`;
export default {
PRESENTATION_SET_ZOOM,
PRESENTATION_SET_WRITERS,
@ -100,4 +110,5 @@ export default {
PRESENTATION_REMOVE,
PRES_ANNOTATION_DELETE,
PRES_ANNOTATION_SUBMIT,
PRESENTATION_PUBLISH_CURSOR,
};

View File

@ -565,6 +565,7 @@ class PresentationUploader extends Component {
commands[newCurrentIndex] = {
$apply: (presentation) => {
if (!presentation) return;
const p = presentation;
if (p) {
p.current = true;

View File

@ -20,7 +20,7 @@ const intlMessages = defineMessages({
},
pushAlertLabel: {
id: 'app.submenu.notification.pushAlertLabel',
description: 'push notifiation label',
description: 'push notification label',
},
messagesLabel: {
id: 'app.submenu.notification.messagesLabel',

View File

@ -12,7 +12,7 @@ import { isChatEnabled } from '/imports/ui/services/features';
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
const TYPING_INDICATOR_ENABLED = CHAT_CONFIG.typingIndicator.enabled;
const SUBSCRIPTIONS = [
'users',
// 'users',
'meetings',
'polls',
'captions',

View File

@ -177,28 +177,34 @@ const UserListItem: React.FC<UserListItemProps> = ({ user, lockSettings }) => {
const getIconUser = () => {
const emojiSize = convertRemToPixels(1.3);
if (user.isDialIn) {
return <Icon iconName="volume_level_2" />;
}
if (user.raiseHand === true) {
return reactionsEnabled
? <Emoji key={emojiIcons[0].id} emoji={emojiIcons[0]} native={emojiIcons[0].native} size={emojiSize} />
: <Icon iconName={normalizeEmojiName('raiseHand')} />;
} if (user.away === true) {
}
if (user.away === true) {
return reactionsEnabled
? <Emoji key="away" emoji={emojiIcons[1]} native={emojiIcons[1].native} size={emojiSize} />
: <Icon iconName={normalizeEmojiName('away')} />;
} if (user.emoji !== 'none' && user.emoji !== 'notAway') {
}
if (user.emoji !== 'none' && user.emoji !== 'notAway') {
return <Icon iconName={normalizeEmojiName(user.emoji)} />;
} if (user.reaction && user.reaction.reactionEmoji !== 'none') {
}
if (user.reaction && user.reaction.reactionEmoji !== 'none') {
return user.reaction.reactionEmoji;
} if (user.name && userAvatarFiltered.length === 0) {
}
if (user.name && userAvatarFiltered.length === 0) {
return user.name.toLowerCase().slice(0, 2);
} return '';
}
return '';
};
const iconUser = getIconUser();
const avatarContent = user.lastBreakoutRoom?.currentlyInRoom && userAvatarFiltered.length === 0
? user.lastBreakoutRoom?.sequence
: iconUser;
: getIconUser();
const hasWhiteboardAccess = user?.presPagesWritable?.some((page) => page.isCurrentPage);

View File

@ -123,7 +123,7 @@ interface RenderModalProps {
isOpen: boolean;
priority: string;
/* Use 'any' if you don't have specific props;
As this props varies in types usage of any is most apropriate */
As this props varies in types usage of any is most appropriate */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Component: React.ComponentType<any>;
otherOptions: object;

View File

@ -309,7 +309,7 @@ class VideoPreview extends Component {
const newDevices = await navigator.mediaDevices.enumerateDevices();
webcams = PreviewService.digestVideoDevices(newDevices, webcamDeviceId).webcams;
} catch (error) {
// Not a critical error beucase it should only affect UI; log it
// Not a critical error because it should only affect UI; log it
// and go ahead
logger.error({
logCode: 'video_preview_enumerate_relabel_failure',
@ -1059,7 +1059,7 @@ class VideoPreview extends Component {
<Styled.BrowserWarning>
<FormattedMessage
id="app.audioModal.unsupportedBrowserLabel"
description="Warning when someone joins with a browser that isnt supported"
description="Warning when someone joins with a browser that isn't supported"
values={{
0: <a href="https://www.google.com/chrome/">Chrome</a>,
1: <a href="https://getfirefox.com">Firefox</a>,

View File

@ -200,7 +200,7 @@ const doGUM = (deviceId, profile) => {
// Chrome/Edge sometimes bork gUM calls when switching camera
// profiles. This looks like a browser bug. Track release not
// being done synchronously -> quick subsequent gUM calls for the same
// device (profile switching) -> device becoming unavaible while previous
// device (profile switching) -> device becoming unavailable while previous
// tracks aren't finished - prlanzarin
if (browserInfo.isChrome || browserInfo.isEdge) {
const opts = {

View File

@ -205,7 +205,7 @@ class VideoProvider extends Component {
} = this.props;
const { socketOpen } = this.state;
// Only debounce when page changes to avoid unecessary debouncing
// Only debounce when page changes to avoid unnecessary debouncing
const shouldDebounce = VideoService.isPaginationEnabled()
&& prevProps.currentVideoPageIndex !== currentVideoPageIndex;
@ -1174,7 +1174,7 @@ class VideoProvider extends Component {
peer.started = true;
// Clear camera shared timeout when camera succesfully starts
// Clear camera shared timeout when camera successfully starts
this.clearRestartTimers(stream);
this.attachVideoStream(stream);
@ -1261,6 +1261,7 @@ class VideoProvider extends Component {
focusedId,
handleVideoFocus,
isGridEnabled,
users,
} = this.props;
return (
@ -1273,6 +1274,7 @@ class VideoProvider extends Component {
focusedId,
handleVideoFocus,
isGridEnabled,
users,
}}
onVideoItemMount={this.createVideoTag}
onVideoItemUnmount={this.destroyVideoTag}

View File

@ -1,10 +1,14 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import { useMutation } from '@apollo/client';
import { useMutation, useSubscription } from '@apollo/client';
import VideoProvider from './component';
import VideoService from './service';
import { sortVideoStreams } from '/imports/ui/components/video-provider/stream-sorting';
import { CAMERA_BROADCAST_START, CAMERA_BROADCAST_STOP } from './mutations';
import { getVideoData, getVideoDataGrid } from './queries';
import useMeeting from '/imports/ui/core/hooks/useMeeting';
import Auth from '/imports/ui/services/auth';
import useCurrentUser from '../../core/hooks/useCurrentUser';
const { defaultSorting: DEFAULT_SORTING } = window.meetingClientSettings.public.kurento.cameraSortingModes;
@ -44,24 +48,70 @@ const VideoProviderContainer = ({ children, ...props }) => {
};
export default withTracker(({ swapLayout, ...rest }) => {
// getVideoStreams returns a dictionary consisting of:
// {
// streams: array of mapped streams
// totalNumberOfStreams: total number of shared streams in the server
// }
const isGridLayout = Session.get('isGridEnabled');
const graphqlQuery = isGridLayout ? getVideoDataGrid : getVideoData;
const currUserId = Auth.userID;
const { data: currentMeeting } = useMeeting((m) => ({
usersPolicies: m.usersPolicies,
}));
const { data: currentUser } = useCurrentUser((user) => ({
locked: user.locked,
}));
const fetchedStreams = VideoService.fetchVideoStreams();
const variables = isGridLayout
? {}
: {
userIds: fetchedStreams.map((stream) => stream.userId) || [],
};
const {
streams,
gridUsers,
totalNumberOfStreams,
} = VideoService.getVideoStreams();
data: videoUserSubscription,
} = useSubscription(graphqlQuery, { variables });
const users = videoUserSubscription?.user || [];
let streams = [];
let gridUsers = [];
let totalNumberOfStreams = 0;
if (isGridLayout) {
streams = fetchedStreams;
gridUsers = VideoService.getGridUsers(videoUserSubscription?.user, fetchedStreams);
totalNumberOfStreams = fetchedStreams.length;
} else {
const {
streams: s,
totalNumberOfStreams: ts,
} = VideoService.getVideoStreams();
streams = s;
totalNumberOfStreams = ts;
}
let usersVideo = streams;
if(gridUsers.length > 0) {
if (gridUsers.length > 0) {
const items = usersVideo.concat(gridUsers);
usersVideo = sortVideoStreams(items, DEFAULT_SORTING);
}
if (currentMeeting?.usersPolicies?.webcamsOnlyForModerator
&& currentUser?.locked) {
if (users.length > 0) {
usersVideo = usersVideo.filter((uv) => {
if (uv.userId === currUserId) {
return true;
}
const user = users.find((u) => u.userId === uv.userId);
return user?.isModerator;
});
}
}
return {
swapLayout,
streams: usersVideo,
@ -69,6 +119,7 @@ export default withTracker(({ swapLayout, ...rest }) => {
isUserLocked: VideoService.isUserLocked(),
currentVideoPageIndex: VideoService.getCurrentVideoPageIndex(),
isMeteorConnected: Meteor.status().connected,
users,
...rest,
};
})(VideoProviderContainer);

View File

@ -0,0 +1,57 @@
import { gql } from '@apollo/client';
import { User } from '../../Types/user';
export interface getVideoDataResponse {
user: Array<Pick<User, 'loggedOut'| 'away'| 'disconnected'| 'emoji'| 'name'>>
}
export type queryUser = Pick<User, 'loggedOut'| 'away'| 'disconnected'| 'emoji'| 'name'>
export const getVideoData = gql`
subscription getvideoData($userIds: [String]!) {
user(where: {userId: {_in: $userIds}}) {
loggedOut
away
disconnected
emoji
name
role
avatar
color
presenter
clientType
userId
raiseHand
isModerator
reaction {
reactionEmoji
}
}
}
`;
export const getVideoDataGrid = gql`
subscription getVideoDataGrid {
user {
loggedOut
away
disconnected
emoji
name
role
avatar
color
presenter
clientType
userId
raiseHand
reaction {
reactionEmoji
}
}
}
`;
export default {
getVideoData,
getVideoDataGrid,
};

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