refactor(guest-wait): turn guest wait page into a React component (#20344)

* refactor(guest-wait): turn guest wait page into a React component

* Fix rendering when the meeting is ended

* refactor(guest-wait): Backend portion for migration of `guest-wait` to Graphql

* Add message timeout

* Remove static guest wait page

---------

Co-authored-by: Gustavo Trott <gustavo@trott.com.br>
This commit is contained in:
João Victor Nunes 2024-06-05 17:41:23 -03:00 committed by GitHub
parent 6e3b582bdd
commit 5d3178f15d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 474 additions and 714 deletions

View File

@ -21,6 +21,12 @@ case class IsMeetingActorAliveMessage(meetingId: String) extends InMessage
case class MonitorNumberOfUsersInternalMsg(meetingID: String) extends InMessage
/**
* Audit message sent to meeting to trigger updating clients of meeting time remaining.
* @param meetingId
*/
case class MonitorGuestWaitPresenceInternalMsg(meetingId: String) extends InMessage
/**
* Audit message sent to meeting to trigger updating clients of meeting time remaining.
* @param meetingId

View File

@ -5,7 +5,6 @@ import org.bigbluebutton.core2.message.handlers.guests._
trait GuestsApp extends GetGuestsWaitingApprovalReqMsgHdlr
with GuestsWaitingApprovedMsgHdlr
with GuestWaitingLeftMsgHdlr
with UpdatePositionInWaitingQueueReqMsgHdlr
with SetGuestPolicyMsgHdlr
with SetGuestLobbyMessageMsgHdlr

View File

@ -3,7 +3,7 @@ package org.bigbluebutton.core.apps.users
import org.bigbluebutton.common2.msgs.{ BbbClientMsgHeader, BbbCommonEnvCoreMsg, BbbCoreEnvelope, MessageTypes, Routing, UserMobileFlagChangedEvtMsg, UserMobileFlagChangedEvtMsgBody }
import org.bigbluebutton.core.api.UserEstablishedGraphqlConnectionInternalMsg
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.models.{ UserState, Users2x }
import org.bigbluebutton.core.models.{ RegisteredUsers, UserState, Users2x }
import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting, MeetingActor, OutMsgRouter }
trait UserEstablishedGraphqlConnectionInternalMsgHdlr extends HandlerHelpers {
@ -14,6 +14,13 @@ trait UserEstablishedGraphqlConnectionInternalMsgHdlr extends HandlerHelpers {
def handleUserEstablishedGraphqlConnectionInternalMsg(msg: UserEstablishedGraphqlConnectionInternalMsg, state: MeetingState2x): MeetingState2x = {
log.info("Received user established a graphql connection. msg={} meetingId={}", msg, liveMeeting.props.meetingProp.intId)
for {
regUser <- RegisteredUsers.findWithUserId(msg.userId, liveMeeting.registeredUsers)
} yield {
RegisteredUsers.updateUserConnectedToGraphql(liveMeeting.registeredUsers, regUser, graphqlConnected = true)
}
Users2x.findWithIntId(liveMeeting.users2x, msg.userId) match {
case Some(connectingUser) =>
var userNewState = connectingUser

View File

@ -19,6 +19,12 @@ trait UserLeaveReqMsgHdlr extends HandlerHelpers {
def handleUserClosedAllGraphqlConnectionsInternalMsg(msg: UserClosedAllGraphqlConnectionsInternalMsg, state: MeetingState2x): MeetingState2x = {
log.info("Received user closed all graphql connections. user {} meetingId={}", msg.userId, liveMeeting.props.meetingProp.intId)
for {
regUser <- RegisteredUsers.findWithUserId(msg.userId, liveMeeting.registeredUsers)
} yield {
RegisteredUsers.updateUserConnectedToGraphql(liveMeeting.registeredUsers, regUser, graphqlConnected = false)
}
handleUserLeaveReq(msg.userId, liveMeeting.props.meetingProp.intId, loggedOut = false, state)
}

View File

@ -31,7 +31,7 @@ object UsersApp {
u <- RegisteredUsers.findWithUserId(userId, liveMeeting.registeredUsers)
} yield {
RegisteredUsers.eject(u.id, liveMeeting.registeredUsers, false)
RegisteredUsers.eject(u.id, liveMeeting.registeredUsers, ban = false)
val event = MsgBuilder.buildGuestWaitingLeftEvtMsg(liveMeeting.props.meetingProp.intId, u.id)
outGW.send(event)

View File

@ -143,6 +143,7 @@ object UserDAO {
TableQuery[UserDbTableDef]
.filter(_.meetingId === meetingId)
.filter(_.userId === userId)
.filter(_.loggedOut =!= true)
.map(u => (u.loggedOut))
.update((true))
).onComplete {

View File

@ -26,6 +26,8 @@ object RegisteredUsers {
System.currentTimeMillis(),
0,
false,
0,
false,
false,
enforceLayout,
customParameters,
@ -37,6 +39,10 @@ object RegisteredUsers {
users.toVector.find(u => u.authToken == token)
}
def findAll(users: RegisteredUsers): Vector[RegisteredUser] = {
users.toVector
}
def findWithUserId(id: String, users: RegisteredUsers): Option[RegisteredUser] = {
users.toVector.find(ru => id == ru.id)
}
@ -123,13 +129,14 @@ object RegisteredUsers {
u
} else {
users.delete(ejectedUser.id)
// UserDAO.softDelete(ejectedUser) it's being removed in User2x already
UserDAO.softDelete(ejectedUser.meetingId, ejectedUser.id)
ejectedUser
}
}
def eject(id: String, users: RegisteredUsers, ban: Boolean): Option[RegisteredUser] = {
def eject(userId: String, users: RegisteredUsers, ban: Boolean): Option[RegisteredUser] = {
for {
ru <- findWithUserId(id, users)
ru <- findWithUserId(userId, users)
} yield {
banOrEjectUser(ru, users, ban)
}
@ -172,6 +179,23 @@ object RegisteredUsers {
u
}
def updateUserConnectedToGraphql(users: RegisteredUsers, user: RegisteredUser, graphqlConnected: Boolean): RegisteredUser = {
val u = user.copy(
graphqlConnected = graphqlConnected,
graphqlDisconnectedOn = {
if(graphqlConnected) {
0
} else if(!graphqlConnected && user.graphqlDisconnectedOn == 0) {
System.currentTimeMillis()
} else {
user.graphqlDisconnectedOn
}
}
)
users.save(u)
u
}
def setUserLoggedOutFlag(users: RegisteredUsers, user: RegisteredUser): RegisteredUser = {
val u = user.copy(loggedOut = true)
users.save(u)
@ -216,6 +240,8 @@ case class RegisteredUser(
excludeFromDashboard: Boolean,
registeredOn: Long,
lastAuthTokenValidatedOn: Long,
graphqlConnected: Boolean,
graphqlDisconnectedOn: Long,
joined: Boolean,
banned: Boolean,
enforceLayout: String,

View File

@ -82,8 +82,6 @@ class ReceivedJsonMsgHandlerActor(
routeGenericMsg[GetGuestsWaitingApprovalReqMsg](envelope, jsonNode)
case GuestsWaitingApprovedMsg.NAME =>
routeGenericMsg[GuestsWaitingApprovedMsg](envelope, jsonNode)
case GuestWaitingLeftMsg.NAME =>
routeGenericMsg[GuestWaitingLeftMsg](envelope, jsonNode)
case UpdatePositionInWaitingQueueReqMsg.NAME =>
routeGenericMsg[UpdatePositionInWaitingQueueReqMsg](envelope, jsonNode)
case SetGuestPolicyCmdMsg.NAME =>

View File

@ -272,6 +272,7 @@ class MeetingActor(
//=======================================
// internal messages
case msg: MonitorNumberOfUsersInternalMsg => handleMonitorNumberOfUsers(msg)
case msg: MonitorGuestWaitPresenceInternalMsg => handleMonitorGuestWaitPresenceInternalMsg(msg)
case msg: SetPresenterInDefaultPodInternalMsg => state = presentationPodsApp.handleSetPresenterInDefaultPodInternalMsg(msg, state, liveMeeting, msgBus)
case msg: UserClosedAllGraphqlConnectionsInternalMsg =>
state = handleUserClosedAllGraphqlConnectionsInternalMsg(msg, state)
@ -652,7 +653,6 @@ class MeetingActor(
case m: GuestsWaitingApprovedMsg =>
handleGuestsWaitingApprovedMsg(m)
updateUserLastActivity(m.header.userId)
case m: GuestWaitingLeftMsg => handleGuestWaitingLeftMsg(m)
case m: GetGuestPolicyReqMsg => handleGetGuestPolicyReqMsg(m)
case m: UpdatePositionInWaitingQueueReqMsg => handleUpdatePositionInWaitingQueueReqMsg(m)
case m: SetPrivateGuestLobbyMessageCmdMsg =>
@ -914,6 +914,25 @@ class MeetingActor(
checkIfNeedToEndMeetingWhenNoModerators(liveMeeting)
}
def handleMonitorGuestWaitPresenceInternalMsg(msg: MonitorGuestWaitPresenceInternalMsg) {
if (liveMeeting.props.usersProp.waitingGuestUsersTimeout > 0) {
for {
regUser <- RegisteredUsers.findAll(liveMeeting.registeredUsers)
} yield {
if (!regUser.loggedOut
&& regUser.guestStatus == GuestStatus.WAIT
&& !regUser.graphqlConnected
&& regUser.graphqlDisconnectedOn != 0) {
val diff = System.currentTimeMillis() - regUser.graphqlDisconnectedOn
if (diff > liveMeeting.props.usersProp.waitingGuestUsersTimeout) {
GuestsWaiting.remove(liveMeeting.guestsWaiting, regUser.id)
UsersApp.guestWaitingLeft(liveMeeting, regUser.id, outGW)
}
}
}
}
}
def checkVoiceConfUsersStatus(): Unit = {
val event = MsgBuilder.buildLastcheckVoiceConfUsersStatus(
props.meetingProp.intId,

View File

@ -71,6 +71,7 @@ class MeetingActorAudit(
def handleMonitorNumberOfWebUsers() {
eventBus.publish(BigBlueButtonEvent(props.meetingProp.intId, MonitorNumberOfUsersInternalMsg(props.meetingProp.intId)))
eventBus.publish(BigBlueButtonEvent(props.meetingProp.intId, MonitorGuestWaitPresenceInternalMsg(props.meetingProp.intId)))
// Trigger updating users of time remaining on meeting.
eventBus.publish(BigBlueButtonEvent(props.meetingProp.intId, SendTimeRemainingAuditInternalMsg(props.meetingProp.intId, 0)))

View File

@ -151,7 +151,6 @@ class AnalyticsActor(val includeChat: Boolean) extends Actor with ActorLogging {
// Guest Management
case m: GuestsWaitingApprovedMsg => logMessage(msg)
case m: GuestsWaitingApprovedEvtMsg => logMessage(msg)
case m: GuestWaitingLeftMsg => logMessage(msg)
case m: GuestWaitingLeftEvtMsg => logMessage(msg)
case m: GuestsWaitingForApprovalEvtMsg => logMessage(msg)
case m: UpdatePositionInWaitingQueueReqMsg => logMessage(msg)

View File

@ -1,18 +0,0 @@
package org.bigbluebutton.core2.message.handlers.guests
import org.bigbluebutton.common2.msgs.GuestWaitingLeftMsg
import org.bigbluebutton.core.apps.users.UsersApp
import org.bigbluebutton.core.models.GuestsWaiting
import org.bigbluebutton.core.running.{ BaseMeetingActor, LiveMeeting, OutMsgRouter }
trait GuestWaitingLeftMsgHdlr {
this: BaseMeetingActor =>
val liveMeeting: LiveMeeting
val outGW: OutMsgRouter
def handleGuestWaitingLeftMsg(msg: GuestWaitingLeftMsg): Unit = {
GuestsWaiting.remove(liveMeeting.guestsWaiting, msg.body.userId)
UsersApp.guestWaitingLeft(liveMeeting, msg.body.userId, outGW)
}
}

View File

@ -49,7 +49,8 @@ case class UsersProp(
meetingLayout: String,
allowModsToUnmuteUsers: Boolean,
allowModsToEjectCameras: Boolean,
authenticatedGuest: Boolean
authenticatedGuest: Boolean,
waitingGuestUsersTimeout: Long,
)
case class MetadataProp(metadata: collection.immutable.Map[String, String])

View File

@ -63,16 +63,6 @@ case class GuestApprovedEvtMsg(
) extends BbbCoreMsg
case class GuestApprovedEvtMsgBody(status: String, approvedBy: String)
/**
* Message from bbb-web when it detects a guest stopped polling for his status.
*/
object GuestWaitingLeftMsg { val NAME = "GuestWaitingLeftMsg" }
case class GuestWaitingLeftMsg(
header: BbbClientMsgHeader,
body: GuestWaitingLeftMsgBody
) extends StandardMsg
case class GuestWaitingLeftMsgBody(userId: String)
/**
* Message sent to all clients that a guest left the waiting page.
*/

View File

@ -45,7 +45,6 @@ import org.bigbluebutton.api2.domain.UploadedTrack;
import org.bigbluebutton.common2.redis.RedisStorageService;
import org.bigbluebutton.presentation.PresentationUrlDownloadService;
import org.bigbluebutton.presentation.imp.SlidesGenerationProgressNotifier;
import org.bigbluebutton.web.services.WaitingGuestCleanupTimerTask;
import org.bigbluebutton.web.services.UserCleanupTimerTask;
import org.bigbluebutton.web.services.EnteredUserCleanupTimerTask;
import org.bigbluebutton.web.services.callback.CallbackUrlService;
@ -79,7 +78,6 @@ public class MeetingService implements MessageListener {
private RecordingService recordingService;
private LearningDashboardService learningDashboardService;
private WaitingGuestCleanupTimerTask waitingGuestCleaner;
private UserCleanupTimerTask userCleaner;
private EnteredUserCleanupTimerTask enteredUserCleaner;
private StunTurnService stunTurnService;
@ -259,31 +257,6 @@ public class MeetingService implements MessageListener {
}
}
/**
* Remove registered waiting guest users who left the waiting page.
*/
public void purgeWaitingGuestUsers() {
for (AbstractMap.Entry<String, Meeting> entry : this.meetings.entrySet()) {
Long now = System.currentTimeMillis();
Meeting meeting = entry.getValue();
ConcurrentMap<String, User> users = meeting.getUsersMap();
for (AbstractMap.Entry<String, RegisteredUser> registeredUser : meeting.getRegisteredUsers().entrySet()) {
String registeredUserID = registeredUser.getKey();
RegisteredUser ru = registeredUser.getValue();
long elapsedTime = now - ru.getGuestWaitedOn();
if (elapsedTime >= waitingGuestUsersTimeout && ru.getGuestStatus() == GuestPolicy.WAIT) {
log.info("Purging user [{}]", registeredUserID);
if (meeting.userUnregistered(registeredUserID) != null) {
gw.guestWaitingLeft(meeting.getInternalId(), registeredUserID);
meeting.setLeftGuestLobby(registeredUserID, true);
};
}
}
}
}
private void kickOffProcessingOfRecording(Meeting m) {
if (m.isRecord() && m.getNumUsers() == 0) {
processRecording(m);
@ -446,7 +419,8 @@ public class MeetingService implements MessageListener {
m.getWebcamsOnlyForModerator(), m.getMeetingCameraCap(), m.getUserCameraCap(), m.getMaxPinnedCameras(), m.getModeratorPassword(), m.getViewerPassword(),
m.getLearningDashboardAccessToken(), m.getCreateTime(),
formatPrettyDate(m.getCreateTime()), m.isBreakout(), m.getSequence(), m.isFreeJoin(), m.getMetadata(),
m.getGuestPolicy(), m.getAuthenticatedGuest(), m.getMeetingLayout(), m.getWelcomeMessageTemplate(), m.getWelcomeMessage(), m.getWelcomeMsgForModerators(),
m.getGuestPolicy(), m.getAuthenticatedGuest(), m.getWaitingGuestUsersTimeout(), m.getMeetingLayout(),
m.getWelcomeMessageTemplate(), m.getWelcomeMessage(), m.getWelcomeMsgForModerators(),
m.getDialNumber(), m.getMaxUsers(), m.getMaxUserConcurrentAccesses(),
m.getMeetingExpireIfNoUserJoinedInMinutes(), m.getMeetingExpireWhenLastUserLeftInMinutes(),
m.getUserInactivityInspectTimerInMinutes(), m.getUserInactivityThresholdInMinutes(),
@ -1349,7 +1323,6 @@ public class MeetingService implements MessageListener {
public void stop() {
processMessage = false;
waitingGuestCleaner.stop();
userCleaner.stop();
enteredUserCleaner.stop();
}
@ -1374,12 +1347,6 @@ public class MeetingService implements MessageListener {
this.gw = gw;
}
public void setWaitingGuestCleanupTimerTask(WaitingGuestCleanupTimerTask c) {
waitingGuestCleaner = c;
waitingGuestCleaner.setMeetingService(this);
waitingGuestCleaner.start();
}
public void setEnteredUserCleanupTimerTask(EnteredUserCleanupTimerTask c) {
enteredUserCleaner = c;
enteredUserCleaner.setMeetingService(this);

View File

@ -77,13 +77,13 @@ public class ParamsProcessorUtil {
private String defaultHTML5ClientUrl;
private String graphqlWebsocketUrl;
private String defaultGuestWaitURL;
private Boolean allowRequestsWithoutSession = false;
private Integer defaultHttpSessionTimeout = 14400;
private Boolean useDefaultAvatar = false;
private String defaultAvatarURL;
private String defaultGuestPolicy;
private Boolean authenticatedGuest;
private Long waitingGuestUsersTimeout;
private String defaultMeetingLayout;
private int defaultMeetingDuration;
private boolean disableRecordingDefault;
@ -767,6 +767,7 @@ public class ParamsProcessorUtil {
.withIsBreakout(isBreakout)
.withGuestPolicy(guestPolicy)
.withAuthenticatedGuest(authenticatedGuest)
.withWaitingGuestUsersTimeout(waitingGuestUsersTimeout)
.withAllowRequestsWithoutSession(allowRequestsWithoutSession)
.withMeetingLayout(meetingLayout)
.withBreakoutRoomsParams(breakoutParams)
@ -878,10 +879,6 @@ public class ParamsProcessorUtil {
return graphqlWebsocketUrl;
}
public String getDefaultGuestWaitURL() {
return defaultGuestWaitURL;
}
public Boolean getUseDefaultLogo() {
return useDefaultLogo;
}
@ -1245,10 +1242,6 @@ public class ParamsProcessorUtil {
this.graphqlWebsocketUrl = graphqlWebsocketUrl.replace("https://","wss://");
}
public void setDefaultGuestWaitURL(String url) {
this.defaultGuestWaitURL = url;
}
public void setUseDefaultLogo(Boolean value) {
this.useDefaultLogo = value;
}
@ -1321,7 +1314,11 @@ public class ParamsProcessorUtil {
this.authenticatedGuest = value;
}
public void setDefaultMeetingLayout(String meetingLayout) {
public void setWaitingGuestUsersTimeout(Long value) {
this.waitingGuestUsersTimeout = value;
}
public void setDefaultMeetingLayout(String meetingLayout) {
this.defaultMeetingLayout = meetingLayout;
}

View File

@ -81,6 +81,7 @@ public class Meeting {
private String guestLobbyMessage = "";
private Map<String,String> usersWithGuestLobbyMessages;
private Boolean authenticatedGuest = false;
private long waitingGuestUsersTimeout = 30000;
private String meetingLayout = MeetingLayout.SMART_LAYOUT;
private boolean userHasJoined = false;
private Map<String, String> guestUsersWithPositionInWaitingLine;
@ -165,6 +166,7 @@ public class Meeting {
isBreakout = builder.isBreakout;
guestPolicy = builder.guestPolicy;
authenticatedGuest = builder.authenticatedGuest;
waitingGuestUsersTimeout = builder.waitingGuestUsersTimeout;
meetingLayout = builder.meetingLayout;
allowRequestsWithoutSession = builder.allowRequestsWithoutSession;
breakoutRoomsParams = builder.breakoutRoomsParams;
@ -501,6 +503,14 @@ public class Meeting {
return authenticatedGuest;
}
public void setWaitingGuestUsersTimeout(long waitingGuestUsersTimeout) {
waitingGuestUsersTimeout = waitingGuestUsersTimeout;
}
public long getWaitingGuestUsersTimeout() {
return waitingGuestUsersTimeout;
}
public void setMeetingLayout(String layout) {
meetingLayout = layout;
}
@ -910,6 +920,7 @@ public class Meeting {
private boolean isBreakout;
private String guestPolicy;
private Boolean authenticatedGuest;
private long waitingGuestUsersTimeout;
private Boolean allowRequestsWithoutSession;
private String meetingLayout;
private BreakoutRoomsParams breakoutRoomsParams;
@ -1096,6 +1107,11 @@ public class Meeting {
return this;
}
public Builder withWaitingGuestUsersTimeout(long waitingGuestUsersTimeout) {
this.waitingGuestUsersTimeout = waitingGuestUsersTimeout;
return this;
}
public Builder withAllowRequestsWithoutSession(Boolean value) {
allowRequestsWithoutSession = value;
return this;

View File

@ -15,19 +15,39 @@ import org.bigbluebutton.presentation.messages.IDocConversionMsg;
public interface IBbbWebApiGWApp {
void send(String channel, String message);
void createMeeting(String meetingID, String externalMeetingID,
String parentMeetingID, String meetingName, Boolean recorded,
String voiceBridge, Integer duration, Boolean autoStartRecording,
void createMeeting(String meetingID,
String externalMeetingID,
String parentMeetingID,
String meetingName,
Boolean recorded,
String voiceBridge,
Integer duration,
Boolean autoStartRecording,
Boolean allowStartStopRecording,
Boolean recordFullDurationMedia,
Boolean webcamsOnlyForModerator,
Integer meetingCameraCap,
Integer userCameraCap,
Integer maxPinnedCameras,
String moderatorPass, String viewerPass, String learningDashboardAccessToken, Long createTime,
String createDate, Boolean isBreakout, Integer sequence, Boolean freejoin, Map<String, String> metadata,
String guestPolicy, Boolean authenticatedGuest, String meetingLayout, String welcomeMsgTemplate, String welcomeMsg, String welcomeMsgForModerators,
String dialNumber, Integer maxUsers, Integer maxUserConcurrentAccesses,
String moderatorPass,
String viewerPass,
String learningDashboardAccessToken,
Long createTime,
String createDate,
Boolean isBreakout,
Integer sequence,
Boolean freejoin,
Map<String, String> metadata,
String guestPolicy,
Boolean authenticatedGuest,
Long waitingGuestUsersTimeout,
String meetingLayout,
String welcomeMsgTemplate,
String welcomeMsg,
String welcomeMsgForModerators,
String dialNumber,
Integer maxUsers,
Integer maxUserConcurrentAccesses,
Integer meetingExpireIfNoUserJoinedInMinutes,
Integer meetingExpireWhenLastUserLeftInMinutes,
Integer userInactivityInspectTimerInMinutes,
@ -57,7 +77,6 @@ public interface IBbbWebApiGWApp {
String externUserID, String authToken, String sessionToken, String avatarURL,
Boolean guest, Boolean authed, String guestStatus, Boolean excludeFromDashboard,
String enforceLayout, Map<String, String> customParameters);
void guestWaitingLeft(String meetingID, String internalUserId);
void destroyMeeting(DestroyMeetingMessage msg);
void endMeeting(EndMeetingMessage msg);

View File

@ -1,56 +0,0 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2020 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.web.services;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.bigbluebutton.api.MeetingService;
public class WaitingGuestCleanupTimerTask {
private MeetingService service;
private ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(1);
private long runEvery = 15000;
public void setMeetingService(MeetingService svc) {
this.service = svc;
}
public void start() {
scheduledThreadPool.scheduleWithFixedDelay(new CleanupTask(), 60000, runEvery, TimeUnit.MILLISECONDS);
}
public void stop() {
scheduledThreadPool.shutdownNow();
}
public void setRunEvery(long v) {
runEvery = v;
}
private class CleanupTask implements Runnable {
@Override
public void run() {
service.purgeWaitingGuestUsers();
}
}
}

View File

@ -130,7 +130,11 @@ class BbbWebApiGWApp(
createTime: java.lang.Long, createDate: String, isBreakout: java.lang.Boolean,
sequence: java.lang.Integer,
freeJoin: java.lang.Boolean,
metadata: java.util.Map[String, String], guestPolicy: String, authenticatedGuest: java.lang.Boolean, meetingLayout: String,
metadata: java.util.Map[String, String],
guestPolicy: String,
authenticatedGuest: java.lang.Boolean,
waitingGuestUsersTimeout: java.lang.Long,
meetingLayout: String,
welcomeMsgTemplate: String, welcomeMsg: String, welcomeMsgForModerators: String,
dialNumber: String,
maxUsers: java.lang.Integer,
@ -215,7 +219,8 @@ class BbbWebApiGWApp(
userCameraCap = userCameraCap.intValue(),
guestPolicy = guestPolicy, meetingLayout = meetingLayout, allowModsToUnmuteUsers = allowModsToUnmuteUsers.booleanValue(),
allowModsToEjectCameras = allowModsToEjectCameras.booleanValue(),
authenticatedGuest = authenticatedGuest.booleanValue()
authenticatedGuest = authenticatedGuest.booleanValue(),
waitingGuestUsersTimeout = waitingGuestUsersTimeout.longValue()
)
val metadataProp = MetadataProp(mapAsScalaMap(metadata).toMap)
@ -294,11 +299,6 @@ class BbbWebApiGWApp(
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))
}
def guestWaitingLeft(meetingId: String, intUserId: String): Unit = {
val event = MsgBuilder.buildGuestWaitingLeftMsg(meetingId, intUserId)
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))
}
def destroyMeeting(msg: DestroyMeetingMessage): Unit = {
val event = MsgBuilder.buildDestroyMeetingSysCmdMsg(msg)
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))

View File

@ -55,15 +55,6 @@ object MsgBuilder {
BbbCommonEnvCoreMsg(envelope, req)
}
def buildGuestWaitingLeftMsg(meetingId: String, userId: String): BbbCommonEnvCoreMsg = {
val routing = collection.immutable.HashMap("sender" -> "bbb-web")
val envelope = BbbCoreEnvelope(GuestWaitingLeftMsg.NAME, routing)
val header = BbbClientMsgHeader(GuestWaitingLeftMsg.NAME, meetingId, "not-used")
val body = GuestWaitingLeftMsgBody(userId)
val req = GuestWaitingLeftMsg(header, body)
BbbCommonEnvCoreMsg(envelope, req)
}
def buildCheckAlivePingSysMsg(system: String, bbbWebTimestamp: Long, akkaAppsTimestamp: Long): BbbCommonEnvCoreMsg = {
val routing = collection.immutable.HashMap("sender" -> "bbb-web")
val envelope = BbbCoreEnvelope(CheckAlivePingSysMsg.NAME, routing)

View File

@ -478,7 +478,8 @@ u."isDenied",
COALESCE(NULLIF(u."guestLobbyMessage",''),NULLIF(mup."guestLobbyMessage",'')) AS "guestLobbyMessage"
FROM "user" u
JOIN "meeting_usersPolicies" mup using("meetingId")
where u."guestStatus" = 'WAIT';
where u."guestStatus" = 'WAIT'
and u."loggedOut" is false;
--v_user_ref will be used only as foreign key (not possible to fetch this table directly through graphql)
--it is necessary because v_user has some conditions like "lockSettings-hideUserList"

View File

@ -42,6 +42,7 @@ select_permissions:
columns:
- guestLobbyMessage
- guestStatus
- isAllowed
- positionInWaitingQueue
filter:
_and:

View File

@ -10,8 +10,6 @@ import Logger from './logger';
import setMinBrowserVersions from './minBrowserVersion';
import { PrometheusAgent, METRIC_NAMES } from './prom-metrics/index.js'
let guestWaitHtml = '';
const DEFAULT_LANGUAGE = Meteor.settings.public.app.defaultSettings.application.fallbackLocale;
const FALLBACK_ON_EMPTY_STRING = Meteor.settings.public.app.fallbackOnEmptyLocaleString;
@ -354,17 +352,3 @@ WebApp.connectHandlers.use('/feedback', (req, res) => {
Logger.info('FEEDBACK LOG:', feedback);
}));
});
WebApp.connectHandlers.use('/guestWait', (req, res) => {
if (!guestWaitHtml) {
try {
guestWaitHtml = Assets.getText('static/guest-wait/guest-wait.html');
} catch (e) {
Logger.warn(`Could not process guest wait html file: ${e}`);
}
}
res.setHeader('Content-Type', 'text/html');
res.writeHead(200);
res.end(guestWaitHtml);
});

View File

@ -79,12 +79,12 @@ const ConnectionManager: React.FC<ConnectionManagerProps> = ({ children }): Reac
throw new Error('Error fetching GraphQL URL: '.concat(error.message || ''));
});
logger.info('Fetching GraphQL URL');
loadingContextInfo.setLoading(true, '1/4');
loadingContextInfo.setLoading(true, '1/5');
}, []);
useEffect(() => {
logger.info('Connecting to GraphQL server');
loadingContextInfo.setLoading(true, '2/4');
loadingContextInfo.setLoading(true, '2/5');
if (graphqlUrl) {
const urlParams = new URLSearchParams(window.location.search);
const sessionToken = urlParams.get('sessionToken');

View File

@ -0,0 +1,199 @@
import React, {
useCallback,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { LoadingContext } from '../../common/loading-screen/loading-screen-HOC/component';
import Styled from './styles';
const REDIRECT_TIMEOUT = 15000;
export const GUEST_STATUSES = {
ALLOW: 'ALLOW',
DENY: 'DENY',
WAIT: 'WAIT',
};
const intlMessages = defineMessages({
windowTitle: {
id: 'app.guest.windowTitle',
description: 'tab title',
},
guestWait: {
id: 'app.guest.guestWait',
description: '',
},
noSessionToken: {
id: 'app.guest.noSessionToken',
description: '',
},
guestInvalid: {
id: 'app.guest.guestInvalid',
description: '',
},
allow: {
id: 'app.guest.allow',
description: '',
},
deny: {
id: 'app.guest.guestDeny',
description: '',
},
firstPosition: {
id: 'app.guest.firstPositionInWaitingQueue',
description: '',
},
position: {
id: 'app.guest.positionInWaitingQueue',
description: '',
},
calculating: {
id: 'app.guest.calculating',
description: '',
},
});
function getSearchParam(name: string) {
const params = new URLSearchParams(window.location.search);
if (params && params.has(name)) {
const param = params.get(name);
return param;
}
return null;
}
interface GuestWaitProps {
guestStatus: string | null;
guestLobbyMessage: string | null;
positionInWaitingQueue: number | null;
logoutUrl: string;
}
const GuestWait: React.FC<GuestWaitProps> = (props) => {
const {
guestLobbyMessage,
guestStatus,
logoutUrl,
positionInWaitingQueue,
} = props;
const intl = useIntl();
const [animate, setAnimate] = useState(true);
const [message, setMessage] = useState(intl.formatMessage(intlMessages.guestWait));
const [positionMessage, setPositionMessage] = useState(intl.formatMessage(intlMessages.calculating));
const lobbyMessageRef = useRef('');
const positionInWaitingQueueRef = useRef('');
const loadingContextInfo = useContext(LoadingContext);
const updateLobbyMessage = useCallback((message: string | null) => {
if (!message) {
setMessage(intl.formatMessage(intlMessages.guestWait));
return;
}
if (message !== lobbyMessageRef.current) {
lobbyMessageRef.current = message;
if (lobbyMessageRef.current.length !== 0) {
setMessage(lobbyMessageRef.current);
} else {
setMessage(intl.formatMessage(intlMessages.guestWait));
}
}
}, [intl]);
const updatePositionInWaitingQueue = useCallback((newPositionInWaitingQueue: number) => {
if (positionInWaitingQueueRef.current !== newPositionInWaitingQueue.toString()) {
positionInWaitingQueueRef.current = newPositionInWaitingQueue.toString();
if (positionInWaitingQueueRef.current === '1') {
setPositionMessage(intl.formatMessage(intlMessages.firstPosition));
} else {
setPositionMessage(intl.formatMessage(intlMessages.position) + positionInWaitingQueueRef.current);
}
}
}, [intl]);
useEffect(() => {
document.title = intl.formatMessage(intlMessages.windowTitle);
}, []);
useEffect(() => {
const sessionToken = getSearchParam('sessionToken');
if (loadingContextInfo.isLoading) {
loadingContextInfo.setLoading(false, '');
}
if (!sessionToken) {
setAnimate(false);
setMessage(intl.formatMessage(intlMessages.noSessionToken));
return;
}
if (!guestStatus) {
setAnimate(false);
setPositionMessage('');
setMessage(intl.formatMessage(intlMessages.guestInvalid));
return;
}
if (guestStatus === GUEST_STATUSES.ALLOW) {
setPositionMessage('');
updateLobbyMessage(intl.formatMessage(intlMessages.allow));
setAnimate(false);
return;
}
if (guestStatus === GUEST_STATUSES.DENY) {
setAnimate(false);
setPositionMessage('');
setMessage(intl.formatMessage(intlMessages.deny));
setTimeout(() => {
window.location.assign(logoutUrl);
}, REDIRECT_TIMEOUT);
return;
}
// WAIT
updateLobbyMessage(guestLobbyMessage || '');
if (positionInWaitingQueue) {
updatePositionInWaitingQueue(positionInWaitingQueue);
}
}, [
guestLobbyMessage,
guestStatus,
logoutUrl,
positionInWaitingQueue,
intl,
updateLobbyMessage,
updatePositionInWaitingQueue,
]);
return (
<Styled.Container>
<Styled.Content id="content">
<Styled.Heading as="h2">3/5</Styled.Heading>
<Styled.Heading id="heading">{intl.formatMessage(intlMessages.windowTitle)}</Styled.Heading>
{animate && (
<Styled.Spinner>
<Styled.Bounce1 />
<Styled.Bounce2 />
<Styled.Bounce />
</Styled.Spinner>
)}
<p aria-live="polite" data-test="guestMessage">
{message}
</p>
<Styled.Position id="positionInWaitingQueue">
<p aria-live="polite">{positionMessage}</p>
</Styled.Position>
</Styled.Content>
</Styled.Container>
);
};
export default GuestWait;

View File

@ -0,0 +1,71 @@
import styled, { keyframes } from 'styled-components';
const Container = styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
`;
const Content = styled.div`
text-align: center;
color: white;
font-weight: bold;
font-size: 24px;
`;
const Heading = styled.h1`
font-size: 2rem;
`;
const Position = styled.div`
align-items: center;
text-align: center;
`;
const sk_bouncedelay = keyframes`
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1.0);
}
`;
const Spinner = styled.div`
margin: 20px auto;
font-size: 0px;
`;
const Bounce = styled.div`
width: 18px;
height: 18px;
margin: 0 5px;
background-color: rgb(255, 255, 255);
display: inline-block;
border-radius: 100%;
animation: ${sk_bouncedelay} calc(1.4s) infinite ease-in-out both;
`;
const Bounce1 = styled(Bounce)`
animation-delay: -0.32s;
`;
const Bounce2 = styled(Bounce)`
animation-delay: -0.16s;
`;
export default {
Container,
Content,
Heading,
Position,
Bounce,
Bounce1,
Bounce2,
Spinner,
};

View File

@ -1,5 +1,5 @@
import { useMutation, useQuery } from '@apollo/client';
import React, { useContext, useEffect } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { Session } from 'meteor/session';
import {
getUserCurrent,
@ -15,8 +15,10 @@ import { LoadingContext } from '../../common/loading-screen/loading-screen-HOC/c
import useDeduplicatedSubscription from '/imports/ui/core/hooks/useDeduplicatedSubscription';
import logger from '/imports/startup/client/logger';
import deviceInfo from '/imports/utils/deviceInfo';
import GuestWaitContainer, { GUEST_STATUSES } from '../guest-wait/component';
const connectionTimeout = 60000;
const MESSAGE_TIMEOUT = 3000;
interface PresenceManagerContainerProps {
children: React.ReactNode;
@ -41,6 +43,9 @@ interface PresenceManagerProps extends PresenceManagerContainerProps {
bannerText: string;
customLogoUrl: string;
loggedOut: boolean;
guestStatus: string;
guestLobbyMessage: string | null;
positionInWaitingQueue: number | null;
}
const PresenceManager: React.FC<PresenceManagerProps> = ({
@ -63,18 +68,28 @@ const PresenceManager: React.FC<PresenceManagerProps> = ({
bannerText,
customLogoUrl,
loggedOut,
guestLobbyMessage,
guestStatus,
positionInWaitingQueue,
}) => {
const [allowToRender, setAllowToRender] = React.useState(false);
const [dispatchUserJoin] = useMutation(userJoinMutation);
const timeoutRef = React.useRef<ReturnType<typeof setTimeout>>();
const loadingContextInfo = useContext(LoadingContext);
const [isGuestAllowed, setIsGuestAllowed] = useState(guestStatus === GUEST_STATUSES.ALLOW);
useEffect(() => {
timeoutRef.current = setTimeout(() => {
loadingContextInfo.setLoading(false, '');
throw new Error('Authentication timeout');
}, connectionTimeout);
const allowed = guestStatus === GUEST_STATUSES.ALLOW;
if (allowed) {
setTimeout(() => {
setIsGuestAllowed(true);
}, MESSAGE_TIMEOUT);
} else {
setIsGuestAllowed(false);
}
}, [guestStatus]);
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const sessionToken = urlParams.get('sessionToken') as string;
setAuthData({
@ -100,6 +115,15 @@ const PresenceManager: React.FC<PresenceManagerProps> = ({
});
}, []);
useEffect(() => {
if (isGuestAllowed) {
timeoutRef.current = setTimeout(() => {
loadingContextInfo.setLoading(false, '');
throw new Error('Authentication timeout');
}, connectionTimeout);
}
}, [isGuestAllowed]);
useEffect(() => {
if (bannerColor || bannerText) {
Session.set('bannerText', bannerText);
@ -108,7 +132,7 @@ const PresenceManager: React.FC<PresenceManagerProps> = ({
}, [bannerColor, bannerText]);
useEffect(() => {
if (authToken && !joined) {
if (authToken && !joined && isGuestAllowed) {
dispatchUserJoin({
variables: {
authToken,
@ -117,7 +141,7 @@ const PresenceManager: React.FC<PresenceManagerProps> = ({
},
});
}
}, [joined, authToken]);
}, [joined, authToken, isGuestAllowed]);
useEffect(() => {
if (joined) {
@ -149,6 +173,18 @@ const PresenceManager: React.FC<PresenceManagerProps> = ({
)
: null
}
{
!isGuestAllowed && !(meetingEnded || joinErrorCode || ejectReasonCode || loggedOut)
? (
<GuestWaitContainer
guestLobbyMessage={guestLobbyMessage}
guestStatus={guestStatus}
logoutUrl={logoutUrl}
positionInWaitingQueue={positionInWaitingQueue}
/>
)
: null
}
</>
);
};
@ -182,6 +218,8 @@ const PresenceManagerContainer: React.FC<PresenceManagerContainerProps> = ({ chi
ejectReasonCode,
meeting,
loggedOut,
guestStatusDetails,
guestStatus,
} = data.user_current[0];
const {
logoutUrl,
@ -213,6 +251,9 @@ const PresenceManagerContainer: React.FC<PresenceManagerContainerProps> = ({ chi
bannerText={bannerText}
loggedOut={loggedOut}
customLogoUrl={customLogoUrl}
guestLobbyMessage={guestStatusDetails?.guestLobbyMessage ?? null}
positionInWaitingQueue={guestStatusDetails?.positionInWaitingQueue ?? null}
guestStatus={guestStatus}
>
{children}
</PresenceManager>

View File

@ -9,10 +9,17 @@ export interface GetUserCurrentResponse {
joinErrorMessage: string;
ejectReasonCode: string;
loggedOut: boolean;
guestStatus: string;
guestStatusDetails: {
guestLobbyMessage: string | null;
positionInWaitingQueue: number;
isAllowed: boolean;
} | null;
meeting: {
ended: boolean;
endedReasonCode: string;
endedByUserName: string;
logoutUrl: string;
};
}>;
}
@ -61,10 +68,17 @@ subscription getUserCurrent {
joined
ejectReasonCode
loggedOut
guestStatus
meeting {
ended
endedReasonCode
endedByUserName
logoutUrl
}
guestStatusDetails {
guestLobbyMessage
positionInWaitingQueue
isAllowed
}
}
}

View File

@ -23,7 +23,7 @@ const SettingsLoader: React.FC = () => {
const loadingContextInfo = useContext(LoadingContext);
useEffect(() => {
logger.info('Fetching settings');
loadingContextInfo.setLoading(true, '3/4');
loadingContextInfo.setLoading(true, '4/5');
}, []);
useEffect(() => {

View File

@ -1,393 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>BigBlueButton - Guest Lobby</title>
<meta charset="UTF-8">
<style>
/* vietnamese */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 300;
src: local('Source Sans Pro Light'), local('SourceSansPro-Light'),
url('fonts/SourceSansPro/SourceSansPro-Light.woff') format('woff');
unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 300;
src: local('Source Sans Pro Light'), local('SourceSansPro-Light'),
url('fonts/SourceSansPro/SourceSansPro-Light.woff') format('woff');
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 300;
src: local('Source Sans Pro Light'), local('SourceSansPro-Light'),
url('fonts/SourceSansPro/SourceSansPro-Light.woff') format('woff');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
}
:root {
--enableAnimation: 1;
}
body {
display: flex;
justify-content: center;
align-items: center;
background: #06172A;
height: 100vh;
margin: 0;
}
#content {
text-align: center;
color: white;
font-weight: bold;
font-size: 24px;
font-family: 'Source Sans Pro', arial, sans-serif;
}
#content h1 {
font-size: 2rem;
}
.spinner {
margin: 20px auto;
font-size: 0px;
}
.spinner .bounce1 {
animation-delay: -0.32s;
}
.spinner .bounce2 {
animation-delay: -0.16s;
}
.spinner>div {
width: 18px;
height: 18px;
margin: 0 5px;
background-color: rgb(255, 255, 255);
display: inline-block;
border-radius: 100%;
animation: sk-bouncedelay calc(var(--enableAnimation) * 1.4s) infinite ease-in-out both;
}
#positionInWaitingQueue {
align-items: center;
text-align: center;
}
@-webkit-keyframes sk-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0)
}
40% {
-webkit-transform: scale(1.0)
}
}
@keyframes sk-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0);
transform: scale(0);
}
40% {
-webkit-transform: scale(1.0);
transform: scale(1.0);
}
}
</style>
<script type="text/javascript">
let messages = {};
function _(message) {
return messages[message];
}
const REDIRECT_TIMEOUT = 15000;
const MESSAGE_TIMEOUT = 3000;
function updateMessage(message) {
document.querySelector('#content > p').innerHTML = message;
}
let lobbyMessage = '';
function updateLobbyMessage(message) {
if (message !== lobbyMessage) {
lobbyMessage = message;
if (lobbyMessage.length !== 0) {
updateMessage(lobbyMessage);
} else {
updateMessage(_('app.guest.guestWait'));
}
}
}
function updatePositionOnPage(message, currentPosition) {
document.querySelector('#positionInWaitingQueue > p').innerHTML = message + currentPosition;
}
function stopUpdatingWaitingPosition() {
document.querySelector('#positionInWaitingQueue > p').innerHTML = '';
}
let positionInWaitingQueue = '';
function updatePositionInWaitingQueue(newPositionInWaitingQueue) {
if (positionInWaitingQueue !== newPositionInWaitingQueue) {
positionInWaitingQueue = newPositionInWaitingQueue.toString();
if (positionInWaitingQueue === '1') {
updatePositionOnPage(_('app.guest.firstPositionInWaitingQueue'), '');
} else {
updatePositionOnPage(_('app.guest.positionInWaitingQueue'), positionInWaitingQueue);
}
}
}
function getSearchParam(name) {
const params = new URLSearchParams(window.location.search);
if (params && params.has(name)) {
const param = params.get(name);
return param;
}
return null;
}
function getClientJoinUrl() {
const joinEndpoint = '/html5client/join';
const sessionToken = getSearchParam('sessionToken');
const url = new URL(`${window.location.origin}${joinEndpoint}`);
url.search = `sessionToken=${sessionToken}`;
return url;
}
async function fetchLocalizedMessages() {
const DEFAULT_LANGUAGE = 'en';
const LOCALES_ENDPOINT = '/html5client/locale';
const url = new URL(`${window.location.origin}${LOCALES_ENDPOINT}`);
const overrideLocale = getSearchParam('locale');
url.search = overrideLocale
? `locale=${overrideLocale}`
: `locale=${navigator.language}&init=true`;
document.getElementsByTagName('html')[0].lang = overrideLocale || navigator.language;
const localesPath = 'locales';
fetch(url)
.then((response) => {
if (!response.ok) {
return false;
}
return response.json();
})
.then(({ normalizedLocale, regionDefaultLocale }) => {
const fetchFallbackMessages = new Promise((resolve, reject) => {
fetch(`${localesPath}/${DEFAULT_LANGUAGE}.json`)
.then((response) => {
if (!response.ok) {
return reject();
}
return resolve(response.json());
});
});
const fetchRegionMessages = new Promise((resolve) => {
if (!regionDefaultLocale) {
return resolve(false);
}
fetch(`${localesPath}/${regionDefaultLocale}.json`)
.then((response) => {
if (!response.ok) {
return resolve(false);
}
return response.json()
.then((jsonResponse) => resolve(jsonResponse))
.catch(() => {
resolve(false);
});
});
});
const fetchSpecificMessages = new Promise((resolve) => {
if (!normalizedLocale || normalizedLocale === DEFAULT_LANGUAGE || normalizedLocale === regionDefaultLocale) {
return resolve(false);
}
fetch(`${localesPath}/${normalizedLocale}.json`)
.then((response) => {
if (!response.ok) {
return resolve(false);
}
return response.json()
.then((jsonResponse) => resolve(jsonResponse))
.catch(() => {
resolve(false);
});
});
});
Promise.all([fetchFallbackMessages, fetchRegionMessages, fetchSpecificMessages])
.then((values) => {
let mergedMessages = Object.assign({}, values[0]);
if (!values[1] && !values[2]) {
normalizedLocale = DEFAULT_LANGUAGE;
} else {
if (values[1]) {
mergedMessages = Object.assign(mergedMessages, values[1]);
}
if (values[2]) {
mergedMessages = Object.assign(mergedMessages, values[2]);
}
}
messages = mergedMessages;
window.document.title = _('app.guest.windowTitle');
document.querySelector('#heading').innerHTML = _('app.guest.windowTitle');
updateMessage(_('app.guest.guestWait'));
enableAnimation();
try {
const sessionToken = getSearchParam('sessionToken');
if (!sessionToken) {
disableAnimation();
updateMessage(_('app.guest.noSessionToken'));
return;
}
pollGuestStatus(sessionToken, 0);
} catch (e) {
disableAnimation();
console.error(e);
updateMessage(_('app.guest.errorSeeConsole'));
}
})
.catch((e) => {
console.error(e);
});
});
}
function fetchUserCurrent(sessionToken) {
const GUEST_WAIT_ENDPOINT = '/api/rest/usercurrent/';
const url = new URL(`${window.location.origin}${GUEST_WAIT_ENDPOINT}`);
const headers = new Headers({
'X-Session-Token': sessionToken,
'Content-Type': 'application/json',
});
return fetch(url, { method: 'get', headers: headers });
};
function redirect(message, url) {
disableAnimation();
stopUpdatingWaitingPosition();
updateMessage(message);
setTimeout(() => {
window.location = url;
}, REDIRECT_TIMEOUT);
};
function pollGuestStatus(token, everyMs) {
setTimeout(function () {
fetchUserCurrent(token)
.then(async (resp) => await resp.json())
.then((data) => {
if(data.hasOwnProperty('error') || !data.hasOwnProperty('user_current')) {
disableAnimation();
stopUpdatingWaitingPosition();
updateMessage(_('app.guest.guestInvalid'));
return;
}
const userData = data.user_current[0];
if (userData.guestStatus === 'ALLOW') {
updateLobbyMessage(_('app.guest.allow'));
stopUpdatingWaitingPosition();
// Timeout is required by accessibility to allow viewing of the message for a minimum of 3 seconds
// before redirecting.
setTimeout(() => {
disableAnimation();
window.location = getClientJoinUrl();
}, MESSAGE_TIMEOUT);
return;
}
if (userData.guestStatus === 'DENY') {
stopUpdatingWaitingPosition();
return redirect(_('app.guest.guestDeny'), userData.meeting.logoutUrl);
}
if (userData.guestStatus === 'WAIT') {
//Wait message will be set by `updateLobbyMessage`
//updateMessage(updateMessage(_('app.guest.guestWait')));
}
if(userData.guestStatusDetails !== null) {
updatePositionInWaitingQueue(userData.guestStatusDetails.positionInWaitingQueue);
updateLobbyMessage(userData.guestStatusDetails.guestLobbyMessage || '');
}
const ATTEMPT_EVERY_MS = 10 * 1000; // 10 seconds
return pollGuestStatus(token, ATTEMPT_EVERY_MS);
});
}, everyMs);
};
function enableAnimation() {
document.documentElement.style.setProperty('--enableAnimation', 1);
}
function disableAnimation() {
document.documentElement.style.setProperty('--enableAnimation', 0);
}
window.onload = function () {
fetchLocalizedMessages();
};
</script>
</head>
<body>
<div id="content">
<h1 id="heading">BigBlueButton - Guest Lobby</h1>
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
<p aria-live="polite" data-test="guestMessage">Please wait for a moderator to approve you joining the meeting.</p>
<div id="positionInWaitingQueue">
<p aria-live="polite">Calculating position in waiting queue</p>
</div>
</div>
</body>
</html>

View File

@ -871,6 +871,7 @@
"app.guest.positionInWaitingQueue": "Your current position in waiting queue: ",
"app.guest.guestInvalid": "Guest user is invalid",
"app.guest.meetingForciblyEnded": "You cannot join a meeting that has already been forcibly ended",
"app.guest.calculating": "Calculating position in waiting queue",
"app.userList.guest.waitingUsers": "Waiting Users",
"app.userList.guest.waitingUsersTitle": "User Management",
"app.userList.guest.optionTitle": "Review Pending Users",

View File

@ -321,9 +321,6 @@ allowRequestsWithoutSession=false
# For more info, refer to javax.servlet.http.HttpSession#setMaxInactiveInterval 's spec
defaultHttpSessionTimeout=14400
# The url for where the guest will poll if approved to join or not.
defaultGuestWaitURL=${bigbluebutton.web.serverURL}/html5client/guestWait
# The default avatar image to display.
useDefaultAvatar=false
defaultAvatarURL=${bigbluebutton.web.serverURL}/html5client/resources/images/avatar.png

View File

@ -34,7 +34,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
</property>
</bean>
<bean id="waitingGuestCleanupTimerTask" class="org.bigbluebutton.web.services.WaitingGuestCleanupTimerTask"/>
<bean id="userCleanupTimerTask" class="org.bigbluebutton.web.services.UserCleanupTimerTask"/>
<bean id="enteredUserCleanupTimerTask" class="org.bigbluebutton.web.services.EnteredUserCleanupTimerTask"/>
@ -51,14 +50,12 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<property name="presDownloadService" ref="presDownloadService"/>
<property name="paramsProcessorUtil" ref="paramsProcessorUtil"/>
<property name="stunTurnService" ref="stunTurnService"/>
<property name="waitingGuestCleanupTimerTask" ref="waitingGuestCleanupTimerTask"/>
<property name="userCleanupTimerTask" ref="userCleanupTimerTask"/>
<property name="sessionsCleanupDelayInMinutes" value="${sessionsCleanupDelayInMinutes}"/>
<property name="enteredUserCleanupTimerTask" ref="enteredUserCleanupTimerTask"/>
<property name="gw" ref="bbbWebApiGWApp"/>
<property name="callbackUrlService" ref="callbackUrlService"/>
<property name="usersTimeout" value="${usersTimeout}"/>
<property name="waitingGuestUsersTimeout" value="${waitingGuestUsersTimeout}"/>
<property name="enteredUsersTimeout" value="${enteredUsersTimeout}"/>
<property name="slidesGenerationProgressNotifier" ref="slidesGenerationProgressNotifier"/>
</bean>
@ -147,7 +144,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<property name="graphqlWebsocketUrl" value="${graphqlWebsocketUrl}"/>
<property name="useDefaultLogo" value="${useDefaultLogo}"/>
<property name="defaultLogoURL" value="${defaultLogoURL}"/>
<property name="defaultGuestWaitURL" value="${defaultGuestWaitURL}"/>
<property name="allowRequestsWithoutSession" value="${allowRequestsWithoutSession}"/>
<property name="defaultHttpSessionTimeout" value="${defaultHttpSessionTimeout}"/>
<property name="defaultMeetingDuration" value="${defaultMeetingDuration}"/>
@ -165,6 +161,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<property name="defaultAvatarURL" value="${defaultAvatarURL}"/>
<property name="defaultGuestPolicy" value="${defaultGuestPolicy}"/>
<property name="authenticatedGuest" value="${authenticatedGuest}"/>
<property name="waitingGuestUsersTimeout" value="${waitingGuestUsersTimeout}"/>
<property name="defaultMeetingLayout" value="${defaultMeetingLayout}"/>
<property name="meetingExpireIfNoUserJoinedInMinutes" value="${meetingExpireIfNoUserJoinedInMinutes}"/>
<property name="meetingExpireWhenLastUserLeftInMinutes" value="${meetingExpireWhenLastUserLeftInMinutes}"/>

View File

@ -508,17 +508,7 @@ class ApiController {
// Process if we send the user directly to the client or
// have it wait for approval.
String destUrl = clientURL + "?sessionToken=" + sessionToken
if (guestStatusVal.equals(GuestPolicy.WAIT)) {
String guestWaitUrl = paramsProcessorUtil.getDefaultGuestWaitURL();
destUrl = guestWaitUrl + "?sessionToken=" + sessionToken
// Check if the user has her/his default locale overridden by an userdata
String customLocale = userCustomData.get("bbb_override_default_locale")
if (customLocale != null) {
destUrl += "&locale=" + customLocale
}
msgKey = "guestWait"
msgValue = "Guest waiting for approval to join meeting."
} else if (guestStatusVal.equals(GuestPolicy.DENY)) {
if (guestStatusVal == GuestPolicy.DENY) {
invalid("guestDeniedAccess", "You have been denied access to this meeting based on the meeting's guest policy", redirectClient, errorRedirectUrl)
return
}
@ -744,115 +734,6 @@ class ApiController {
return reqParams;
}
/**********************************************
* GUEST WAIT API
*********************************************/
def guestWaitHandler = {
String API_CALL = 'guestWait'
log.debug CONTROLLER_NAME + "#${API_CALL}"
String msgKey = "defaultKey"
String msgValue = "defaultValue"
String destURL = paramsProcessorUtil.getDefaultLogoutUrl()
Map.Entry<String, String> validationResponse = validateRequest(
ValidationService.ApiCall.GUEST_WAIT,
request
)
if(!(validationResponse == null)) {
msgKey = validationResponse.getKey()
msgValue = validationResponse.getValue()
respondWithJSONError(msgKey, msgValue, destURL)
return
}
String sessionToken = sanitizeSessionToken(params.sessionToken)
UserSession us = getUserSession(sessionToken)
Meeting meeting = meetingService.getMeeting(us.meetingID)
String status = us.guestStatus
destURL = us.clientUrl
String posInWaitingQueue = meeting.getWaitingPositionsInWaitingQueue(us.internalUserId)
String lobbyMsg = meeting.getGuestLobbyMessage(us.internalUserId)
Boolean redirectClient = true
if (!StringUtils.isEmpty(params.redirect)) {
try {
redirectClient = Boolean.parseBoolean(params.redirect)
} catch (Exception e) {
redirectClient = true
}
}
String guestURL = paramsProcessorUtil.getDefaultGuestWaitURL() + "?sessionToken=" + sessionToken
switch (status) {
case GuestPolicy.WAIT:
meetingService.guestIsWaiting(us.meetingID, us.internalUserId)
destURL = guestURL
msgKey = "guestWait"
msgValue = "Please wait for a moderator to approve you joining the meeting."
// We force the response to not do a redirect. Otherwise,
// the client would just be redirecting into this endpoint.
redirectClient = false
break
case GuestPolicy.DENY:
destURL = meeting.getLogoutUrl()
msgKey = "guestDeny"
msgValue = "Guest denied of joining the meeting."
redirectClient = false
break
case GuestPolicy.ALLOW:
// IF the user was allowed to join but there is no room available in
// the meeting we must hold his approval
if (hasReachedMaxParticipants(meeting, us)) {
meetingService.guestIsWaiting(us.meetingID, us.internalUserId)
destURL = guestURL
msgKey = "seatWait"
msgValue = "Guest waiting for a seat in the meeting."
redirectClient = false
status = GuestPolicy.WAIT
}
break
default:
break
}
if(meeting.didGuestUserLeaveGuestLobby(us.internalUserId)){
destURL = meeting.getLogoutUrl()
msgKey = "guestInvalid"
msgValue = "Invalid user"
status = GuestPolicy.DENY
redirectClient = false
}
if (redirectClient) {
// User may join the meeting
redirect(url: destURL)
} else {
response.addHeader("Cache-Control", "no-cache")
withFormat {
json {
def builder = new JsonBuilder()
builder.response {
returncode RESP_CODE_SUCCESS
messageKey msgKey
message msgValue
meeting_id us.meetingID
user_id us.internalUserId
auth_token us.authToken
session_token session[sessionToken]
guestStatus status
lobbyMessage lobbyMsg
url destURL
positionInWaitingQueue posInWaitingQueue
}
render(contentType: "application/json", text: builder.toPrettyString())
}
}
}
}
/***********************************************
* ENTER API
***********************************************/

View File

@ -97,7 +97,6 @@ Add these options to `/etc/bigbluebutton/bbb-web.properties`:
defaultHTML5ClientUrl=https://bbb-proxy.example.com/bbb-01/html5client/join
presentationBaseURL=https://bbb-01.example.com/bigbluebutton/presentation
accessControlAllowOrigin=https://bbb-proxy.example.com
defaultGuestWaitURL=https://bbb-01.example.com/bbb-01/html5client/guestWait
graphqlWebsocketUrl=wss://bbb-01.example.com/v1/graphql
```

View File

@ -1249,8 +1249,6 @@ Do the same in `/usr/share/bbb-web/WEB-INF/classes/bigbluebutton.properties` in
```
defaultHTML5ClientUrl=${bigbluebutton.web.serverURL}/html5client/join
defaultGuestWaitURL=${bigbluebutton.web.serverURL}/html5client/guestWait
```
In configuration file for the HTML5 client, located in `/usr/share/meteor/bundle/programs/server/assets/app/config/settings.yml`, change the entry for `public.app.basename`: