Merge branch 'v3.0.x-release' into avoid-unrelated-notifications

This commit is contained in:
Gabriel Luiz Porfirio 2024-03-15 09:06:59 -03:00 committed by GitHub
commit 85f7110a7b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
329 changed files with 5929 additions and 992 deletions

View File

@ -75,5 +75,6 @@ daemonUser in Linux := user
daemonGroup in Linux := group
javaOptions in Universal ++= Seq("-J-Xms130m", "-J-Xmx256m", "-Dconfig.file=/etc/bigbluebutton/bbb-apps-akka.conf", "-Dlogback.configurationFile=conf/logback.xml")
javaOptions in reStart ++= Seq("-Dconfig.file=/etc/bigbluebutton/bbb-apps-akka.conf", "-Dlogback.configurationFile=conf/logback.xml")
debianPackageDependencies in Debian ++= Seq("java17-runtime-headless", "bash")

View File

@ -0,0 +1,25 @@
package org.bigbluebutton.core.apps.users
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.RightsManagementTrait
import org.bigbluebutton.core.db.UserConnectionStatusDAO
import org.bigbluebutton.core.models.{ UserState, Users2x }
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
trait UserConnectionAliveReqMsgHdlr extends RightsManagementTrait {
this: UsersApp =>
val liveMeeting: LiveMeeting
val outGW: OutMsgRouter
def handleUserConnectionAliveReqMsg(msg: UserConnectionAliveReqMsg): Unit = {
log.info("handleUserConnectionAliveReqMsg: userId={}", msg.body.userId)
for {
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId)
} yield {
UserConnectionStatusDAO.updateUserAlive(user.intId)
}
}
}

View File

@ -0,0 +1,25 @@
package org.bigbluebutton.core.apps.users
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.RightsManagementTrait
import org.bigbluebutton.core.db.UserConnectionStatusDAO
import org.bigbluebutton.core.models.{ UserState, Users2x }
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
trait UserConnectionUpdateRttReqMsgHdlr extends RightsManagementTrait {
this: UsersApp =>
val liveMeeting: LiveMeeting
val outGW: OutMsgRouter
def handleUserConnectionUpdateRttReqMsg(msg: UserConnectionUpdateRttReqMsg): Unit = {
log.info("handleUserConnectionUpdateRttReqMsg: networkRttInMs={} userId={}", msg.body.networkRttInMs, msg.body.userId)
for {
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId)
} yield {
UserConnectionStatusDAO.updateUserRtt(user.intId, msg.body.networkRttInMs)
}
}
}

View File

@ -168,6 +168,8 @@ class UsersApp(
with AssignPresenterReqMsgHdlr
with ChangeUserPinStateReqMsgHdlr
with ChangeUserMobileFlagReqMsgHdlr
with UserConnectionAliveReqMsgHdlr
with UserConnectionUpdateRttReqMsgHdlr
with ChangeUserReactionEmojiReqMsgHdlr
with ChangeUserRaiseHandReqMsgHdlr
with ChangeUserAwayReqMsgHdlr

View File

@ -5,18 +5,22 @@ import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{ Failure, Success }
case class UserConnectionStatusDbModel(
userId: String,
meetingId: String,
connectionAliveAt: Option[java.sql.Timestamp]
userId: String,
meetingId: String,
connectionAliveAt: Option[java.sql.Timestamp],
userClientResponseAt: Option[java.sql.Timestamp],
networkRttInMs: Option[Double]
)
class UserConnectionStatusDbTableDef(tag: Tag) extends Table[UserConnectionStatusDbModel](tag, None, "user_connectionStatus") {
override def * = (
userId, meetingId, connectionAliveAt
userId, meetingId, connectionAliveAt, userClientResponseAt, networkRttInMs
) <> (UserConnectionStatusDbModel.tupled, UserConnectionStatusDbModel.unapply)
val userId = column[String]("userId", O.PrimaryKey)
val meetingId = column[String]("meetingId")
val connectionAliveAt = column[Option[java.sql.Timestamp]]("connectionAliveAt")
val userClientResponseAt = column[Option[java.sql.Timestamp]]("userClientResponseAt")
val networkRttInMs = column[Option[Double]]("networkRttInMs")
}
object UserConnectionStatusDAO {
@ -27,7 +31,9 @@ object UserConnectionStatusDAO {
UserConnectionStatusDbModel(
userId = userId,
meetingId = meetingId,
connectionAliveAt = None
connectionAliveAt = None,
userClientResponseAt = None,
networkRttInMs = None
)
)
).onComplete {
@ -36,4 +42,28 @@ object UserConnectionStatusDAO {
}
}
def updateUserAlive(userId: String) = {
DatabaseConnection.db.run(
TableQuery[UserConnectionStatusDbTableDef]
.filter(_.userId === userId)
.map(t => (t.connectionAliveAt))
.update(Some(new java.sql.Timestamp(System.currentTimeMillis())))
).onComplete {
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated connectionAliveAt on UserConnectionStatus table!")
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating connectionAliveAt on UserConnectionStatus: $e")
}
}
def updateUserRtt(userId: String, networkRttInMs: Double) = {
DatabaseConnection.db.run(
TableQuery[UserConnectionStatusDbTableDef]
.filter(_.userId === userId)
.map(t => (t.networkRttInMs, t.userClientResponseAt))
.update((Some(networkRttInMs), Some(new java.sql.Timestamp(System.currentTimeMillis()))))
).onComplete {
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated networkRttInMs on UserConnectionStatus table!")
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating networkRttInMs on UserConnectionStatus: $e")
}
}
}

View File

@ -111,6 +111,10 @@ class ReceivedJsonMsgHandlerActor(
routeGenericMsg[ChangeUserPinStateReqMsg](envelope, jsonNode)
case ChangeUserMobileFlagReqMsg.NAME =>
routeGenericMsg[ChangeUserMobileFlagReqMsg](envelope, jsonNode)
case UserConnectionAliveReqMsg.NAME =>
routeGenericMsg[UserConnectionAliveReqMsg](envelope, jsonNode)
case UserConnectionUpdateRttReqMsg.NAME =>
routeGenericMsg[UserConnectionUpdateRttReqMsg](envelope, jsonNode)
case SetUserSpeechLocaleReqMsg.NAME =>
routeGenericMsg[SetUserSpeechLocaleReqMsg](envelope, jsonNode)
case SelectRandomViewerReqMsg.NAME =>

View File

@ -405,6 +405,8 @@ class MeetingActor(
case m: SelectRandomViewerReqMsg => usersApp.handleSelectRandomViewerReqMsg(m)
case m: ChangeUserPinStateReqMsg => usersApp.handleChangeUserPinStateReqMsg(m)
case m: ChangeUserMobileFlagReqMsg => usersApp.handleChangeUserMobileFlagReqMsg(m)
case m: UserConnectionAliveReqMsg => usersApp.handleUserConnectionAliveReqMsg(m)
case m: UserConnectionUpdateRttReqMsg => usersApp.handleUserConnectionUpdateRttReqMsg(m)
case m: SetUserSpeechLocaleReqMsg => usersApp.handleSetUserSpeechLocaleReqMsg(m)
// Client requested to eject user

View File

@ -78,6 +78,8 @@ class AnalyticsActor(val includeChat: Boolean) extends Actor with ActorLogging {
case m: AssignPresenterReqMsg => logMessage(msg)
case m: ChangeUserPinStateReqMsg => logMessage(msg)
case m: ChangeUserMobileFlagReqMsg => logMessage(msg)
case m: UserConnectionAliveReqMsg => logMessage(msg)
case m: UserConnectionUpdateRttReqMsg => logMessage(msg)
case m: ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg => logMessage(msg)
case m: ScreenshareRtmpBroadcastStoppedVoiceConfEvtMsg => logMessage(msg)
case m: ScreenshareRtmpBroadcastStartedEvtMsg => logMessage(msg)

View File

@ -51,6 +51,7 @@ postgres {
}
numThreads = 1
maxConnections = 1
queueSize = 20000
}

View File

@ -293,6 +293,20 @@ object ChangeUserMobileFlagReqMsg { val NAME = "ChangeUserMobileFlagReqMsg" }
case class ChangeUserMobileFlagReqMsg(header: BbbClientMsgHeader, body: ChangeUserMobileFlagReqMsgBody) extends StandardMsg
case class ChangeUserMobileFlagReqMsgBody(userId: String, mobile: Boolean)
/**
* Sent from client to inform the connection is alive.
*/
object UserConnectionAliveReqMsg { val NAME = "UserConnectionAliveReqMsg" }
case class UserConnectionAliveReqMsg(header: BbbClientMsgHeader, body: UserConnectionAliveReqMsgBody) extends StandardMsg
case class UserConnectionAliveReqMsgBody(userId: String)
/**
* Sent from client to inform the RTT (time it took to send the Alive and receive confirmation).
*/
object UserConnectionUpdateRttReqMsg { val NAME = "UserConnectionUpdateRttReqMsg" }
case class UserConnectionUpdateRttReqMsg(header: BbbClientMsgHeader, body: UserConnectionUpdateRttReqMsgBody) extends StandardMsg
case class UserConnectionUpdateRttReqMsgBody(userId: String, networkRttInMs: Double)
/**
* Sent to all clients about a user mobile flag.
*/

View File

@ -0,0 +1,22 @@
import { RedisMessage } from '../types';
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
const eventName = `UserConnectionAliveReqMsg`;
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 = {
userId: routing.userId,
};
return { eventName, routing, header, body };
}

View File

@ -0,0 +1,23 @@
import { RedisMessage } from '../types';
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
const eventName = `UserConnectionUpdateRttReqMsg`;
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 = {
userId: routing.userId,
networkRttInMs: input.networkRttInMs
};
return { eventName, routing, header, body };
}

View File

@ -27,16 +27,10 @@ export default function UserConnectionStatus() {
//where is not necessary once user can update only its own status
//Hasura accepts "now()" as value to timestamp fields
const [updateUserClientResponseAtToMeAsNow] = useMutation(gql`
mutation UpdateConnectionClientResponse($networkRttInMs: numeric) {
update_user_connectionStatus(
where: {userClientResponseAt: {_is_null: true}}
_set: {
userClientResponseAt: "now()",
networkRttInMs: $networkRttInMs
}
) {
affected_rows
}
mutation UpdateConnectionRtt($networkRttInMs: Float!) {
userSetConnectionRtt(
networkRttInMs: $networkRttInMs
)
}
`);
@ -50,13 +44,8 @@ export default function UserConnectionStatus() {
const [updateConnectionAliveAtToMeAsNow] = useMutation(gql`
mutation UpdateConnectionAliveAt($userId: String, $connectionAliveAt: timestamp) {
update_user_connectionStatus(
where: {},
_set: { connectionAliveAt: "now()" }
) {
affected_rows
}
mutation UpdateConnectionAliveAt {
userSetConnectionAlive
}
`);

View File

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

@ -1,14 +1,17 @@
package main
import (
"context"
"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"
"time"
)
func main() {
@ -49,7 +52,26 @@ func main() {
}
}
http.HandleFunc("/", websrv.ConnectionHandler)
//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
}
}
limiterInterval := rate.NewLimiter(rate.Every(time.Duration(rateLimitInMs)*time.Millisecond), 1)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel()
if err := limiterInterval.Wait(ctx); err != nil {
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
return
}
websrv.ConnectionHandler(w, r)
})
log.Infof("listening on %v:%v", listenIp, listenPort)
log.Fatal(http.ListenAndServe(fmt.Sprintf("%v:%v", listenIp, listenPort), nil))

View File

@ -16,4 +16,5 @@ require (
github.com/evanphx/json-patch v0.5.2 // indirect
github.com/google/uuid v1.6.0 // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
golang.org/x/time v0.5.0 // indirect
)

View File

@ -27,6 +27,8 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

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,9 +50,9 @@ 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)
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,
@ -116,14 +116,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)
@ -136,8 +138,8 @@ func InvalidateSessionTokenConnections(sessionTokenToInvalidate string) {
for _, browserConnection := range BrowserConnections {
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 +159,10 @@ func InvalidateSessionTokenConnections(sessionTokenToInvalidate string) {
time.Sleep(100 * time.Millisecond)
}
log.Debugf("Processing invalidate request for sessionToken %v (hasura connection %v)", sessionTokenToInvalidate, browserConnection.HasuraConnection.Id)
hasuraConnectionId := browserConnection.HasuraConnection.Id
log.Debugf("Processing invalidate request for sessionToken %v (hasura connection %v)", sessionTokenToInvalidate, hasuraConnectionId)
browserConnection.HasuraConnection.ContextCancelFunc()
log.Debugf("Processed invalidate request for sessionToken %v (hasura connection %v)", sessionTokenToInvalidate, browserConnection.HasuraConnection.Id)
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, skiping 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,13 +19,21 @@ 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)
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

@ -18,7 +18,6 @@ RestartSec=60
SuccessExitStatus=143
TimeoutStopSec=5
PermissionsStartOnly=true
LimitNOFILE=1024
[Install]
WantedBy=multi-user.target bigbluebutton.target

View File

@ -451,6 +451,16 @@ type Mutation {
): Boolean
}
type Mutation {
userSetConnectionAlive: Boolean
}
type Mutation {
userSetConnectionRtt(
networkRttInMs: Float!
): Boolean
}
type Mutation {
userSetEmojiStatus(
emoji: String!
@ -544,4 +554,3 @@ input GuestUserApprovalStatus {
guest: String!
status: String!
}

View File

@ -409,6 +409,18 @@ actions:
handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}'
permissions:
- role: bbb_client
- name: userSetConnectionAlive
definition:
kind: synchronous
handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}'
permissions:
- role: bbb_client
- name: userSetConnectionRtt
definition:
kind: synchronous
handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}'
permissions:
- role: bbb_client
- name: userSetEmojiStatus
definition:
kind: synchronous
@ -486,5 +498,4 @@ custom_types:
input_objects:
- name: BreakoutRoom
- name: GuestUserApprovalStatus
objects: []
scalars: []

View File

@ -184,6 +184,7 @@ select_permissions:
- endedBy
- endedByUserName
- endedReasonCode
- isBreakout
- logoutUrl
- meetingId
- name

View File

@ -22,4 +22,4 @@ select_permissions:
filter:
meetingId:
_eq: X-Hasura-MeetingId
comment: ""
comment: ""

View File

@ -34,20 +34,3 @@ select_permissions:
_eq: X-Hasura-MeetingId
- userId:
_eq: X-Hasura-UserId
update_permissions:
- role: bbb_client
permission:
columns:
- connectionAliveAt
- userClientResponseAt
- networkRttInMs
filter:
_and:
- meetingId:
_eq: X-Hasura-MeetingId
- userId:
_eq: X-Hasura-UserId
check: null
set:
meetingId: x-hasura-MeetingId
userId: x-hasura-UserId

View File

@ -212,4 +212,4 @@ update_permissions:
_eq: X-Hasura-MeetingId
- userId:
_eq: X-Hasura-UserId
check: null
check: null

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.2 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu

View File

@ -1 +1 @@
BIGBLUEBUTTON_RELEASE=3.0.0-alpha.4
BIGBLUEBUTTON_RELEASE=3.0.0-alpha.5

View File

@ -0,0 +1,41 @@
import React, {
Suspense,
useContext,
useEffect,
} from 'react';
import { LoadingContext } from '/imports/ui/components/common/loading-screen/loading-screen-HOC/component';
import { defineMessages, useIntl } from 'react-intl';
const MeetingClientLazy = React.lazy(() => import('./meetingClient'));
const intlMessages = defineMessages({
loadingClientLabel: {
id: 'app.meeting.loadingClient',
description: 'loading client label',
},
});
const ClientStartup: React.FC = () => {
const loadingContextInfo = useContext(LoadingContext);
const intl = useIntl();
useEffect(() => {
loadingContextInfo.setLoading(true, intl.formatMessage(intlMessages.loadingClientLabel));
}, []);
return (
<Suspense>
{
(() => {
try {
return <MeetingClientLazy />;
} catch (error) {
loadingContextInfo.setLoading(false, '');
throw new Error('Error on rendering MeetingClientLazy: '.concat(JSON.stringify(error) || ''));
}
})()
}
</Suspense>
);
};
export default ClientStartup;

View File

@ -0,0 +1,42 @@
import React from 'react';
import ConnectionManager from '/imports/ui/components/connection-manager/component';
// eslint-disable-next-line react/no-deprecated
import { render } from 'react-dom';
import SettingsLoader from '/imports/ui/components/settings-loader/component';
import ErrorBoundary from '/imports/ui/components/common/error-boundary/component';
import { ErrorScreen } from '/imports/ui/components/error-screen/component';
import PresenceManager from '/imports/ui/components/join-handler/presenceManager/component';
import LoadingScreenHOC from '/imports/ui/components/common/loading-screen/loading-screen-HOC/component';
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 = () => {
return (
<StartupDataFetch>
<ErrorBoundary Fallback={ErrorScreen}>
<LoadingScreenHOC>
<IntlLoaderContainer>
{/* from there the error messages are located */}
<LocatedErrorBoundary Fallback={ErrorScreen}>
<ConnectionManager>
<PresenceManager>
<SettingsLoader />
<UserGrapQlMiniMongoAdapter />
<VoiceUserGrapQlMiniMongoAdapter />
</PresenceManager>
</ConnectionManager>
</LocatedErrorBoundary>
</IntlLoaderContainer>
</LoadingScreenHOC>
</ErrorBoundary>
</StartupDataFetch>
);
};
render(
<Main />,
document.getElementById('app'),
);

View File

@ -17,43 +17,49 @@
*/
/* eslint no-unused-vars: 0 */
import React from 'react';
import React, { useContext, useEffect } from 'react';
import { Meteor } from 'meteor/meteor';
import { render } from 'react-dom';
import logger from '/imports/startup/client/logger';
import '/imports/ui/services/mobile-app';
import Base from '/imports/startup/client/base';
import JoinHandler from '/imports/ui/components/join-handler/component';
import JoinHandler from '../imports/ui/components/join-handler/component';
import AuthenticatedHandler from '/imports/ui/components/authenticated-handler/component';
import Subscriptions from '/imports/ui/components/subscriptions/component';
import IntlStartup from '/imports/startup/client/intl';
import ContextProviders from '/imports/ui/components/context-providers/component';
import UsersAdapter from '/imports/ui/components/components-data/users-context/adapter';
import GraphqlProvider from '/imports/ui/components/graphql-provider/component';
import ConnectionManager from '/imports/ui/components/connection-manager/component';
import { liveDataEventBrokerInitializer } from '/imports/ui/services/LiveDataEventBroker/LiveDataEventBroker';
// The adapter import is "unused" as far as static code is concerned, but it
// needs to here to override global prototypes. So: don't remove it - prlanzarin 25 Apr 2022
import adapter from 'webrtc-adapter';
import collectionMirrorInitializer from './collection-mirror-initializer';
import { LoadingContext } from '/imports/ui/components/common/loading-screen/loading-screen-HOC/component';
import IntlAdapter from '/imports/startup/client/intlAdapter';
import PresenceAdapter from '/imports/ui/components/authenticated-handler/presence-adapter/component';
import CustomUsersSettings from '/imports/ui/components/join-handler/custom-users-settings/component';
import('/imports/api/audio/client/bridge/bridge-whitelist').catch(() => {
// bridge loading
});
const { disableWebsocketFallback } = Meteor.settings.public.app;
if (disableWebsocketFallback) {
Meteor.connection._stream._sockjsProtocolsWhitelist = function () { return ['websocket']; };
Meteor.disconnect();
Meteor.reconnect();
}
collectionMirrorInitializer();
liveDataEventBrokerInitializer();
Meteor.startup(() => {
// eslint-disable-next-line import/prefer-default-export
const Startup = () => {
const loadingContextInfo = useContext(LoadingContext);
useEffect(() => {
const { disableWebsocketFallback } = window.meetingClientSettings.public.app;
loadingContextInfo.setLoading(false, '');
if (disableWebsocketFallback) {
Meteor.connection._stream._sockjsProtocolsWhitelist = function () { return ['websocket']; };
Meteor.disconnect();
Meteor.reconnect();
}
}, []);
// Logs all uncaught exceptions to the client logger
window.addEventListener('error', (e) => {
let message = e.message || e.error.toString();
@ -76,24 +82,20 @@ Meteor.startup(() => {
}, message);
});
// TODO make this a Promise
render(
return (
<ContextProviders>
<>
<JoinHandler>
<AuthenticatedHandler>
<GraphqlProvider>
<Subscriptions>
<IntlStartup>
<Base />
</IntlStartup>
</Subscriptions>
</GraphqlProvider>
</AuthenticatedHandler>
</JoinHandler>
<PresenceAdapter>
<Subscriptions>
<IntlAdapter>
<Base />
</IntlAdapter>
</Subscriptions>
</PresenceAdapter>
<UsersAdapter />
</>
</ContextProviders>,
document.getElementById('app'),
</ContextProviders>
);
});
};
export default Startup;

View File

@ -2,8 +2,10 @@ import { Meteor } from 'meteor/meteor';
import validateAuthToken from './methods/validateAuthToken';
import setUserEffectiveConnectionType from './methods/setUserEffectiveConnectionType';
import userActivitySign from './methods/userActivitySign';
import validateConnection from './methods/validateConnection';
Meteor.methods({
validateConnection,
validateAuthToken,
setUserEffectiveConnectionType,
userActivitySign,

View File

@ -0,0 +1,65 @@
import { Client } from 'pg';
import upsertValidationState from '/imports/api/auth-token-validation/server/modifiers/upsertValidationState';
import { ValidationStates } from '/imports/api/auth-token-validation';
import userJoin from '/imports/api/users/server/handlers/userJoin';
import Users from '/imports/api/users';
import createDummyUser from '../modifiers/createDummyUser';
import updateUserConnectionId from '../modifiers/updateUserConnectionId';
async function validateConnection(requesterToken, meetingId, userId) {
try {
const client = new Client({
host: process.env.POSTGRES_HOST || Meteor.settings.private.postgresql.host,
port: process.env.POSTGRES_PORT || Meteor.settings.private.postgresql.port,
database: process.env.POSTGRES_HOST || Meteor.settings.private.postgresql.database,
user: process.env.POSTGRES_USER || Meteor.settings.private.postgresql.user,
password: process.env.POSTGRES_PASSWORD || Meteor.settings.private.postgresql.password,
query_timeout: process.env.POSTGRES_TIMEOUT || Meteor.settings.private.postgresql.timeout,
});
await client.connect();
const res = await client.query('select "meetingId", "userId" from v_user_connection_auth where "authToken" = $1', [requesterToken]);
if (res.rows.length === 0) {
await upsertValidationState(
meetingId,
userId,
ValidationStates.INVALID,
this.connection.id,
);
} else {
const sessionId = `${meetingId}--${userId}`;
this.setUserId(sessionId);
await upsertValidationState(
meetingId,
userId,
ValidationStates.VALIDATED,
this.connection.id,
);
const User = await Users.findOneAsync({
meetingId,
userId,
});
if (!User) {
await createDummyUser(meetingId, userId, requesterToken);
} else {
await updateUserConnectionId(meetingId, userId, this.connection.id);
}
userJoin(meetingId, userId, requesterToken);
}
await client.end();
} catch (e) {
await upsertValidationState(
meetingId,
userId,
ValidationStates.INVALID,
this.connection.id,
);
}
}
export default validateConnection;

View File

@ -25,7 +25,7 @@ import { useMutation } from '@apollo/client';
import { SET_EXIT_REASON } from '/imports/ui/core/graphql/mutations/userMutations';
import useUserChangedLocalSettings from '/imports/ui/services/settings/hooks/useUserChangedLocalSettings';
const CHAT_CONFIG = Meteor.settings.public.chat;
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_group_id;
const USER_WAS_EJECTED = 'userWasEjected';
@ -395,7 +395,7 @@ export default withTracker(() => {
currentConnectionId: 1,
connectionIdUpdateTime: 1,
};
const User = Users.findOne({ intId: credentials.requesterUserId }, { fields });
const User = Users.findOne({ userId: credentials.requesterUserId }, { fields });
const meeting = Meetings.findOne({ meetingId }, {
fields: {
meetingEnded: 1,
@ -412,30 +412,21 @@ export default withTracker(() => {
const ejected = User?.ejected;
const ejectedReason = User?.ejectedReason;
const meetingEndedReason = meeting?.meetingEndedReason;
const currentConnectionId = User?.currentConnectionId;
const { connectionID, connectionAuthTime } = Auth;
const connectionIdUpdateTime = User?.connectionIdUpdateTime;
if (ejected) {
// use the connectionID to block users, so we can detect if the user was
// blocked by the current connection. This is the case when a a user is
// ejected from a meeting but not permanently ejected. Permanent ejects are
// managed by the server, not by the client.
BBBStorage.setItem(USER_WAS_EJECTED, connectionID);
}
if (currentConnectionId && currentConnectionId !== connectionID && connectionIdUpdateTime > connectionAuthTime) {
Session.set('codeError', '409');
Session.set('errorMessageDescription', 'joined_another_window_reason')
BBBStorage.setItem(USER_WAS_EJECTED, User.userId);
}
let userSubscriptionHandler;
const codeError = Session.get('codeError');
const { streams: usersVideo } = VideoService.getVideoStreams();
return {
userWasEjected: (BBBStorage.getItem(USER_WAS_EJECTED) == connectionID),
userWasEjected: (BBBStorage.getItem(USER_WAS_EJECTED)),
approved,
ejected,
ejectedReason,

View File

@ -20,9 +20,9 @@ const propTypes = {
children: PropTypes.element.isRequired,
};
const DEFAULT_LANGUAGE = Meteor.settings.public.app.defaultSettings.application.fallbackLocale;
const CLIENT_VERSION = Meteor.settings.public.app.html5ClientBuild;
const FALLBACK_ON_EMPTY_STRING = Meteor.settings.public.app.fallbackOnEmptyLocaleString;
const DEFAULT_LANGUAGE = window.meetingClientSettings.public.app.defaultSettings.application.fallbackLocale;
const CLIENT_VERSION = window.meetingClientSettings.public.app.html5ClientBuild;
const FALLBACK_ON_EMPTY_STRING = window.meetingClientSettings.public.app.fallbackOnEmptyLocaleString;
const RTL_LANGUAGES = ['ar', 'dv', 'fa', 'he'];
const LARGE_FONT_LANGUAGES = ['te', 'km'];

View File

@ -0,0 +1,52 @@
import React, { useContext, useEffect } from 'react';
import { useIntl } from 'react-intl';
import Settings from '/imports/ui/services/settings';
import { Session } from 'meteor/session';
import { formatLocaleCode } from '/imports/utils/string-utils';
import Intl from '/imports/ui/services/locale';
import useCurrentLocale from '/imports/ui/core/local-states/useCurrentLocale';
import { LoadingContext } from '/imports/ui/components/common/loading-screen/loading-screen-HOC/component';
const RTL_LANGUAGES = ['ar', 'dv', 'fa', 'he'];
const LARGE_FONT_LANGUAGES = ['te', 'km'];
interface IntlAdapterProps {
children: React.ReactNode;
}
const IntlAdapter: React.FC<IntlAdapterProps> = ({
children,
}) => {
const [currentLocale] = useCurrentLocale();
const intl = useIntl();
const loadingContextInfo = useContext(LoadingContext);
const setUp = () => {
if (currentLocale) {
const { language, formattedLocale } = formatLocaleCode(currentLocale);
// @ts-ignore - JS code
Settings.application.locale = currentLocale;
Intl.setLocale(formattedLocale, intl.messages);
if (RTL_LANGUAGES.includes(currentLocale.substring(0, 2))) {
// @ts-ignore - JS code
document.body.parentNode.setAttribute('dir', 'rtl');
// @ts-ignore - JS code
Settings.application.isRTL = true;
} else {
// @ts-ignore - JS code
document.body.parentNode.setAttribute('dir', 'ltr');
// @ts-ignore - JS code
Settings.application.isRTL = false;
}
Session.set('isLargeFont', LARGE_FONT_LANGUAGES.includes(currentLocale.substring(0, 2)));
document.getElementsByTagName('html')[0].lang = formattedLocale;
document.body.classList.add(`lang-${language}`);
Settings.save();
}
};
useEffect(setUp, []);
useEffect(setUp, [currentLocale]);
return !loadingContextInfo.isLoading ? children : null;
};
export default IntlAdapter;

View File

@ -0,0 +1,151 @@
import React, { useCallback, useContext, useEffect } from 'react';
import { IntlProvider } from 'react-intl';
import { LoadingContext } from '/imports/ui/components/common/loading-screen/loading-screen-HOC/component';
import useCurrentLocale from '/imports/ui/core/local-states/useCurrentLocale';
import logger from './logger';
interface LocaleEndpointResponse {
defaultLocale: string;
fallbackOnEmptyLocaleString: boolean;
normalizedLocale: string;
regionDefaultLocale: string;
}
interface LocaleJson {
[key: string]: string;
}
interface IntlLoaderContainerProps {
children: React.ReactNode;
}
interface IntlLoaderProps extends IntlLoaderContainerProps {
currentLocale: string;
setCurrentLocale: (locale: string) => void;
}
const buildFetchLocale = (locale: string) => {
const localesPath = 'locales';
return new Promise((resolve) => {
fetch(`${localesPath}/${locale}.json`)
.then((response) => {
if (!response.ok) {
return resolve(false);
}
return response.json()
.then((jsonResponse) => resolve(jsonResponse))
.catch(() => {
logger.error({ logCode: 'intl_parse_locale_SyntaxError' }, `Could not parse locale file ${locale}.json, invalid json`);
resolve(false);
});
});
});
};
const IntlLoader: React.FC<IntlLoaderProps> = ({
children,
currentLocale,
setCurrentLocale,
}) => {
const loadingContextInfo = useContext(LoadingContext);
const [fetching, setFetching] = React.useState(false);
const [normalizedLocale, setNormalizedLocale] = React.useState(navigator.language.replace('_', '-'));
const [messages, setMessages] = React.useState<LocaleJson>({});
const [fallbackOnEmptyLocaleString, setFallbackOnEmptyLocaleString] = React.useState(false);
const fetchLocalizedMessages = useCallback((locale: string, init: boolean) => {
const url = `./locale?locale=${locale}&init=${init}`;
setFetching(true);
// fetch localized messages
fetch(url)
.then((response) => {
if (!response.ok) {
loadingContextInfo.setLoading(false, '');
throw new Error('unable to fetch localized messages');
}
return response.json();
}).then((data: LocaleEndpointResponse) => {
const {
defaultLocale,
regionDefaultLocale,
normalizedLocale,
fallbackOnEmptyLocaleString: FOEL,
} = data;
setFallbackOnEmptyLocaleString(FOEL);
const languageSets = Array.from(new Set([
defaultLocale,
regionDefaultLocale,
normalizedLocale,
])).filter((locale) => locale);
Promise.all(languageSets.map((locale) => buildFetchLocale(locale)))
.then((resp) => {
const typedResp = resp as Array<LocaleJson | boolean>;
const foundLocales = typedResp.filter((locale) => locale instanceof Object) as LocaleJson[];
if (foundLocales.length === 0) {
const error = `${{ logCode: 'intl_fetch_locale_error' }},Could not fetch any locale file for ${languageSets.join(', ')}`;
loadingContextInfo.setLoading(false, '');
logger.error(error);
throw new Error(error);
}
const mergedLocale = foundLocales
.reduce((acc, locale: LocaleJson) => Object.assign(acc, locale), {});
const replacedLocale = normalizedLocale.replace('_', '-');
setFetching(false);
setNormalizedLocale(replacedLocale);
setCurrentLocale(replacedLocale);
setMessages(mergedLocale);
if (!init) {
loadingContextInfo.setLoading(false, '');
}
}).catch((error) => {
loadingContextInfo.setLoading(false, '');
throw new Error(error);
});
});
}, []);
useEffect(() => {
const language = navigator.languages ? navigator.languages[0] : navigator.language;
fetchLocalizedMessages(language, true);
}, []);
useEffect(() => {
if (currentLocale !== normalizedLocale) {
fetchLocalizedMessages(currentLocale, false);
}
}, [currentLocale]);
useEffect(() => {
if (fetching) {
loadingContextInfo.setLoading(true, 'Fetching locale');
}
}, [fetching]);
return !fetching || Object.keys(messages).length > 0 ? (
<IntlProvider
fallbackOnEmptyString={fallbackOnEmptyLocaleString}
locale={normalizedLocale.replace('_', '-').replace('@', '-')}
messages={messages}
>
{children}
</IntlProvider>
) : null;
};
const IntlLoaderContainer: React.FC<IntlLoaderContainerProps> = ({
children,
}) => {
const [currentLocale, setCurrentLocale] = useCurrentLocale();
return (
<IntlLoader
currentLocale={currentLocale}
setCurrentLocale={setCurrentLocale}
>
{children}
</IntlLoader>
);
};
export default IntlLoaderContainer;

View File

@ -1,4 +1,3 @@
import Auth from '/imports/ui/services/auth';
import { Meteor } from 'meteor/meteor';
import { createLogger, stdSerializers } from 'browser-bunyan';
import { ConsoleFormattedStream } from '@browser-bunyan/console-formatted-stream';
@ -15,8 +14,43 @@ import { nameFromLevel } from '@browser-bunyan/levels';
// "url": "","method": ""}
// externalURL is the end-point that logs will be sent to
// Call the logger by doing a function call with the level name, I.e, logger.warn('Hi on warn')
const fallback = { console: { enabled: true, level: 'info' } };
const LOG_CONFIG = (JSON.parse(sessionStorage.getItem('clientStartupSettings') || '{}')?.clientLog) || fallback;
const LOG_CONFIG = Meteor.settings.public.clientLog || { console: { enabled: true, level: 'info' } };
export function createStreamForTarget(target, options) {
const TARGET_EXTERNAL = 'external';
const TARGET_CONSOLE = 'console';
const TARGET_SERVER = 'server';
let Stream = ConsoleRawStream;
switch (target) {
case TARGET_EXTERNAL:
Stream = ServerLoggerStream;
break;
case TARGET_CONSOLE:
Stream = ConsoleFormattedStream;
break;
case TARGET_SERVER:
Stream = MeteorStream;
break;
default:
Stream = ConsoleFormattedStream;
}
return new Stream(options);
}
export function generateLoggerStreams(config) {
let result = [];
Object.keys(config).forEach((key) => {
const logOption = config[key];
if (logOption && logOption.enabled) {
const { level, ...streamOptions } = logOption;
result = result.concat({ level, stream: createStreamForTarget(key, streamOptions) });
}
});
return result;
}
// Custom stream that logs to an end-point
class ServerLoggerStream extends ServerStream {
@ -26,16 +60,56 @@ class ServerLoggerStream extends ServerStream {
if (params.logTag) {
this.logTagString = params.logTag;
}
this.auth = null;
}
setConfig(config) {
const streams = generateLoggerStreams(config);
const { addStream } = this;
streams.forEach((stream) => {
addStream(stream);
});
}
setAuth(auth) {
this.auth = auth;
}
getUserData() {
let userInfo = {};
if (this.auth) {
userInfo = this.auth.fullInfo;
} else {
userInfo = {
meetingId: sessionStorage.getItem('meetingId'),
userId: sessionStorage.getItem('userId'),
logoutUrl: sessionStorage.getItem('logoutUrl'),
sessionToken: sessionStorage.getItem('sessionToken'),
userName: sessionStorage.getItem('userName'),
extId: sessionStorage.getItem('extId'),
meetingName: sessionStorage.getItem('meetingName'),
};
}
if (userInfo.meetingId) {
userInfo = {
sessionToken: sessionStorage.getItem('sessionToken'),
};
}
return {
fullInfo: userInfo,
};
}
write(rec) {
const { fullInfo } = Auth;
const { fullInfo } = this.getUserData();
this.rec = rec;
if (fullInfo.meetingId != null) {
this.rec.userInfo = fullInfo;
}
this.rec.clientBuild = Meteor.settings.public.app.html5ClientBuild;
this.rec.clientBuild = window.meetingClientSettings.public.app.html5ClientBuild;
this.rec.connectionId = Meteor.connection._lastSessionId;
if (this.logTagString) {
this.rec.logTag = this.logTagString;
@ -47,7 +121,7 @@ class ServerLoggerStream extends ServerStream {
// Custom stream to log to the meteor server
class MeteorStream {
write(rec) {
const { fullInfo } = Auth;
const { fullInfo } = this.getUserData();
const clientURL = window.location.href;
this.rec = rec;
@ -78,42 +152,6 @@ class MeteorStream {
}
}
function createStreamForTarget(target, options) {
const TARGET_EXTERNAL = 'external';
const TARGET_CONSOLE = 'console';
const TARGET_SERVER = 'server';
let Stream = ConsoleRawStream;
switch (target) {
case TARGET_EXTERNAL:
Stream = ServerLoggerStream;
break;
case TARGET_CONSOLE:
Stream = ConsoleFormattedStream;
break;
case TARGET_SERVER:
Stream = MeteorStream;
break;
default:
Stream = ConsoleFormattedStream;
}
return new Stream(options);
}
function generateLoggerStreams(config) {
let result = [];
Object.keys(config).forEach((key) => {
const logOption = config[key];
if (logOption && logOption.enabled) {
const { level, ...streamOptions } = logOption;
result = result.concat({ level, stream: createStreamForTarget(key, streamOptions) });
}
});
return result;
}
// Creates the logger with the array of streams of the chosen targets
const logger = createLogger({
name: 'clientLogger',
@ -122,5 +160,4 @@ const logger = createLogger({
src: true,
});
export default logger;

View File

@ -13,6 +13,10 @@ import { PrometheusAgent, METRIC_NAMES } from './prom-metrics/index.js'
let guestWaitHtml = '';
const DEFAULT_LANGUAGE = Meteor.settings.public.app.defaultSettings.application.fallbackLocale;
const CLIENT_VERSION = Meteor.settings.public.app.html5ClientBuild;
const FALLBACK_ON_EMPTY_STRING = Meteor.settings.public.app.fallbackOnEmptyLocaleString;
const env = Meteor.isDevelopment ? 'development' : 'production';
const meteorRoot = fs.realpathSync(`${process.cwd()}/../`);
@ -273,6 +277,8 @@ WebApp.connectHandlers.use('/locale', (req, res) => {
res.end(JSON.stringify({
normalizedLocale: localeFile,
regionDefaultLocale: (regionDefault && regionDefault !== localeFile) ? regionDefault : '',
defaultLocale: DEFAULT_LANGUAGE,
fallbackOnEmptyLocaleString: FALLBACK_ON_EMPTY_STRING,
}));
});

View File

@ -0,0 +1,847 @@
export interface MeetingClientSettings {
public: Public
private: Private
}
export interface Public {
app: App
externalVideoPlayer: ExternalVideoPlayer
kurento: Kurento
syncUsersWithConnectionManager: SyncUsersWithConnectionManager
poll: Poll
captions: Captions
timer: Timer
chat: Chat
userReaction: UserReaction
userStatus: UserStatus
notes: Notes
layout: Layout
pads: Pads
media: Media
stats: Stats
presentation: Presentation
selectRandomUser: SelectRandomUser
user: User
whiteboard: Whiteboard
clientLog: ClientLog
virtualBackgrounds: VirtualBackgrounds
}
export interface App {
instanceId: string
mobileFontSize: string
desktopFontSize: string
audioChatNotification: boolean
autoJoin: boolean
listenOnlyMode: boolean
forceListenOnly: boolean
skipCheck: boolean
skipCheckOnJoin: boolean
enableDynamicAudioDeviceSelection: boolean
clientTitle: string
appName: string
bbbServerVersion: string
displayBbbServerVersion: boolean
copyright: string
html5ClientBuild: string
helpLink: string
delayForUnmountOfSharedNote: number
bbbTabletApp: BbbTabletApp
lockOnJoin: boolean
cdn: string
basename: string
bbbWebBase: string
learningDashboardBase: string
customStyleUrl: string | null
darkTheme: DarkTheme
askForFeedbackOnLogout: boolean
askForConfirmationOnLeave: boolean
wakeLock: WakeLock
allowDefaultLogoutUrl: boolean
allowUserLookup: boolean
dynamicGuestPolicy: boolean
enableGuestLobbyMessage: boolean
guestPolicyExtraAllowOptions: boolean
alwaysShowWaitingRoomUI: boolean
enableLimitOfViewersInWebcam: boolean
enableMultipleCameras: boolean
enableCameraAsContent: boolean
enableWebcamSelectorButton: boolean
enableTalkingIndicator: boolean
enableCameraBrightness: boolean
mirrorOwnWebcam: boolean
viewersInWebcam: number
ipv4FallbackDomain: string
allowLogout: boolean
allowFullscreen: boolean
preloadNextSlides: number
warnAboutUnsavedContentOnMeetingEnd: boolean
audioCaptions: AudioCaptions
mutedAlert: MutedAlert
remainingTimeThreshold: number
remainingTimeAlertThresholdArray: number[]
enableDebugWindow: boolean
breakouts: Breakouts
customHeartbeat: boolean
showAllAvailableLocales: boolean
showAudioFilters: boolean
raiseHandActionButton: RaiseHandActionButton
reactionsButton: ReactionsButton
emojiRain: EmojiRain
enableNetworkStats: boolean
enableCopyNetworkStatsButton: boolean
userSettingsStorage: string
defaultSettings: DefaultSettings
shortcuts: Shortcuts
branding: Branding
connectionTimeout: number
showHelpButton: boolean
effectiveConnection: string[]
fallbackOnEmptyLocaleString: boolean
disableWebsocketFallback: boolean
}
export interface BbbTabletApp {
enabled: boolean
iosAppStoreUrl: string
iosAppUrlScheme: string
}
export interface DarkTheme {
enabled: boolean
}
export interface WakeLock {
enabled: boolean
}
export interface AudioCaptions {
enabled: boolean
mobile: boolean
provider: string
language: Language
}
export interface Language {
available: string[]
forceLocale: boolean
defaultSelectLocale: boolean
locale: string
}
export interface MutedAlert {
enabled: boolean
interval: number
threshold: number
duration: number
}
export interface Breakouts {
allowUserChooseRoomByDefault: boolean
captureWhiteboardByDefault: boolean
captureSharedNotesByDefault: boolean
sendInvitationToAssignedModeratorsByDefault: boolean
breakoutRoomLimit: number
}
export interface RaiseHandActionButton {
enabled: boolean
centered: boolean
}
export interface ReactionsButton {
enabled: boolean
}
export interface EmojiRain {
enabled: boolean
intervalEmojis: number
numberOfEmojis: number
emojiSize: number
}
export interface DefaultSettings {
application: Application
audio: Audio
dataSaving: DataSaving
}
export interface Application {
selectedLayout: string
animations: boolean
chatAudioAlerts: boolean
chatPushAlerts: boolean
userJoinAudioAlerts: boolean
userJoinPushAlerts: boolean
userLeaveAudioAlerts: boolean
userLeavePushAlerts: boolean
raiseHandAudioAlerts: boolean
raiseHandPushAlerts: boolean
guestWaitingAudioAlerts: boolean
guestWaitingPushAlerts: boolean
wakeLock: boolean
paginationEnabled: boolean
whiteboardToolbarAutoHide: boolean
autoCloseReactionsBar: boolean
darkTheme: boolean
fallbackLocale: string
overrideLocale: string | null
}
export interface Audio {
inputDeviceId: string
outputDeviceId: string
}
export interface DataSaving {
viewParticipantsWebcams: boolean
viewScreenshare: boolean
}
export interface Shortcuts {
openOptions: OpenOptions
toggleUserList: ToggleUserList
toggleMute: ToggleMute
joinAudio: JoinAudio
leaveAudio: LeaveAudio
togglePublicChat: TogglePublicChat
hidePrivateChat: HidePrivateChat
closePrivateChat: ClosePrivateChat
raiseHand: RaiseHand
openActions: OpenActions
openDebugWindow: OpenDebugWindow
}
export interface OpenOptions {
accesskey: string
descId: string
}
export interface ToggleUserList {
accesskey: string
descId: string
}
export interface ToggleMute {
accesskey: string
descId: string
}
export interface JoinAudio {
accesskey: string
descId: string
}
export interface LeaveAudio {
accesskey: string
descId: string
}
export interface TogglePublicChat {
accesskey: string
descId: string
}
export interface HidePrivateChat {
accesskey: string
descId: string
}
export interface ClosePrivateChat {
accesskey: string
descId: string
}
export interface RaiseHand {
accesskey: string
descId: string
}
export interface OpenActions {
accesskey: string
descId: string
}
export interface OpenDebugWindow {
accesskey: string
descId: string
}
export interface Branding {
displayBrandingArea: boolean
}
export interface ExternalVideoPlayer {
enabled: boolean
}
export interface Kurento {
wsUrl: string
cameraWsOptions: CameraWsOptions
gUMTimeout: number
signalCandidates: boolean
traceLogs: boolean
cameraTimeouts: CameraTimeouts
screenshare: Screenshare
cameraProfiles: CameraProfile[]
enableScreensharing: boolean
enableVideo: boolean
enableVideoMenu: boolean
enableVideoPin: boolean
autoShareWebcam: boolean
skipVideoPreview: boolean
skipVideoPreviewOnFirstJoin: boolean
cameraSortingModes: CameraSortingModes
cameraQualityThresholds: CameraQualityThresholds
pagination: Pagination
paginationThresholds: PaginationThresholds
}
export interface CameraWsOptions {
wsConnectionTimeout: number
maxRetries: number
debug: boolean
heartbeat: Heartbeat
}
export interface Heartbeat {
interval: number
delay: number
reconnectOnFailure: boolean
}
export interface CameraTimeouts {
baseTimeout: number
maxTimeout: number
}
export interface Screenshare {
enableVolumeControl: boolean
subscriberOffering: boolean
bitrate: number
mediaTimeouts: MediaTimeouts
constraints: Constraints
}
export interface MediaTimeouts {
maxConnectionAttempts: number
baseTimeout: number
baseReconnectionTimeout: number
maxTimeout: number
timeoutIncreaseFactor: number
}
export interface Constraints {
video: Video
audio: boolean
}
export interface Video {
frameRate: FrameRate
width: Width
height: Height
}
export interface FrameRate {
ideal: number
max: number
}
export interface Width {
max: number
}
export interface Height {
max: number
}
export interface CameraProfile {
id: string
name: string
bitrate: number
hidden?: boolean
default?: boolean
constraints?: Constraints2
}
export interface Constraints2 {
width: number
height: number
frameRate?: number
}
export interface CameraSortingModes {
defaultSorting: string
paginationSorting: string
}
export interface CameraQualityThresholds {
enabled: boolean
applyConstraints: boolean
privilegedStreams: boolean
debounceTime: number
thresholds: Threshold[]
}
export interface Threshold {
threshold: number
profile: string
}
export interface Pagination {
paginationToggleEnabled: boolean
pageChangeDebounceTime: number
desktopPageSizes: DesktopPageSizes
mobilePageSizes: MobilePageSizes
desktopGridSizes: DesktopGridSizes
mobileGridSizes: MobileGridSizes
}
export interface DesktopPageSizes {
moderator: number
viewer: number
}
export interface MobilePageSizes {
moderator: number
viewer: number
}
export interface DesktopGridSizes {
moderator: number
viewer: number
}
export interface MobileGridSizes {
moderator: number
viewer: number
}
export interface PaginationThresholds {
enabled: boolean
thresholds: Threshold2[]
}
export interface Threshold2 {
users: number
desktopPageSizes: DesktopPageSizes2
}
export interface DesktopPageSizes2 {
moderator: number
viewer: number
}
export interface SyncUsersWithConnectionManager {
enabled: boolean
syncInterval: number
}
export interface Poll {
enabled: boolean
allowCustomResponseInput: boolean
maxCustom: number
maxTypedAnswerLength: number
chatMessage: boolean
}
export interface Captions {
enabled: boolean
id: string
dictation: boolean
background: string
font: Font
lines: number
time: number
}
export interface Font {
color: string
family: string
size: string
}
export interface Timer {
enabled: boolean
alarm: boolean
music: Music
interval: Interval
time: number
tabIndicator: boolean
}
export interface Music {
enabled: boolean
volume: number
track1: string
track2: string
track3: string
}
export interface Interval {
clock: number
offset: number
}
export interface Chat {
enabled: boolean
itemsPerPage: number
timeBetweenFetchs: number
enableSaveAndCopyPublicChat: boolean
bufferChatInsertsMs: number
startClosed: boolean
min_message_length: number
max_message_length: number
grouping_messages_window: number
type_system: string
type_public: string
type_private: string
system_userid: string
system_username: string
public_id: string
public_group_id: string
public_userid: string
public_username: string
storage_key: string
system_messages_keys: SystemMessagesKeys
typingIndicator: TypingIndicator
moderatorChatEmphasized: boolean
autoConvertEmoji: boolean
emojiPicker: EmojiPicker
disableEmojis: string[]
allowedElements: string[]
}
export interface SystemMessagesKeys {
chat_clear: string
chat_poll_result: string
chat_exported_presentation: string
chat_status_message: string
}
export interface TypingIndicator {
enabled: boolean
showNames: boolean
}
export interface EmojiPicker {
enable: boolean
}
export interface UserReaction {
enabled: boolean
expire: number
reactions: Reaction[]
}
export interface Reaction {
id: string
native: string
}
export interface UserStatus {
enabled: boolean
}
export interface Notes {
enabled: boolean
id: string
pinnable: boolean
}
export interface Layout {
hidePresentationOnJoin: boolean
showParticipantsOnLogin: boolean
showPushLayoutButton: boolean
showPushLayoutToggle: boolean
}
export interface Pads {
url: string
cookie: Cookie
}
export interface Cookie {
path: string
sameSite: string
secure: boolean
}
export interface Media {
audio: Audio2
stunTurnServersFetchAddress: string
cacheStunTurnServers: boolean
fallbackStunServer: string
forceRelay: boolean
forceRelayOnFirefox: boolean
mediaTag: string
callTransferTimeout: number
callHangupTimeout: number
callHangupMaximumRetries: number
echoTestNumber: string
listenOnlyCallTimeout: number
transparentListenOnly: boolean
fullAudioOffering: boolean
listenOnlyOffering: boolean
iceGatheringTimeout: number
audioConnectionTimeout: number
audioReconnectionDelay: number
audioReconnectionAttempts: number
sipjsHackViaWs: boolean
sipjsAllowMdns: boolean
sip_ws_host: string
toggleMuteThrottleTime: number
websocketKeepAliveInterval: number
websocketKeepAliveDebounce: number
traceSip: boolean
sdpSemantics: string
localEchoTest: LocalEchoTest
showVolumeMeter: boolean
}
export interface Audio2 {
defaultFullAudioBridge: string
defaultListenOnlyBridge: string
bridges: Bridge[]
retryThroughRelay: boolean
}
export interface Bridge {
name: string
path: string
}
export interface LocalEchoTest {
enabled: boolean
initialHearingState: boolean
useRtcLoopbackInChromium: boolean
delay: Delay
}
export interface Delay {
enabled: boolean
delayTime: number
maxDelayTime: number
}
export interface Stats {
enabled: boolean
interval: number
timeout: number
log: boolean
notification: Notification
jitter: number[]
loss: number[]
rtt: number[]
level: string[]
help: string
}
export interface Notification {
warning: boolean
error: boolean
}
export interface Presentation {
allowDownloadOriginal: boolean
allowDownloadWithAnnotations: boolean
allowSnapshotOfCurrentSlide: boolean
panZoomThrottle: number
restoreOnUpdate: boolean
uploadEndpoint: string
fileUploadConstraintsHint: boolean
mirroredFromBBBCore: MirroredFromBbbcore
uploadValidMimeTypes: UploadValidMimeType[]
}
export interface MirroredFromBbbcore {
uploadSizeMax: number
uploadPagesMax: number
}
export interface UploadValidMimeType {
extension: string
mime: string
}
export interface SelectRandomUser {
enabled: boolean
countdown: boolean
}
export interface User {
role_moderator: string
role_viewer: string
label: Label
}
export interface Label {
moderator: boolean
mobile: boolean
guest: boolean
sharingWebcam: boolean
}
export interface Whiteboard {
annotationsQueueProcessInterval: number
cursorInterval: number
pointerDiameter: number
maxStickyNoteLength: number
maxNumberOfAnnotations: number
annotations: Annotations
styles: Styles
toolbar: Toolbar
}
export interface Annotations {
status: Status
}
export interface Status {
start: string
update: string
end: string
}
export interface Styles {
text: Text
}
export interface Text {
family: string
}
export interface Toolbar {
multiUserPenOnly: boolean
colors: Color[]
thickness: Thickness[]
font_sizes: FontSize[]
tools: Tool[]
presenterTools: string[]
multiUserTools: string[]
}
export interface Color {
label: string
value: string
}
export interface Thickness {
value: number
}
export interface FontSize {
value: number
}
export interface Tool {
icon: string
value: string
}
export interface ClientLog {
server: Server
console: Console
external: External
}
export interface Server {
enabled: boolean
level: string
}
export interface Console {
enabled: boolean
level: string
}
export interface External {
enabled: boolean
level: string
url: string
method: string
throttleInterval: number
flushOnClose: boolean
logTag: string
}
export interface VirtualBackgrounds {
enabled: boolean
enableVirtualBackgroundUpload: boolean
storedOnBBB: boolean
showThumbnails: boolean
imagesPath: string
thumbnailsPath: string
fileNames: string[]
}
export interface Private {
analytics: Analytics
app: App2
redis: Redis
serverLog: ServerLog
minBrowserVersions: MinBrowserVersion[]
prometheus: Prometheus
}
export interface Analytics {
includeChat: boolean
}
export interface App2 {
host: string
localesUrl: string
pencilChunkLength: number
loadSlidesFromHttpAlways: boolean
}
export interface Redis {
host: string
port: string
timeout: number
password: string | null
debug: boolean
metrics: Metrics
channels: Channels
subscribeTo: string[]
async: string[]
ignored: string[]
}
export interface Metrics {
queueMetrics: boolean
metricsDumpIntervalMs: number
metricsFolderPath: string
removeMeetingOnEnd: boolean
}
export interface Channels {
toAkkaApps: string
toThirdParty: string
}
export interface ServerLog {
level: string
streamerLog: boolean
includeServerInfo: boolean
healthChecker: HealthChecker
}
export interface HealthChecker {
enable: boolean
intervalMs: number
}
export interface MinBrowserVersion {
browser: string
version: number | number[] | string
}
export interface Prometheus {
enabled: boolean
path: string
collectDefaultMetrics: boolean
collectRedisMetrics: boolean
}
export default MeetingClientSettings;

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

@ -14,7 +14,7 @@ const AboutContainer = (props) => {
const getClientBuildInfo = () => (
{
settings: Meteor.settings.public.app,
settings: window.meetingClientSettings.public.app,
}
);

View File

@ -77,13 +77,13 @@ const ActionsBarContainer = (props) => {
);
};
const SELECT_RANDOM_USER_ENABLED = Meteor.settings.public.selectRandomUser.enabled;
const RAISE_HAND_BUTTON_ENABLED = Meteor.settings.public.app.raiseHandActionButton.enabled;
const RAISE_HAND_BUTTON_CENTERED = Meteor.settings.public.app.raiseHandActionButton.centered;
const SELECT_RANDOM_USER_ENABLED = window.meetingClientSettings.public.selectRandomUser.enabled;
const RAISE_HAND_BUTTON_ENABLED = window.meetingClientSettings.public.app.raiseHandActionButton.enabled;
const RAISE_HAND_BUTTON_CENTERED = window.meetingClientSettings.public.app.raiseHandActionButton.centered;
const isReactionsButtonEnabled = () => {
const USER_REACTIONS_ENABLED = Meteor.settings.public.userReaction.enabled;
const REACTIONS_BUTTON_ENABLED = Meteor.settings.public.app.reactionsButton.enabled;
const USER_REACTIONS_ENABLED = window.meetingClientSettings.public.userReaction.enabled;
const REACTIONS_BUTTON_ENABLED = window.meetingClientSettings.public.app.reactionsButton.enabled;
return USER_REACTIONS_ENABLED && REACTIONS_BUTTON_ENABLED;
};

View File

@ -7,7 +7,7 @@ import { PANELS, ACTIONS } from '../../layout/enums';
import { uniqueId, safeMatch } from '/imports/utils/string-utils';
import PollService from '/imports/ui/components/poll/service';
const POLL_SETTINGS = Meteor.settings.public.poll;
const POLL_SETTINGS = window.meetingClientSettings.public.poll;
const MAX_CUSTOM_FIELDS = POLL_SETTINGS.maxCustom;
const MAX_CHAR_LIMIT = POLL_SETTINGS.maxTypedAnswerLength;
const CANCELED_POLL_DELAY = 250;

View File

@ -11,7 +11,7 @@ import { useMutation } from '@apollo/client';
import Styled from './styles';
const REACTIONS = Meteor.settings.public.userReaction.reactions;
const REACTIONS = window.meetingClientSettings.public.userReaction.reactions;
const ReactionsButton = (props) => {
const {

View File

@ -81,7 +81,7 @@ class ActivityCheck extends Component {
}
playAudioAlert() {
this.alert = new Audio(`${Meteor.settings.public.app.cdn + Meteor.settings.public.app.basename + Meteor.settings.public.app.instanceId}/resources/sounds/notify.mp3`);
this.alert = new Audio(`${window.meetingClientSettings.public.app.cdn + window.meetingClientSettings.public.app.basename + window.meetingClientSettings.public.app.instanceId}/resources/sounds/notify.mp3`);
this.alert.addEventListener('ended', () => { this.alert.src = null; });
this.alert.play();
}

View File

@ -57,11 +57,11 @@ import FloatingWindowContainer from '/imports/ui/components/floating-window/cont
import ChatAlertContainerGraphql from '../chat/chat-graphql/alert/component';
const MOBILE_MEDIA = 'only screen and (max-width: 40em)';
const APP_CONFIG = Meteor.settings.public.app;
const APP_CONFIG = window.meetingClientSettings.public.app;
const DESKTOP_FONT_SIZE = APP_CONFIG.desktopFontSize;
const MOBILE_FONT_SIZE = APP_CONFIG.mobileFontSize;
const LAYOUT_CONFIG = Meteor.settings.public.layout;
const CONFIRMATION_ON_LEAVE = Meteor.settings.public.app.askForConfirmationOnLeave;
const LAYOUT_CONFIG = window.meetingClientSettings.public.layout;
const CONFIRMATION_ON_LEAVE = window.meetingClientSettings.public.app.askForConfirmationOnLeave;
const intlMessages = defineMessages({
userListLabel: {

View File

@ -37,7 +37,7 @@ import App from './component';
import useToggleVoice from '../audio/audio-graphql/hooks/useToggleVoice';
import useUserChangedLocalSettings from '../../services/settings/hooks/useUserChangedLocalSettings';
const CUSTOM_STYLE_URL = Meteor.settings.public.app.customStyleUrl;
const CUSTOM_STYLE_URL = window.meetingClientSettings.public.app.customStyleUrl;
const endMeeting = (code, ejectedReason) => {
Session.set('codeError', code);
@ -339,7 +339,7 @@ export default withTracker(() => {
customStyleUrl = CUSTOM_STYLE_URL;
}
const LAYOUT_CONFIG = Meteor.settings.public.layout;
const LAYOUT_CONFIG = window.meetingClientSettings.public.layout;
return {
captions: CaptionsService.isCaptionsActive() ? <CaptionsContainer /> : null,
@ -376,7 +376,7 @@ export default withTracker(() => {
isLargeFont: Session.get('isLargeFont'),
presentationRestoreOnUpdate: getFromUserSettings(
'bbb_force_restore_presentation_on_new_events',
Meteor.settings.public.presentation.restoreOnUpdate,
window.meetingClientSettings.public.presentation.restoreOnUpdate,
),
hidePresentationOnJoin: getFromUserSettings('bbb_hide_presentation_on_join', LAYOUT_CONFIG.hidePresentationOnJoin),
hideActionsBar: getFromUserSettings('bbb_hide_actions_bar', false),

View File

@ -5,12 +5,13 @@ import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
import ButtonEmoji from '/imports/ui/components/common/button/button-emoji/ButtonEmoji';
import BBBMenu from '/imports/ui/components/common/menu/component';
import Styled from './styles';
import { getSpeechVoices, setAudioCaptions, setSpeechLocale } from '../service';
import {
getSpeechVoices, isAudioTranscriptionEnabled, setAudioCaptions, setSpeechLocale,
} from '../service';
import { defineMessages, useIntl } from 'react-intl';
import { MenuSeparatorItemType, MenuOptionItemType } from '/imports/ui/components/common/menu/menuTypes';
import useAudioCaptionEnable from '/imports/ui/core/local-states/useAudioCaptionEnable';
import { User } from '/imports/ui/Types/user';
import useMeeting from '/imports/ui/core/hooks/useMeeting';
import { useMutation } from '@apollo/client';
import { SET_SPEECH_LOCALE } from '/imports/ui/core/graphql/mutations/userMutations';
@ -260,27 +261,15 @@ const AudioCaptionsButtonContainer: React.FC = () => {
}),
);
const {
data: currentMeeting,
loading: componentsFlagsLoading,
} = useMeeting((m) => {
return {
componentsFlags: m.componentsFlags,
};
});
if (currentUserLoading || componentsFlagsLoading) return null;
if (!currentUser || !currentMeeting) return null;
if (currentUserLoading) return null;
if (!currentUser) return null;
const availableVoices = getSpeechVoices();
const currentSpeechLocale = currentUser.speechLocale || '';
const isSupported = availableVoices.length > 0;
const isVoiceUser = !!currentUser.voice;
const { componentsFlags } = currentMeeting;
const hasCaptions = componentsFlags?.hasCaption;
if (!hasCaptions) return null;
if (!isAudioTranscriptionEnabled()) return null;
return (
<AudioCaptionsButton

View File

@ -21,9 +21,9 @@ import ListenOnly from './buttons/listenOnly';
import LiveSelection from './buttons/LiveSelection';
// @ts-ignore - temporary, while meteor exists in the project
const { enableDynamicAudioDeviceSelection } = Meteor.settings.public.app;
const { enableDynamicAudioDeviceSelection } = window.meetingClientSettings.public.app;
// @ts-ignore - temporary, while meteor exists in the project
const MUTE_ALERT_CONFIG = Meteor.settings.public.app.mutedAlert;
const MUTE_ALERT_CONFIG = window.meetingClientSettings.public.app.mutedAlert;
// @ts-ignore - temporary while settings are still in .js
// eslint-disable-next-line @typescript-eslint/no-unused-vars

View File

@ -7,9 +7,9 @@ import AudioManager from '/imports/ui/services/audio-manager';
const MUTED_KEY = 'muted';
// @ts-ignore - temporary, while meteor exists in the project
const APP_CONFIG = Meteor.settings.public.app;
const APP_CONFIG = window.meetingClientSettings.public.app;
// @ts-ignore - temporary, while meteor exists in the project
const TOGGLE_MUTE_THROTTLE_TIME = Meteor.settings.public.media.toggleMuteThrottleTime;
const TOGGLE_MUTE_THROTTLE_TIME = window.meetingClientSettings.public.media.toggleMuteThrottleTime;
const DEVICE_LABEL_MAX_LENGTH = 40;
const CLIENT_DID_USER_SELECTED_MICROPHONE_KEY = 'clientUserSelectedMicrophone';
const CLIENT_DID_USER_SELECTED_LISTEN_ONLY_KEY = 'clientUserSelectedListenOnly';

View File

@ -19,7 +19,7 @@ import Service from '../service';
const AudioModalContainer = (props) => <AudioModal {...props} />;
const APP_CONFIG = Meteor.settings.public.app;
const APP_CONFIG = window.meetingClientSettings.public.app;
const invalidDialNumbers = ['0', '613-555-1212', '613-555-1234', '0000'];
const isRTL = document.documentElement.getAttribute('dir') === 'rtl';

View File

@ -8,7 +8,7 @@ const AudioTestContainer = (props) => <AudioTest {...props} />;
export default withTracker(() => ({
outputDeviceId: Service.outputDeviceId(),
handlePlayAudioSample: (deviceId) => {
const sound = new Audio(`${Meteor.settings.public.app.cdn + Meteor.settings.public.app.basename + Meteor.settings.public.app.instanceId}/resources/sounds/audioSample.mp3`);
const sound = new Audio(`${window.meetingClientSettings.public.app.cdn + window.meetingClientSettings.public.app.basename + window.meetingClientSettings.public.app.instanceId}/resources/sounds/audioSample.mp3`);
sound.addEventListener('ended', () => { sound.src = null; });
if (deviceId && sound.setSinkId) sound.setSinkId(deviceId);
sound.play();

View File

@ -0,0 +1,104 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import UserContainer from './user/container';
const CAPTIONS_CONFIG = window.meetingClientSettings.public.captions;
class LiveCaptions extends PureComponent {
constructor(props) {
super(props);
this.state = { clear: true };
this.timer = null;
}
componentDidUpdate(prevProps) {
const { clear } = this.state;
if (clear) {
const { transcript } = this.props;
if (prevProps.transcript !== transcript) {
// eslint-disable-next-line react/no-did-update-set-state
this.setState({ clear: false });
}
} else {
this.resetTimer();
this.timer = setTimeout(() => this.setState({ clear: true }), CAPTIONS_CONFIG.time);
}
}
componentWillUnmount() {
this.resetTimer();
}
resetTimer() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
render() {
const {
transcript,
transcriptId,
} = this.props;
const { clear } = this.state;
const hasContent = transcript.length > 0 && !clear;
const wrapperStyles = {
display: 'flex',
};
const captionStyles = {
whiteSpace: 'pre-line',
wordWrap: 'break-word',
fontFamily: 'Verdana, Arial, Helvetica, sans-serif',
fontSize: '1.5rem',
background: '#000000a0',
color: 'white',
padding: hasContent ? '.5rem' : undefined,
};
const visuallyHidden = {
position: 'absolute',
overflow: 'hidden',
clip: 'rect(0 0 0 0)',
height: '1px',
width: '1px',
margin: '-1px',
padding: '0',
border: '0',
};
return (
<div style={wrapperStyles}>
{clear ? null : (
<UserContainer
background="#000000a0"
transcriptId={transcriptId}
/>
)}
<div style={captionStyles}>
{clear ? '' : transcript}
</div>
<div
style={visuallyHidden}
aria-atomic
aria-live="polite"
>
{clear ? '' : transcript}
</div>
</div>
);
}
}
LiveCaptions.propTypes = {
transcript: PropTypes.string.isRequired,
transcriptId: PropTypes.string.isRequired,
};
export default LiveCaptions;

View File

@ -0,0 +1,44 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import Users from '/imports/api/users';
import User from './component';
const MODERATOR = window.meetingClientSettings.public.user.role_moderator;
const Container = (props) => <User {...props} />;
const getUser = (userId) => {
const user = Users.findOne(
{ userId },
{
fields: {
avatar: 1,
color: 1,
role: 1,
name: 1,
},
},
);
if (user) {
return {
avatar: user.avatar,
color: user.color,
moderator: user.role === MODERATOR,
name: user.name,
};
}
return {
avatar: '',
color: '',
moderator: false,
name: '',
};
};
export default withTracker(({ transcriptId }) => {
const userId = transcriptId.split('-')[0];
return getUser(userId);
})(Container);

View File

@ -7,7 +7,7 @@ import deviceInfo from '/imports/utils/deviceInfo';
import { isLiveTranscriptionEnabled } from '/imports/ui/services/features';
import { unique } from 'radash';
const CONFIG = Meteor.settings.public.app.audioCaptions;
const CONFIG = window.meetingClientSettings.public.app.audioCaptions;
const ENABLED = CONFIG.enabled;
const PROVIDER = CONFIG.provider;
const LANGUAGES = CONFIG.language.available;

View File

@ -25,8 +25,8 @@ import Settings from '/imports/ui/services/settings';
import useToggleVoice from './audio-graphql/hooks/useToggleVoice';
import { usePreviousValue } from '/imports/ui/components/utils/hooks';
const APP_CONFIG = Meteor.settings.public.app;
const KURENTO_CONFIG = Meteor.settings.public.kurento;
const APP_CONFIG = window.meetingClientSettings.public.app;
const KURENTO_CONFIG = window.meetingClientSettings.public.kurento;
const intlMessages = defineMessages({
joinedAudio: {

View File

@ -1,13 +1,13 @@
import LocalPCLoopback from '/imports/ui/services/webrtc-base/local-pc-loopback';
import browserInfo from '/imports/utils/browserInfo';
const MEDIA_TAG = Meteor.settings.public.media.mediaTag;
const USE_RTC_LOOPBACK_CHR = Meteor.settings.public.media.localEchoTest.useRtcLoopbackInChromium;
const MEDIA_TAG = window.meetingClientSettings.public.media.mediaTag;
const USE_RTC_LOOPBACK_CHR = window.meetingClientSettings.public.media.localEchoTest.useRtcLoopbackInChromium;
const {
enabled: DELAY_ENABLED = true,
delayTime = 0.5,
maxDelayTime = 2,
} = Meteor.settings.public.media.localEchoTest.delay;
} = window.meetingClientSettings.public.media.localEchoTest.delay;
let audioContext = null;
let sourceContext = null;

View File

@ -8,13 +8,13 @@ import VoiceUsers from '/imports/api/voice-users';
import logger from '/imports/startup/client/logger';
import Storage from '../../services/storage/session';
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
const TOGGLE_MUTE_THROTTLE_TIME = Meteor.settings.public.media.toggleMuteThrottleTime;
const SHOW_VOLUME_METER = Meteor.settings.public.media.showVolumeMeter;
const ROLE_MODERATOR = window.meetingClientSettings.public.user.role_moderator;
const TOGGLE_MUTE_THROTTLE_TIME = window.meetingClientSettings.public.media.toggleMuteThrottleTime;
const SHOW_VOLUME_METER = window.meetingClientSettings.public.media.showVolumeMeter;
const {
enabled: LOCAL_ECHO_TEST_ENABLED,
initialHearingState: LOCAL_ECHO_INIT_HEARING_STATE,
} = Meteor.settings.public.media.localEchoTest;
} = window.meetingClientSettings.public.media.localEchoTest;
const MUTED_KEY = 'muted';
@ -73,7 +73,7 @@ const init = (messages, intl, toggleVoice) => {
const muteMicrophone = (toggleVoice) => {
const user = VoiceUsers.findOne({
meetingId: Auth.meetingID, intId: Auth.userID,
userId: Auth.userID,
}, { fields: { muted: 1 } });
if (!user.muted) {
@ -87,14 +87,14 @@ const muteMicrophone = (toggleVoice) => {
};
const isVoiceUser = () => {
const voiceUser = VoiceUsers.findOne({ intId: Auth.userID },
const voiceUser = VoiceUsers.findOne({ userId: Auth.userID },
{ fields: { joined: 1 } });
return voiceUser ? voiceUser.joined : false;
};
const toggleMuteMicrophone = throttle((toggleVoice) => {
const user = VoiceUsers.findOne({
meetingId: Auth.meetingID, intId: Auth.userID,
userId: Auth.userID,
}, { fields: { muted: 1 } });
Storage.setItem(MUTED_KEY, !user.muted);

View File

@ -38,12 +38,10 @@ class AuthenticatedHandler extends Component {
if (Auth.loggedIn) {
callback();
}
AuthenticatedHandler.addReconnectObservable();
const setReason = (reason) => {
const log = reason.error === 403 ? 'warn' : 'error';
logger[log]({
logCode: 'authenticatedhandlercomponent_setreason',
extraInfo: { reason },
@ -70,6 +68,7 @@ class AuthenticatedHandler extends Component {
componentDidMount() {
if (Session.get('codeError')) {
console.log('Session.get(codeError)', Session.get('codeError'));
this.setState({ authenticated: true });
}
AuthenticatedHandler.authenticatedRouteHandler((value, error) => {

View File

@ -0,0 +1,44 @@
import React, { useEffect } from 'react';
import useAuthData from '/imports/ui/core/local-states/useAuthData';
import Auth from '/imports/ui/services/auth';
import { Session } from 'meteor/session';
interface PresenceAdapterProps {
children: React.ReactNode;
}
const PresenceAdapter: React.FC<PresenceAdapterProps> = ({ children }) => {
const [authData] = useAuthData();
const [authSetted, setAuthSetted] = React.useState(false);
useEffect(() => {
const {
authToken,
logoutUrl,
meetingId,
sessionToken,
userId,
userName,
extId,
meetingName,
} = authData;
Auth.clearCredentials();
Auth.set(
meetingId,
userId,
authToken,
logoutUrl,
sessionToken,
userName,
extId,
meetingName,
);
Auth.loggedIn = true;
Auth.connectionAuthTime = new Date().getTime();
Session.set('userWillAuth', false);
setAuthSetted(true);
}, []);
return authSetted ? children : null;
};
export default PresenceAdapter;

View File

@ -2,7 +2,6 @@ import React, { useMemo } from 'react';
import ModalFullscreen from '/imports/ui/components/common/modal/fullscreen/component';
import { defineMessages, useIntl } from 'react-intl';
import { range } from 'ramda';
import { Meteor } from 'meteor/meteor';
import { uniqueId } from '/imports/utils/string-utils';
import { isImportPresentationWithAnnotationsFromBreakoutRoomsEnabled, isImportSharedNotesFromBreakoutRoomsEnabled } from '/imports/ui/services/features';
import useMeeting from '/imports/ui/core/hooks/useMeeting';
@ -27,7 +26,7 @@ import {
} from './room-managment-state/types';
import { BREAKOUT_ROOM_CREATE, BREAKOUT_ROOM_MOVE_USER } from '../../mutations';
const BREAKOUT_LIM = Meteor.settings.public.app.breakouts.breakoutRoomLimit;
const BREAKOUT_LIM = window.meetingClientSettings.public.app.breakouts.breakoutRoomLimit;
const MIN_BREAKOUT_ROOMS = 2;
const MAX_BREAKOUT_ROOMS = BREAKOUT_LIM > MIN_BREAKOUT_ROOMS ? BREAKOUT_LIM : MIN_BREAKOUT_ROOMS;
const MIN_BREAKOUT_TIME = 5;
@ -315,7 +314,7 @@ const CreateBreakoutRoom: React.FC<CreateBreakoutRoomProps> = ({
moveUser({
variables: {
userId,
fromBreakoutRoomId: fromRoomId,
fromBreakoutRoomId: fromRoomId || '',
toBreakoutRoomId: toRoomId,
},
});

View File

@ -4,7 +4,7 @@ import MessageForm from './component';
import ChatService from '/imports/ui/components/chat/service';
import { BREAKOUT_ROOM_SEND_MESSAGE_TO_ALL } from '../mutations';
const CHAT_CONFIG = Meteor.settings.public.chat;
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
const MessageFormContainer = (props) => {
const [sendMessageToAllBreakouts] = useMutation(BREAKOUT_ROOM_SEND_MESSAGE_TO_ALL);

View File

@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import Service from '/imports/ui/components/captions/service';
const CAPTIONS_CONFIG = Meteor.settings.public.captions;
const CAPTIONS_CONFIG = window.meetingClientSettings.public.captions;
class LiveCaptions extends PureComponent {
constructor(props) {

View File

@ -7,7 +7,7 @@ import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
import { isCaptionsEnabled } from '/imports/ui/services/features';
const CAPTIONS_CONFIG = Meteor.settings.public.captions;
const CAPTIONS_CONFIG = window.meetingClientSettings.public.captions;
const LINE_BREAK = '\n';
const getAvailableLocales = () => {

View File

@ -17,10 +17,11 @@ import { generateExportedMessages } from './services';
import { getDateString } from '/imports/utils/string-utils';
import { ChatCommands } from '/imports/ui/core/enums/chat';
import { CHAT_PUBLIC_CLEAR_HISTORY } from './mutations';
import useMeetingSettings from '/imports/ui/core/local-states/useMeetingSettings';
// @ts-ignore - temporary, while meteor exists in the project
const CHAT_CONFIG = Meteor.settings.public.chat;
const ENABLE_SAVE_AND_COPY_PUBLIC_CHAT = CHAT_CONFIG.enableSaveAndCopyPublicChat;
// const CHAT_CONFIG = window.meetingClientSettings.public.chat;
// const ENABLE_SAVE_AND_COPY_PUBLIC_CHAT = CHAT_CONFIG.enableSaveAndCopyPublicChat;
const intlMessages = defineMessages({
clear: {
@ -54,6 +55,9 @@ const intlMessages = defineMessages({
});
const ChatActions: React.FC = () => {
const [MeetingSettings] = useMeetingSettings();
const chatConfig = MeetingSettings.public.chat;
const { enableSaveAndCopyPublicChat } = chatConfig;
const intl = useIntl();
const isRTL = layoutSelect((i: Layout) => i.isRTL);
const uniqueIdsRef = useRef<string[]>([uid(1), uid(2), uid(3), uid(4)]);
@ -116,7 +120,7 @@ const ChatActions: React.FC = () => {
const dropdownActions = [
{
key: uniqueIdsRef.current[0],
enable: ENABLE_SAVE_AND_COPY_PUBLIC_CHAT,
enable: enableSaveAndCopyPublicChat,
icon: 'download',
dataTest: 'chatSave',
label: intl.formatMessage(intlMessages.save),
@ -127,7 +131,7 @@ const ChatActions: React.FC = () => {
},
{
key: uniqueIdsRef.current[1],
enable: ENABLE_SAVE_AND_COPY_PUBLIC_CHAT,
enable: enableSaveAndCopyPublicChat,
icon: 'copy',
id: 'clipboardButton',
dataTest: 'chatCopy',

View File

@ -38,7 +38,7 @@ import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
import { throttle } from '/imports/utils/throttle';
// @ts-ignore - temporary, while meteor exists in the project
const CHAT_CONFIG = Meteor.settings.public.chat;
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id;
@ -114,9 +114,9 @@ const messages = defineMessages({
});
// @ts-ignore - temporary, while meteor exists in the project
const AUTO_CONVERT_EMOJI = Meteor.settings.public.chat.autoConvertEmoji;
const AUTO_CONVERT_EMOJI = window.meetingClientSettings.public.chat.autoConvertEmoji;
// @ts-ignore - temporary, while meteor exists in the project
const ENABLE_EMOJI_PICKER = Meteor.settings.public.chat.emojiPicker.enable;
const ENABLE_EMOJI_PICKER = window.meetingClientSettings.public.chat.emojiPicker.enable;
const ENABLE_TYPING_INDICATOR = CHAT_CONFIG.typingIndicator.enabled;
const ChatMessageForm: React.FC<ChatMessageFormProps> = ({

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

@ -5,7 +5,6 @@ import React, {
useState,
useMemo,
} from 'react';
import { Meteor } from 'meteor/meteor';
import { makeVar, useMutation } from '@apollo/client';
import { defineMessages, useIntl } from 'react-intl';
import LAST_SEEN_MUTATION from './queries';
@ -27,7 +26,7 @@ import { Layout } from '../../../layout/layoutTypes';
import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
// @ts-ignore - temporary, while meteor exists in the project
const CHAT_CONFIG = Meteor.settings.public.chat;
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_id;
const PUBLIC_GROUP_CHAT_KEY = CHAT_CONFIG.public_group_id;

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

@ -4,7 +4,7 @@ import Styled from './styles';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - temporary, while meteor exists in the project
const APP_CONFIG = Meteor.settings.public.app;
const APP_CONFIG = window.meetingClientSettings.public.app;
interface ChatMessagePresentationContentProps {
metadata: string;

View File

@ -14,7 +14,7 @@ const ChatMessageTextContent: React.FC<ChatMessageTextContentProps> = ({
}) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - temporary, while meteor exists in the project
const { allowedElements } = Meteor.settings.public.chat;
const { allowedElements } = window.meetingClientSettings.public.chat;
return (
<Styled.ChatMessage systemMsg={systemMsg} emphasizedMessage={emphasizedMessage} data-test="messageContent">

View File

@ -14,9 +14,10 @@ import ChatMessage from './chat-message/component';
import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
import { useCreateUseSubscription } from '/imports/ui/core/hooks/createUseSubscription';
import { setLoadedMessageGathering } from '/imports/ui/core/hooks/useLoadedChatMessages';
import { ChatLoading } from '../../component';
// @ts-ignore - temporary, while meteor exists in the project
const CHAT_CONFIG = Meteor.settings.public.chat;
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
const PUBLIC_GROUP_CHAT_KEY = CHAT_CONFIG.public_group_id;
interface ChatListPageContainerProps {
@ -113,7 +114,9 @@ const ChatListPageContainer: React.FC<ChatListPageContainerProps> = ({
setLoadedMessageGathering(page, []);
};
}, []);
if (!chatMessageData) return null;
if (chatMessageData.length > 0 && chatId !== chatMessageData[0].chatId) return <ChatLoading isRTL={document.dir === 'rtl'} />;
if (chatMessageData.length > 0 && chatMessageData[chatMessageData.length - 1].user?.userId) {
setLastSender(page, chatMessageData[chatMessageData.length - 1].user?.userId);
}

View File

@ -16,7 +16,7 @@ interface ChatPopupProps {
const WELCOME_MSG_KEY = 'welcomeMsg';
const WELCOME_MSG_FOR_MODERATORS_KEY = 'welcomeMsgForModerators';
// @ts-ignore - temporary, while meteor exists in the project
const CHAT_CONFIG = Meteor.settings.public.chat;
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
const PUBLIC_GROUP_CHAT_KEY = CHAT_CONFIG.public_group_id;
const setWelcomeMsgsOnSession = (key: string, value: boolean) => {

View File

@ -24,7 +24,7 @@ const DEBUG_CONSOLE = false;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - temporary, while meteor exists in the project
const CHAT_CONFIG = Meteor.settings.public.chat;
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
const PUBLIC_GROUP_CHAT_KEY = CHAT_CONFIG.public_group_id;
const TYPING_INDICATOR_ENABLED = CHAT_CONFIG.typingIndicator.enabled;

View File

@ -31,7 +31,7 @@ const Chat: React.FC<ChatProps> = ({ isRTL }) => {
);
};
const ChatLoading: React.FC<ChatProps> = ({ isRTL }) => {
export const ChatLoading: React.FC<ChatProps> = ({ isRTL }) => {
const { isChrome } = browserInfo;
return (
<Styled.Chat isRTL={isRTL} isChrome={isChrome}>

View File

@ -8,7 +8,8 @@ import { meetingIsBreakout } from '/imports/ui/components/app/service';
import { defineMessages } from 'react-intl';
import PollService from '/imports/ui/components/poll/service';
const CHAT_CONFIG = Meteor.settings.public.chat;
const APP = window.meetingClientSettings.public.app;
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
const GROUPING_MESSAGES_WINDOW = CHAT_CONFIG.grouping_messages_window;
const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system;
@ -19,7 +20,7 @@ const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id;
const PUBLIC_CHAT_CLEAR = CHAT_CONFIG.system_messages_keys.chat_clear;
const CHAT_POLL_RESULTS_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_poll_result;
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
const ROLE_MODERATOR = window.meetingClientSettings.public.user.role_moderator;
const ScrollCollection = new Mongo.Collection(null);

View File

@ -1,16 +1,43 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import logger from '/imports/startup/client/logger';
import logger, { generateLoggerStreams } from '/imports/startup/client/logger';
const propTypes = {
children: PropTypes.element.isRequired,
Fallback: PropTypes.func.isRequired,
Fallback: PropTypes.element,
errorMessage: PropTypes.string,
};
const defaultProps = {
Fallback: null,
errorMessage: 'Something went wrong',
};
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { error: false, errorInfo: null };
this.state = { error: '', errorInfo: null };
}
componentDidMount() {
const data = JSON.parse((sessionStorage.getItem('clientStartupSettings')) || {});
const logConfig = data?.clientLog;
if (logConfig) {
generateLoggerStreams(logConfig).forEach((stream) => {
logger.addStream(stream);
});
}
}
componentDidUpdate() {
const { code, error, errorInfo } = this.state;
const log = code === '403' ? 'warn' : 'error';
if (error || errorInfo) {
logger[log]({
logCode: 'Error_Boundary_wrapper',
extraInfo: { error, errorInfo },
}, 'generic error boundary logger');
}
}
componentDidCatch(error, errorInfo) {
@ -18,26 +45,27 @@ class ErrorBoundary extends Component {
error,
errorInfo,
});
logger.error({
logCode: 'Error_Boundary_wrapper',
extraInfo: { error, errorInfo },
}, 'generic error boundary logger');
}
render() {
const { error } = this.state;
const { children, Fallback } = this.props;
const { error, errorInfo } = this.state;
const { children, Fallback, errorMessage } = this.props;
return (error ? (<Fallback {...this.state} />) : children);
const fallbackElement = Fallback && error
? <Fallback error={error || {}} errorInfo={errorInfo} /> : <div>{errorMessage}</div>;
return (error
? fallbackElement
: children);
}
}
ErrorBoundary.propTypes = propTypes;
ErrorBoundary.defaultProps = defaultProps;
export default ErrorBoundary;
export const withErrorBoundary = (WrappedComponent, FallbackComponent) => (props) => (
<ErrorBoundary Fallback={FallbackComponent}>
<WrappedComponent {...props} />
</ErrorBoundary>
);
export default ErrorBoundary;

View File

@ -0,0 +1,24 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import ErrorBoundary from '../component';
const intlMessages = defineMessages({
errorMessage: {
id: 'app.presentationUploder.genericError',
defaultMessage: 'Something went wrong',
},
});
const LocatedErrorBoundary = ({ children, ...props }) => {
const intl = useIntl();
return (
<ErrorBoundary
{...props}
errorMessage={intl.formatMessage(intlMessages.errorMessage)}
>
{children}
</ErrorBoundary>
);
};
export default LocatedErrorBoundary;

View File

@ -1,14 +1,11 @@
import React from 'react';
import Styled from './styles';
import Settings from '/imports/ui/services/settings';
const { animations } = Settings.application;
const LoadingScreen = ({ children }) => (
<Styled.Background>
<Styled.Spinner animations={animations}>
<Styled.Bounce1 animations={animations} />
<Styled.Bounce2 animations={animations} />
<Styled.Spinner animations>
<Styled.Bounce1 animations />
<Styled.Bounce2 animations />
<div />
</Styled.Spinner>
<Styled.Message>

View File

@ -0,0 +1,57 @@
import React from 'react';
import LoadingScreen from '../component';
interface LoadingContent {
isLoading: boolean;
loadingMessage: string;
}
interface LoadingContextContent extends LoadingContent {
setLoading: (isLoading: boolean, loadingMessage: string) => void;
}
export const LoadingContext = React.createContext<LoadingContextContent>({
isLoading: false,
loadingMessage: '',
setLoading: () => { },
});
interface LoadingScreenHOCProps {
children: React.ReactNode;
}
const LoadingScreenHOC: React.FC<LoadingScreenHOCProps> = ({
children,
}) => {
const [loading, setLoading] = React.useState<LoadingContent>({
isLoading: false,
loadingMessage: '',
});
return (
<LoadingContext.Provider value={{
loadingMessage: loading.loadingMessage,
isLoading: loading.isLoading,
setLoading: (isLoading: boolean, loadingMessage: string = '') => {
setLoading({
isLoading,
loadingMessage,
});
},
}}
>
{
loading.isLoading
? (
<LoadingScreen>
<h1>{loading.loadingMessage}</h1>
</LoadingScreen>
)
: null
}
{children}
</LoadingContext.Provider>
);
};
export default LoadingScreenHOC;

View File

@ -22,7 +22,7 @@ class LocalesDropdown extends PureComponent {
filterLocaleVariations(value) {
const { allLocales } = this.props;
if (allLocales) {
if (Meteor.settings.public.app.showAllAvailableLocales) {
if (window.meetingClientSettings.public.app.showAllAvailableLocales) {
return allLocales;
}

View File

@ -5,7 +5,7 @@ import ModalSimple from '/imports/ui/components/common/modal/simple/component';
import AudioService from '/imports/ui/components/audio/service';
import Styled from './styles';
const SELECT_RANDOM_USER_COUNTDOWN = Meteor.settings.public.selectRandomUser.countdown;
const SELECT_RANDOM_USER_COUNTDOWN = window.meetingClientSettings.public.selectRandomUser.countdown;
const messages = defineMessages({
noViewers: {
@ -102,9 +102,9 @@ class RandomUserSelect extends Component {
}
play() {
AudioService.playAlertSound(`${Meteor.settings.public.app.cdn
+ Meteor.settings.public.app.basename
+ Meteor.settings.public.app.instanceId}`
AudioService.playAlertSound(`${window.meetingClientSettings.public.app.cdn
+ window.meetingClientSettings.public.app.basename
+ window.meetingClientSettings.public.app.instanceId}`
+ '/resources/sounds/Poll.mp3');
}

View File

@ -11,7 +11,7 @@ import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
import { useMutation } from '@apollo/client';
import { PICK_RANDOM_VIEWER } from '/imports/ui/core/graphql/mutations/userMutations';
const SELECT_RANDOM_USER_ENABLED = Meteor.settings.public.selectRandomUser.enabled;
const SELECT_RANDOM_USER_ENABLED = window.meetingClientSettings.public.selectRandomUser.enabled;
// A value that is used by component to remember
// whether it should be open or closed after a render

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

@ -0,0 +1,41 @@
import { useSubscription } from '@apollo/client';
import React, { useEffect, useState } from 'react';
import VoiceUsers from '/imports/api/voice-users/';
import logger from '/imports/startup/client/logger';
import { UserVoiceStreamResponse, voiceUserStream } from './queries';
const VoiceUserGrapQlMiniMongoAdapter: React.FC = () => {
const {
loading,
error,
data,
} = useSubscription<UserVoiceStreamResponse>(voiceUserStream);
const [voiceUserDataSetted, setVoiceUserDataSetted] = useState(false);
useEffect(() => {
if (error) {
logger.error('Error in VoiceUserGrapQlMiniMongoAdapter', error);
}
}, [error]);
useEffect(() => {
if (data && data.user_voice_mongodb_adapter_stream) {
const usersVoice = data.user_voice_mongodb_adapter_stream;
usersVoice.forEach((userVoice) => {
VoiceUsers.upsert({ userId: userVoice.userId }, userVoice);
});
}
}, [data]);
useEffect(() => {
if (loading) {
if (!voiceUserDataSetted) {
setVoiceUserDataSetted(true);
}
}
}, [loading]);
return null;
};
export default VoiceUserGrapQlMiniMongoAdapter;

View File

@ -0,0 +1,67 @@
import { gql } from '@apollo/client';
interface UserVoiceStream {
callerName: string | null;
callerNum: string | null;
callingWith: string | null;
endTime: string | null;
endedAt: string | null;
floor: string | null;
hideTalkingIndicatorAt: string | null;
joined: string | null;
lastFloorTime: string | null;
lastSpeakChangedAt: number;
listenOnly: string | null;
muted: string | null;
showTalkingIndicator: boolean;
spoke: string | null;
startTime: string | null;
startedAt: string | null;
talking: string | null;
userId: string;
voiceConf: string | null;
voiceConfCallSession: string | null;
voiceConfCallState: string | null;
voiceConfClientSession: string | null;
voiceUpdatedAt: string | null;
voiceUserId: string | null;
}
export interface UserVoiceStreamResponse {
user_voice_mongodb_adapter_stream: UserVoiceStream[];
}
export const voiceUserStream = gql`
subscription voiceUserStream {
user_voice_mongodb_adapter_stream(cursor: {initial_value: {voiceUpdatedAt: "2020-01-01"}}, batch_size: 100) {
callerName
callerNum
callingWith
endTime
endedAt
floor
hideTalkingIndicatorAt
joined
lastFloorTime
lastSpeakChangedAt
listenOnly
muted
showTalkingIndicator
spoke
startTime
startedAt
talking
userId
voiceConf
voiceConfCallSession
voiceConfCallState
voiceConfClientSession
voiceUpdatedAt
voiceUserId
}
}
`;
export default {
voiceUserStream,
};

View File

@ -0,0 +1,107 @@
import {
ApolloClient, ApolloProvider, InMemoryCache, NormalizedCacheObject,
} from '@apollo/client';
import { WebSocketLink } from '@apollo/client/link/ws';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import React, { useContext, useEffect } from 'react';
import { Meteor } from 'meteor/meteor';
import { LoadingContext } from '/imports/ui/components/common/loading-screen/loading-screen-HOC/component';
interface ConnectionManagerProps {
children: React.ReactNode;
}
interface Response {
response: {
returncode: string;
version: string;
apiVersion: string;
bbbVersion: string;
graphqlWebsocketUrl: string;
}
}
const ConnectionManager: React.FC<ConnectionManagerProps> = ({ children }): React.ReactNode => {
const [graphqlUrlApolloClient, setApolloClient] = React.useState<ApolloClient<NormalizedCacheObject> | null>(null);
const [graphqlUrl, setGraphqlUrl] = React.useState<string>('');
const loadingContextInfo = useContext(LoadingContext);
useEffect(() => {
fetch(`https://${window.location.hostname}/bigbluebutton/api`, {
headers: {
'Content-Type': 'application/json',
},
}).then(async (response) => {
const responseJson: Response = await response.json();
setGraphqlUrl(responseJson.response.graphqlWebsocketUrl);
}).catch((error) => {
loadingContextInfo.setLoading(false, '');
throw new Error('Error fetching GraphQL URL: '.concat(error.message || ''));
});
loadingContextInfo.setLoading(true, 'Fetching GraphQL URL');
}, []);
useEffect(() => {
loadingContextInfo.setLoading(true, 'Connecting to GraphQL server');
if (graphqlUrl) {
const urlParams = new URLSearchParams(window.location.search);
const sessionToken = urlParams.get('sessionToken');
if (!sessionToken) {
loadingContextInfo.setLoading(false, '');
throw new Error('Missing session token');
}
sessionStorage.setItem('sessionToken', sessionToken);
let wsLink;
try {
const subscription = new SubscriptionClient(graphqlUrl, {
reconnect: true,
timeout: 30000,
connectionParams: {
headers: {
'X-Session-Token': sessionToken,
},
},
});
subscription.onError(() => {
loadingContextInfo.setLoading(false, '');
throw new Error('Error: on subscription to server');
});
wsLink = new WebSocketLink(
subscription,
);
wsLink.setOnError((error) => {
loadingContextInfo.setLoading(false, '');
throw new Error('Error: on apollo connection'.concat(JSON.stringify(error) || ''));
});
} catch (error) {
loadingContextInfo.setLoading(false, '');
throw new Error('Error creating WebSocketLink: '.concat(JSON.stringify(error) || ''));
}
let client;
try {
client = new ApolloClient({
link: wsLink,
cache: new InMemoryCache(),
connectToDevTools: Meteor.isDevelopment,
});
setApolloClient(client);
} catch (error) {
loadingContextInfo.setLoading(false, '');
throw new Error('Error creating Apollo Client: '.concat(JSON.stringify(error) || ''));
}
}
},
[graphqlUrl]);
return (
graphqlUrlApolloClient
? (
<ApolloProvider
client={graphqlUrlApolloClient}
>
{children}
</ApolloProvider>
) : null
);
};
export default ConnectionManager;

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