Merge remote-tracking branch 'upstream/v3.0.x-release' into PR_19785
This commit is contained in:
commit
3105bee74b
@ -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.
|
||||
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 = {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
|
@ -65,6 +65,7 @@ expire {
|
||||
services {
|
||||
bbbWebAPI = "https://192.168.23.33/bigbluebutton/api"
|
||||
sharedSecret = "changeme"
|
||||
checkSumAlgorithmForBreakouts = "sha256"
|
||||
}
|
||||
|
||||
eventBus {
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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.");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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.");
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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"
|
||||
|
@ -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")) {
|
||||
|
@ -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)
|
||||
|
@ -26,7 +26,7 @@ import org.slf4j.LoggerFactory;
|
||||
* the following:
|
||||
* <pre>
|
||||
<extension>
|
||||
<condition field="destination_number" expresssion="444">
|
||||
<condition field="destination_number" expression="444">
|
||||
<action application="socket" data="192.168.100.88:8084 async full"/>
|
||||
</condition>
|
||||
</extension>
|
||||
|
@ -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 });
|
||||
//
|
||||
|
24
bbb-graphql-actions/src/actions/presentationPublishCursor.ts
Normal file
24
bbb-graphql-actions/src/actions/presentationPublishCursor.ts
Normal 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 };
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
64
bbb-graphql-middleware/internal/common/CustomRateLimiter.go
Normal file
64
bbb-graphql-middleware/internal/common/CustomRateLimiter.go
Normal 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
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -287,6 +287,14 @@ type Mutation {
|
||||
): Boolean
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
presentationPublishCursor(
|
||||
whiteboardId: String!
|
||||
xPercent: Float!
|
||||
yPercent: Float!
|
||||
): Boolean
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
presentationRemove(
|
||||
presentationId: String!
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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 -->
|
||||
|
@ -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"/>
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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({
|
||||
|
@ -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({
|
||||
|
@ -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;
|
||||
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
import './methods';
|
@ -1,6 +0,0 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import publishCursorUpdate from './methods/publishCursorUpdate';
|
||||
|
||||
Meteor.methods({
|
||||
publishCursorUpdate,
|
||||
});
|
@ -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);
|
||||
}
|
@ -66,6 +66,7 @@ const currentParameters = [
|
||||
'bbb_hide_nav_bar',
|
||||
'bbb_change_layout',
|
||||
'bbb_direct_leave_button',
|
||||
'bbb_default_layout',
|
||||
];
|
||||
|
||||
function valueParser(val) {
|
||||
|
@ -27,6 +27,7 @@ export interface Public {
|
||||
}
|
||||
|
||||
export interface App {
|
||||
instanceId: string
|
||||
mobileFontSize: string
|
||||
desktopFontSize: string
|
||||
audioChatNotification: boolean
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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>,
|
||||
|
@ -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({
|
||||
|
@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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;
|
@ -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,
|
||||
|
@ -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}
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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>,
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
|
@ -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),
|
||||
|
@ -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 {
|
||||
|
@ -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),
|
||||
|
@ -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}
|
||||
|
@ -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',
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -565,6 +565,7 @@ class PresentationUploader extends Component {
|
||||
|
||||
commands[newCurrentIndex] = {
|
||||
$apply: (presentation) => {
|
||||
if (!presentation) return;
|
||||
const p = presentation;
|
||||
if (p) {
|
||||
p.current = true;
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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>,
|
||||
|
@ -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 = {
|
||||
|
@ -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}
|
||||
|
@ -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);
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user