From 0365018e9244d02368e91892a03869762b524e8d Mon Sep 17 00:00:00 2001
From: Pedro Beschorner Marin <pedrobmarin@gmail.com>
Date: Fri, 2 Oct 2020 14:29:27 -0300
Subject: [PATCH] Add guest lobby messages

Moderators are able to send a message to the meeting's guest lobby. This new
event reaches bbb-web and is sent to the guest user with her/his status response
while polling. All guest users that are waiting for acceptance will be able to
read this message.

enableGuestLobbyMessage is disabled by default.
---
 .../bigbluebutton/core/apps/GuestsApp.scala   |  1 +
 .../core/models/GuestsWaiting.scala           |  7 ++
 .../senders/ReceivedJsonMsgHandlerActor.scala |  2 +
 .../core/running/MeetingActor.scala           |  1 +
 .../bigbluebutton/core2/AnalyticsActor.scala  |  2 +
 .../guests/SetGuestLobbyMessageMsgHdlr.scala  | 31 +++++++
 .../core2/message/senders/MsgBuilder.scala    | 11 +++
 .../common2/msgs/GuestsMsgs.scala             | 20 +++++
 .../org/bigbluebutton/api/MeetingService.java | 10 +++
 .../org/bigbluebutton/api/domain/Meeting.java |  9 +++
 .../messages/GuestLobbyMessageChanged.java    | 11 +++
 .../api2/bus/ReceivedJsonMsgHdlrActor.scala   |  2 +
 .../api2/meeting/OldMeetingMsgHdlrActor.scala |  5 ++
 .../imports/api/guest-users/server/methods.js |  2 +
 .../server/methods/setGuestLobbyMessage.js    | 24 ++++++
 .../api/meetings/server/eventHandlers.js      |  2 +
 .../handlers/guestLobbyMessageChanged.js      | 11 +++
 .../meetings/server/modifiers/addMeeting.js   |  1 +
 .../server/modifiers/setGuestLobbyMessage.js  | 32 ++++++++
 .../ui/components/text-input/component.jsx    | 81 +++++++++++++++++++
 .../ui/components/text-input/styles.scss      | 53 ++++++++++++
 .../ui/components/waiting-users/component.jsx | 22 +++++
 .../ui/components/waiting-users/container.jsx |  3 +
 .../ui/components/waiting-users/service.js    | 21 +++++
 .../ui/components/waiting-users/styles.scss   | 12 +++
 .../private/config/settings.yml               |  1 +
 bigbluebutton-html5/private/locales/en.json   |  3 +
 .../private/static/guest-wait/guest-wait.html | 19 ++++-
 .../web/controllers/ApiController.groovy      |  2 +
 29 files changed, 398 insertions(+), 3 deletions(-)
 create mode 100755 akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/guests/SetGuestLobbyMessageMsgHdlr.scala
 create mode 100755 bbb-common-web/src/main/java/org/bigbluebutton/api/messaging/messages/GuestLobbyMessageChanged.java
 create mode 100644 bigbluebutton-html5/imports/api/guest-users/server/methods/setGuestLobbyMessage.js
 create mode 100644 bigbluebutton-html5/imports/api/meetings/server/handlers/guestLobbyMessageChanged.js
 create mode 100644 bigbluebutton-html5/imports/api/meetings/server/modifiers/setGuestLobbyMessage.js
 create mode 100644 bigbluebutton-html5/imports/ui/components/text-input/component.jsx
 create mode 100644 bigbluebutton-html5/imports/ui/components/text-input/styles.scss

diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/GuestsApp.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/GuestsApp.scala
index 823e999df6..9c97de077f 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/GuestsApp.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/GuestsApp.scala
@@ -7,6 +7,7 @@ trait GuestsApp extends GetGuestsWaitingApprovalReqMsgHdlr
   with GuestsWaitingApprovedMsgHdlr
   with GuestWaitingLeftMsgHdlr
   with SetGuestPolicyMsgHdlr
+  with SetGuestLobbyMessageMsgHdlr
   with GetGuestPolicyReqMsgHdlr {
 
   this: MeetingActor =>
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/GuestsWaiting.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/GuestsWaiting.scala
index 88339acf05..67eedfcb48 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/GuestsWaiting.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/GuestsWaiting.scala
@@ -24,6 +24,9 @@ object GuestsWaiting {
     guests.setGuestPolicy(policy)
   }
 
+  def setGuestLobbyMessage(guests: GuestsWaiting, message: String): Unit = {
+    guests.setGuestLobbyMessage(message)
+  }
 }
 
 class GuestsWaiting {
@@ -31,6 +34,8 @@ class GuestsWaiting {
 
   private var guestPolicy = GuestPolicy(GuestPolicyType.ALWAYS_ACCEPT, SystemUser.ID)
 
+  private var guestLobbyMessage = ""
+
   private def toVector: Vector[GuestWaiting] = guests.values.toVector
 
   private def save(user: GuestWaiting): GuestWaiting = {
@@ -49,6 +54,8 @@ class GuestsWaiting {
 
   def getGuestPolicy(): GuestPolicy = guestPolicy
   def setGuestPolicy(policy: GuestPolicy) = guestPolicy = policy
+
+  def setGuestLobbyMessage(message: String) = guestLobbyMessage = message
 }
 
 case class GuestWaiting(intId: String, name: String, role: String, guest: Boolean, avatar: String, authenticated: Boolean, registeredOn: Long)
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala
index 497e168bb5..fcb3dcf925 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala
@@ -95,6 +95,8 @@ class ReceivedJsonMsgHandlerActor(
         routeGenericMsg[SetGuestPolicyCmdMsg](envelope, jsonNode)
       case GetGuestPolicyReqMsg.NAME =>
         routeGenericMsg[GetGuestPolicyReqMsg](envelope, jsonNode)
+      case SetGuestLobbyMessageCmdMsg.NAME =>
+        routeGenericMsg[SetGuestLobbyMessageCmdMsg](envelope, jsonNode)
 
       // Users
       case GetUsersMeetingReqMsg.NAME =>
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala
index 98172c3dae..066935415b 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala
@@ -443,6 +443,7 @@ class MeetingActor(
       // Guests
       case m: GetGuestsWaitingApprovalReqMsg           => handleGetGuestsWaitingApprovalReqMsg(m)
       case m: SetGuestPolicyCmdMsg                     => handleSetGuestPolicyMsg(m)
+      case m: SetGuestLobbyMessageCmdMsg               => handleSetGuestLobbyMessageMsg(m)
       case m: GuestsWaitingApprovedMsg                 => handleGuestsWaitingApprovedMsg(m)
       case m: GuestWaitingLeftMsg                      => handleGuestWaitingLeftMsg(m)
       case m: GetGuestPolicyReqMsg                     => handleGetGuestPolicyReqMsg(m)
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala
index 7c08cdfdfd..608f9c2d84 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala
@@ -139,6 +139,8 @@ class AnalyticsActor extends Actor with ActorLogging {
       case m: GuestsWaitingForApprovalEvtMsg => logMessage(msg)
       case m: SetGuestPolicyCmdMsg => logMessage(msg)
       case m: GuestPolicyChangedEvtMsg => logMessage(msg)
+      case m: SetGuestLobbyMessageCmdMsg => logMessage(msg)
+      case m: GuestLobbyMessageChangedEvtMsg => logMessage(msg)
 
       // System
       case m: ClientToServerLatencyTracerMsg => traceMessage(msg)
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/guests/SetGuestLobbyMessageMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/guests/SetGuestLobbyMessageMsgHdlr.scala
new file mode 100755
index 0000000000..dc5ad18a1d
--- /dev/null
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/guests/SetGuestLobbyMessageMsgHdlr.scala
@@ -0,0 +1,31 @@
+package org.bigbluebutton.core2.message.handlers.guests
+
+import org.bigbluebutton.common2.msgs.SetGuestLobbyMessageCmdMsg
+import org.bigbluebutton.core.models.{ GuestsWaiting }
+import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
+import org.bigbluebutton.core2.message.senders.MsgBuilder
+import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
+import org.bigbluebutton.core.running.MeetingActor
+
+trait SetGuestLobbyMessageMsgHdlr extends RightsManagementTrait {
+  this: MeetingActor =>
+
+  val liveMeeting: LiveMeeting
+  val outGW: OutMsgRouter
+
+  def handleSetGuestLobbyMessageMsg(msg: SetGuestLobbyMessageCmdMsg): Unit = {
+    if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
+      val meetingId = liveMeeting.props.meetingProp.intId
+      val reason = "No permission to set guest lobby message in meeting."
+      PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)
+    } else {
+      GuestsWaiting.setGuestLobbyMessage(liveMeeting.guestsWaiting, msg.body.message)
+      val event = MsgBuilder.buildGuestLobbyMessageChangedEvtMsg(
+        liveMeeting.props.meetingProp.intId,
+        msg.header.userId,
+        msg.body.message
+      )
+      outGW.send(event)
+    }
+  }
+}
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/senders/MsgBuilder.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/senders/MsgBuilder.scala
index 82f228180c..cebf928937 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/senders/MsgBuilder.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/senders/MsgBuilder.scala
@@ -16,6 +16,17 @@ object MsgBuilder {
     BbbCommonEnvCoreMsg(envelope, event)
   }
 
+  def buildGuestLobbyMessageChangedEvtMsg(meetingId: String, userId: String, message: String): BbbCommonEnvCoreMsg = {
+    val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId)
+    val envelope = BbbCoreEnvelope(GuestLobbyMessageChangedEvtMsg.NAME, routing)
+    val header = BbbClientMsgHeader(GuestLobbyMessageChangedEvtMsg.NAME, meetingId, userId)
+
+    val body = GuestLobbyMessageChangedEvtMsgBody(message)
+    val event = GuestLobbyMessageChangedEvtMsg(header, body)
+
+    BbbCommonEnvCoreMsg(envelope, event)
+  }
+
   def buildGuestApprovedEvtMsg(meetingId: String, userId: String, status: String, approvedBy: String): BbbCommonEnvCoreMsg = {
     val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, userId)
     val envelope = BbbCoreEnvelope(GuestApprovedEvtMsg.NAME, routing)
diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/GuestsMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/GuestsMsgs.scala
index fd438b24b8..308c481f56 100755
--- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/GuestsMsgs.scala
+++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/GuestsMsgs.scala
@@ -103,6 +103,26 @@ case class GuestPolicyChangedEvtMsg(
 ) extends BbbCoreMsg
 case class GuestPolicyChangedEvtMsgBody(policy: String, setBy: String)
 
+/**
+ * Message from user to set the guest lobby message.
+ */
+object SetGuestLobbyMessageCmdMsg { val NAME = "SetGuestLobbyMessageCmdMsg" }
+case class SetGuestLobbyMessageCmdMsg(
+    header: BbbClientMsgHeader,
+    body:   SetGuestLobbyMessageCmdMsgBody
+) extends StandardMsg
+case class SetGuestLobbyMessageCmdMsgBody(message: String)
+
+/**
+ * Message sent to all clients that guest lobby message has been changed.
+ */
+object GuestLobbyMessageChangedEvtMsg { val NAME = "GuestLobbyMessageChangedEvtMsg" }
+case class GuestLobbyMessageChangedEvtMsg(
+    header: BbbClientMsgHeader,
+    body:   GuestLobbyMessageChangedEvtMsgBody
+) extends BbbCoreMsg
+case class GuestLobbyMessageChangedEvtMsgBody(message: String)
+
 /**
  * Message from user to get the guest policy.
  */
diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java
index 814736ff60..fad6f60344 100755
--- a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java
+++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java
@@ -61,6 +61,7 @@ import org.bigbluebutton.api.messaging.messages.CreateBreakoutRoom;
 import org.bigbluebutton.api.messaging.messages.CreateMeeting;
 import org.bigbluebutton.api.messaging.messages.EndMeeting;
 import org.bigbluebutton.api.messaging.messages.GuestPolicyChanged;
+import org.bigbluebutton.api.messaging.messages.GuestLobbyMessageChanged;
 import org.bigbluebutton.api.messaging.messages.GuestStatusChangedEventMsg;
 import org.bigbluebutton.api.messaging.messages.GuestsStatus;
 import org.bigbluebutton.api.messaging.messages.IMessage;
@@ -1101,6 +1102,8 @@ public class MeetingService implements MessageListener {
           processGuestStatusChangedEventMsg((GuestStatusChangedEventMsg) message);
         } else if (message instanceof GuestPolicyChanged) {
           processGuestPolicyChanged((GuestPolicyChanged) message);
+        } else if (message instanceof GuestLobbyMessageChanged) {
+          processGuestLobbyMessageChanged((GuestLobbyMessageChanged) message);
         } else if (message instanceof RecordChapterBreak) {
           processRecordingChapterBreak((RecordChapterBreak) message);
         } else if (message instanceof AddPad) {
@@ -1125,6 +1128,13 @@ public class MeetingService implements MessageListener {
     }
   }
 
+  public void processGuestLobbyMessageChanged(GuestLobbyMessageChanged msg) {
+    Meeting m = getMeeting(msg.meetingId);
+    if (m != null) {
+      m.setGuestLobbyMessage(msg.message);
+    }
+  }
+
   public void processAddPad(AddPad msg) {
     Meeting m = getMeeting(msg.meetingId);
     if (m != null) {
diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java
index 1393228d56..e2884db344 100755
--- a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java
+++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java
@@ -68,6 +68,7 @@ public class Meeting {
 	private String defaultAvatarURL;
 	private String defaultConfigToken;
 	private String guestPolicy = GuestPolicy.ASK_MODERATOR;
+	private String guestLobbyMessage = "";
 	private Boolean authenticatedGuest = false;
 	private boolean userHasJoined = false;
 	private Map<String, String> pads;
@@ -376,6 +377,14 @@ public class Meeting {
     	return guestPolicy;
 	}
 
+	public void setGuestLobbyMessage(String message) {
+		guestLobbyMessage = message;
+	}
+
+	public String getGuestLobbyMessage() {
+		return guestLobbyMessage;
+	}
+
 	public void setAuthenticatedGuest(Boolean authGuest) {
 		authenticatedGuest = authGuest;
 	}
diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/messaging/messages/GuestLobbyMessageChanged.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/messaging/messages/GuestLobbyMessageChanged.java
new file mode 100755
index 0000000000..ca76c835e8
--- /dev/null
+++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/messaging/messages/GuestLobbyMessageChanged.java
@@ -0,0 +1,11 @@
+package org.bigbluebutton.api.messaging.messages;
+
+public class GuestLobbyMessageChanged implements IMessage {
+    public final String meetingId;
+    public final String message;
+
+    public GuestLobbyMessageChanged(String meetingId, String message) {
+        this.meetingId = meetingId;
+        this.message = message;
+    }
+}
diff --git a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/bus/ReceivedJsonMsgHdlrActor.scala b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/bus/ReceivedJsonMsgHdlrActor.scala
index 7eb216f80f..2676e1bdad 100755
--- a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/bus/ReceivedJsonMsgHdlrActor.scala
+++ b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/bus/ReceivedJsonMsgHdlrActor.scala
@@ -94,6 +94,8 @@ class ReceivedJsonMsgHdlrActor(val msgFromAkkaAppsEventBus: MsgFromAkkaAppsEvent
         route[GuestsWaitingApprovedEvtMsg](envelope, jsonNode)
       case GuestPolicyChangedEvtMsg.NAME =>
         route[GuestPolicyChangedEvtMsg](envelope, jsonNode)
+      case GuestLobbyMessageChangedEvtMsg.NAME =>
+        route[GuestLobbyMessageChangedEvtMsg](envelope, jsonNode)
       case AddPadEvtMsg.NAME =>
         route[AddPadEvtMsg](envelope, jsonNode)
       case AddCaptionsPadsEvtMsg.NAME =>
diff --git a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala
index 09fd1744a7..ac25f7610c 100755
--- a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala
+++ b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala
@@ -39,6 +39,7 @@ class OldMeetingMsgHdlrActor(val olgMsgGW: OldMessageReceivedGW)
       case m: PresentationUploadTokenSysPubMsg  => handlePresentationUploadTokenSysPubMsg(m)
       case m: GuestsWaitingApprovedEvtMsg       => handleGuestsWaitingApprovedEvtMsg(m)
       case m: GuestPolicyChangedEvtMsg          => handleGuestPolicyChangedEvtMsg(m)
+      case m: GuestLobbyMessageChangedEvtMsg    => handleGuestLobbyMessageChangedEvtMsg(m)
       case m: AddCaptionsPadsEvtMsg             => handleAddCaptionsPadsEvtMsg(m)
       case m: AddPadEvtMsg                      => handleAddPadEvtMsg(m)
       case m: RecordingChapterBreakSysMsg       => handleRecordingChapterBreakSysMsg(m)
@@ -52,6 +53,10 @@ class OldMeetingMsgHdlrActor(val olgMsgGW: OldMessageReceivedGW)
     olgMsgGW.handle(new GuestPolicyChanged(msg.header.meetingId, msg.body.policy))
   }
 
+  def handleGuestLobbyMessageChangedEvtMsg(msg: GuestLobbyMessageChangedEvtMsg): Unit = {
+    olgMsgGW.handle(new GuestLobbyMessageChanged(msg.header.meetingId, msg.body.message))
+  }
+
   def handleAddPadEvtMsg(msg: AddPadEvtMsg): Unit = {
     olgMsgGW.handle(new AddPad(msg.header.meetingId, msg.body.padId, msg.body.readOnlyId))
   }
diff --git a/bigbluebutton-html5/imports/api/guest-users/server/methods.js b/bigbluebutton-html5/imports/api/guest-users/server/methods.js
index dc7ccd93f0..f1dbcab3f7 100644
--- a/bigbluebutton-html5/imports/api/guest-users/server/methods.js
+++ b/bigbluebutton-html5/imports/api/guest-users/server/methods.js
@@ -1,8 +1,10 @@
 import { Meteor } from 'meteor/meteor';
 import allowPendingUsers from '/imports/api/guest-users/server/methods/allowPendingUsers';
 import changeGuestPolicy from '/imports/api/guest-users/server/methods/changeGuestPolicy';
+import setGuestLobbyMessage from '/imports/api/guest-users/server/methods/setGuestLobbyMessage';
 
 Meteor.methods({
   allowPendingUsers,
   changeGuestPolicy,
+  setGuestLobbyMessage,
 });
diff --git a/bigbluebutton-html5/imports/api/guest-users/server/methods/setGuestLobbyMessage.js b/bigbluebutton-html5/imports/api/guest-users/server/methods/setGuestLobbyMessage.js
new file mode 100644
index 0000000000..770ea41523
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/guest-users/server/methods/setGuestLobbyMessage.js
@@ -0,0 +1,24 @@
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import RedisPubSub from '/imports/startup/server/redis';
+import Logger from '/imports/startup/server/logger';
+import { extractCredentials } from '/imports/api/common/server/helpers';
+
+const REDIS_CONFIG = Meteor.settings.private.redis;
+const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
+const EVENT_NAME = 'SetGuestLobbyMessageCmdMsg';
+
+export default function setGuestLobbyMessage(message) {
+  check(message, String);
+
+  const { meetingId, requesterUserId } = extractCredentials(this.userId);
+
+  check(meetingId, String);
+  check(requesterUserId, String);
+
+  const payload = { message };
+
+  Logger.info(`User=${requesterUserId} set guest lobby message to ${message}`);
+
+  return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
+}
diff --git a/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js b/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js
index 81f0ef206a..e13e7c5f2d 100644
--- a/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js
+++ b/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js
@@ -4,6 +4,7 @@ import handleGetAllMeetings from './handlers/getAllMeetings';
 import handleMeetingEnd from './handlers/meetingEnd';
 import handleMeetingDestruction from './handlers/meetingDestruction';
 import handleMeetingLocksChange from './handlers/meetingLockChange';
+import handleGuestLobbyMessageChanged from './handlers/guestLobbyMessageChanged';
 import handleUserLockChange from './handlers/userLockChange';
 import handleRecordingStatusChange from './handlers/recordingStatusChange';
 import handleRecordingTimerChange from './handlers/recordingTimerChange';
@@ -21,5 +22,6 @@ RedisPubSub.on('RecordingStatusChangedEvtMsg', handleRecordingStatusChange);
 RedisPubSub.on('UpdateRecordingTimerEvtMsg', handleRecordingTimerChange);
 RedisPubSub.on('WebcamsOnlyForModeratorChangedEvtMsg', handleChangeWebcamOnlyModerator);
 RedisPubSub.on('GetLockSettingsRespMsg', handleMeetingLocksChange);
+RedisPubSub.on('GuestLobbyMessageChangedEvtMsg', handleGuestLobbyMessageChanged);
 RedisPubSub.on('MeetingTimeRemainingUpdateEvtMsg', handleTimeRemainingUpdate);
 RedisPubSub.on('SelectRandomViewerRespMsg', handleSelectRandomViewer);
diff --git a/bigbluebutton-html5/imports/api/meetings/server/handlers/guestLobbyMessageChanged.js b/bigbluebutton-html5/imports/api/meetings/server/handlers/guestLobbyMessageChanged.js
new file mode 100644
index 0000000000..e6bca30ecf
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/meetings/server/handlers/guestLobbyMessageChanged.js
@@ -0,0 +1,11 @@
+import setGuestLobbyMessage from '../modifiers/setGuestLobbyMessage';
+import { check } from 'meteor/check';
+
+export default function handleGuestLobbyMessageChanged({ body }, meetingId) {
+  const { message } = body;
+
+  check(meetingId, String);
+  check(message, String);
+
+  return setGuestLobbyMessage(meetingId, message);
+}
diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js
index a17bec63f2..a0dea94a0a 100755
--- a/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js
+++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js
@@ -148,6 +148,7 @@ export default function addMeeting(meeting) {
       meetingId,
       meetingEnded,
       publishedPoll: false,
+      guestLobbyMessage: '',
       randomlySelectedUser: '',
     }, flat(newMeeting, {
       safe: true,
diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/setGuestLobbyMessage.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/setGuestLobbyMessage.js
new file mode 100644
index 0000000000..8769aac5b5
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/setGuestLobbyMessage.js
@@ -0,0 +1,32 @@
+import Meetings from '/imports/api/meetings';
+import Logger from '/imports/startup/server/logger';
+import { check } from 'meteor/check';
+
+export default function setGuestLobbyMessage(meetingId, guestLobbyMessage) {
+  check(meetingId, String);
+  check(guestLobbyMessage, String);
+
+  const selector = {
+    meetingId,
+  };
+
+  const modifier = {
+    $set: {
+      guestLobbyMessage,
+    },
+  };
+
+  const cb = (err, numChanged) => {
+    if (err) {
+      return Logger.error(`Changing meeting guest lobby message: ${err}`);
+    }
+
+    if (!numChanged) {
+      return Logger.info(`Meeting's ${meetingId} guest lobby message=${guestLobbyMessage} wasn't updated`);
+    }
+
+    return Logger.info(`Meeting's ${meetingId} guest lobby message=${guestLobbyMessage} updated`);
+  };
+
+  return Meetings.update(selector, modifier, cb);
+}
diff --git a/bigbluebutton-html5/imports/ui/components/text-input/component.jsx b/bigbluebutton-html5/imports/ui/components/text-input/component.jsx
new file mode 100644
index 0000000000..d3a1a3d1eb
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/text-input/component.jsx
@@ -0,0 +1,81 @@
+import React, { PureComponent } from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+import TextareaAutosize from 'react-autosize-textarea';
+import PropTypes from 'prop-types';
+import logger from '/imports/startup/client/logger';
+import Button from '/imports/ui/components/button/component';
+import { styles } from './styles.scss';
+
+const propTypes = {
+  placeholder: PropTypes.string,
+  send: PropTypes.func.isRequired,
+};
+
+const defaultProps = {
+  placeholder: '',
+  send: () => logger.warn({ logCode: 'text_input_send_function' }, `Missing`),
+};
+
+const messages = defineMessages({
+  sendLabel: {
+    id: 'app.textInput.sendLabel',
+    description: 'Text input send button label',
+  },
+});
+
+class TextInput extends PureComponent {
+  constructor(props) {
+    super(props);
+
+    this.state = { message: '' };
+  }
+
+  handleOnChange(e) {
+    const message = e.target.value;
+    this.setState({ message });
+  }
+
+  handleOnClick() {
+    const { send } = this.props;
+    const { message } = this.state;
+
+    send(message);
+    this.setState({ message: '' });
+  }
+
+  render() {
+    const {
+      intl,
+      maxLength,
+      placeholder,
+    } = this.props;
+
+    const { message } = this.state;
+
+    return (
+      <div className={styles.wrapper}>
+        <TextareaAutosize
+          className={styles.textarea}
+          maxLength={maxLength}
+          onChange={(e) => this.handleOnChange(e)}
+          placeholder={placeholder}
+          value={message}
+        />
+        <Button
+          circle
+          className={styles.button}
+          color="primary"
+          hideLabel
+          icon="send"
+          label={intl.formatMessage(messages.sendLabel)}
+          onClick={() => this.handleOnClick()}
+        />
+      </div>
+    );
+  }
+}
+
+TextInput.propTypes = propTypes;
+TextInput.defaultProps = defaultProps;
+
+export default injectIntl(TextInput);
diff --git a/bigbluebutton-html5/imports/ui/components/text-input/styles.scss b/bigbluebutton-html5/imports/ui/components/text-input/styles.scss
new file mode 100644
index 0000000000..5f13ce4fa0
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/text-input/styles.scss
@@ -0,0 +1,53 @@
+@import "/imports/ui/stylesheets/mixins/focus";
+@import "/imports/ui/stylesheets/mixins/_indicators";
+@import "/imports/ui/stylesheets/variables/_all";
+
+.wrapper {
+  display: flex;
+  flex-direction: row;
+}
+
+.textarea {
+  @include inputFocus(var(--color-blue-light));
+
+  flex: 1;
+  background: #fff;
+  background-clip: padding-box;
+  margin: 0;
+  color: var(--color-text);
+  -webkit-appearance: none;
+  padding: calc(var(--sm-padding-y) * 2.5) calc(var(--sm-padding-x) * 1.25);
+  resize: none;
+  transition: none;
+  border-radius: var(--border-radius);
+  font-size: var(--font-size-base);
+  min-height: 2.5rem;
+  max-height: 10rem;
+  border: 1px solid var(--color-gray-lighter);
+  box-shadow: 0 0 0 1px var(--color-gray-lighter);
+
+  &:hover {
+    @include highContrastOutline();
+  }
+
+  &:active,
+  &:focus {
+    @include highContrastOutline();
+    outline-style: solid;
+  }
+}
+
+.button {
+  margin:0 0 0 var(--sm-padding-x);
+  align-self: center;
+  font-size: 0.9rem;
+
+  [dir="rtl"]  & {
+    margin: 0 var(--sm-padding-x) 0 0;
+    -webkit-transform: scale(-1, 1);
+    -moz-transform: scale(-1, 1);
+    -ms-transform: scale(-1, 1);
+    -o-transform: scale(-1, 1);
+    transform: scale(-1, 1);
+  }
+}
diff --git a/bigbluebutton-html5/imports/ui/components/waiting-users/component.jsx b/bigbluebutton-html5/imports/ui/components/waiting-users/component.jsx
index 94d4890011..db6ee3d12c 100755
--- a/bigbluebutton-html5/imports/ui/components/waiting-users/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/waiting-users/component.jsx
@@ -4,6 +4,7 @@ import { Session } from 'meteor/session';
 import { defineMessages, injectIntl } from 'react-intl';
 import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
 import UserAvatar from '/imports/ui/components/user-avatar/component';
+import TextInput from '/imports/ui/components/text-input/component';
 import Button from '/imports/ui/components/button/component';
 import { styles } from './styles';
 
@@ -48,6 +49,14 @@ const intlMessages = defineMessages({
     id: 'app.userList.guest.rememberChoice',
     description: 'Remember label for checkbox',
   },
+  emptyMessage: {
+    id: 'app.userList.guest.emptyMessage',
+    description: 'Empty guest lobby message label',
+  },
+  inputPlaceholder: {
+    id: 'app.userList.guest.inputPlaceholder',
+    description: 'Placeholder to guest lobby message input',
+  },
   accept: {
     id: 'app.userList.guest.acceptLabel',
     description: 'Accept guest button label'
@@ -153,6 +162,9 @@ const WaitingUsers = (props) => {
     guestUsers,
     guestUsersCall,
     changeGuestPolicy,
+    isGuestLobbyMessageEnabled,
+    setGuestLobbyMessage,
+    guestLobbyMessage,
     authenticatedGuest,
   } = props;
 
@@ -234,6 +246,16 @@ const WaitingUsers = (props) => {
           />
         </div>
       </header>
+      {isGuestLobbyMessageEnabled ? (
+        <div className={styles.lobbyMessage}>
+          <TextInput
+            maxLength={128}
+            placeholder={intl.formatMessage(intlMessages.inputPlaceholder)}
+            send={setGuestLobbyMessage}
+          />
+          <p><i>"{guestLobbyMessage.length > 0 ? guestLobbyMessage : intl.formatMessage(intlMessages.emptyMessage)}"</i></p>
+        </div>
+      ) : null}
       <div>
         <div>
           <p className={styles.mainTitle}>{intl.formatMessage(intlMessages.optionTitle)}</p>
diff --git a/bigbluebutton-html5/imports/ui/components/waiting-users/container.jsx b/bigbluebutton-html5/imports/ui/components/waiting-users/container.jsx
index 7e6e879ae4..0341da5a5d 100644
--- a/bigbluebutton-html5/imports/ui/components/waiting-users/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/waiting-users/container.jsx
@@ -38,6 +38,9 @@ export default withTracker(() => {
     authenticatedUsers,
     guestUsersCall: Service.guestUsersCall,
     changeGuestPolicy: Service.changeGuestPolicy,
+    isGuestLobbyMessageEnabled: Service.isGuestLobbyMessageEnabled,
+    setGuestLobbyMessage: Service.setGuestLobbyMessage,
+    guestLobbyMessage: Service.getGuestLobbyMessage(),
     authenticatedGuest,
   };
 })(WaitingContainer);
diff --git a/bigbluebutton-html5/imports/ui/components/waiting-users/service.js b/bigbluebutton-html5/imports/ui/components/waiting-users/service.js
index 23392f0c52..62d4165000 100644
--- a/bigbluebutton-html5/imports/ui/components/waiting-users/service.js
+++ b/bigbluebutton-html5/imports/ui/components/waiting-users/service.js
@@ -1,9 +1,30 @@
+import Meetings from '/imports/api/meetings';
+import Auth from '/imports/ui/services/auth';
 import { makeCall } from '/imports/ui/services/api';
 
 const guestUsersCall = (guestsArray, status) => makeCall('allowPendingUsers', guestsArray, status);
 
 const changeGuestPolicy = policyRule => makeCall('changeGuestPolicy', policyRule);
+
+const isGuestLobbyMessageEnabled = Meteor.settings.public.app.enableGuestLobbyMessage;
+
+const getGuestLobbyMessage = () => {
+  const meeting = Meetings.findOne(
+    { meetingId: Auth.meetingID },
+    { fields: { guestLobbyMessage: 1 } },
+  );
+
+  if (meeting) return meeting.guestLobbyMessage;
+
+  return '';
+};
+
+const setGuestLobbyMessage = (message) => makeCall('setGuestLobbyMessage', message);
+
 export default {
   guestUsersCall,
   changeGuestPolicy,
+  isGuestLobbyMessageEnabled,
+  getGuestLobbyMessage,
+  setGuestLobbyMessage,
 };
diff --git a/bigbluebutton-html5/imports/ui/components/waiting-users/styles.scss b/bigbluebutton-html5/imports/ui/components/waiting-users/styles.scss
index 95f43ac21c..bc5791c572 100644
--- a/bigbluebutton-html5/imports/ui/components/waiting-users/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/waiting-users/styles.scss
@@ -186,6 +186,18 @@
   text-overflow: ellipsis;
 }
 
+.lobbyMessage {
+  border-bottom: 1px solid var(--color-gray-lightest);
+
+  p {
+    background-color: var(--color-off-white);
+    box-sizing: border-box;
+    color: var(--color-gray);
+    padding: 1rem;
+    text-align: center;
+  }
+}
+
 .rememberContainer {
   margin: 1rem 1rem;
   height: 2rem;
diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml
index d268233858..add66fbc5d 100755
--- a/bigbluebutton-html5/private/config/settings.yml
+++ b/bigbluebutton-html5/private/config/settings.yml
@@ -22,6 +22,7 @@ public:
     # in some cases we want only custom logoutUrl to be used when provided on meeting create. Default value: true
     allowDefaultLogoutUrl: true
     allowUserLookup: false
+    enableGuestLobbyMessage: false
     enableNetworkInformation: false
     enableLimitOfViewersInWebcam: false
     enableMultipleCameras: true
diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json
index 4e0dc1eb55..4b8fd113e8 100755
--- a/bigbluebutton-html5/private/locales/en.json
+++ b/bigbluebutton-html5/private/locales/en.json
@@ -48,6 +48,7 @@
     "app.captions.pad.dictationStop": "Stop dictation",
     "app.captions.pad.dictationOnDesc": "Turns speech recognition on",
     "app.captions.pad.dictationOffDesc": "Turns speech recognition off",
+    "app.textInput.sendLabel": "Send",
     "app.note.title": "Shared Notes",
     "app.note.label": "Note",
     "app.note.hideNoteLabel": "Hide note",
@@ -538,6 +539,8 @@
     "app.userList.guest.pendingGuestUsers": "{0} Pending Guest Users",
     "app.userList.guest.pendingGuestAlert": "Has joined the session and is waiting for your approval.",
     "app.userList.guest.rememberChoice": "Remember choice",
+    "app.userList.guest.emptyMessage": "There is currently no message",
+    "app.userList.guest.inputPlaceholder": "Message to the guests' lobby",
     "app.userList.guest.acceptLabel": "Accept",
     "app.userList.guest.denyLabel": "Deny",
     "app.user-info.title": "Directory Lookup",
diff --git a/bigbluebutton-html5/private/static/guest-wait/guest-wait.html b/bigbluebutton-html5/private/static/guest-wait/guest-wait.html
index 8de097f302..e2181188db 100755
--- a/bigbluebutton-html5/private/static/guest-wait/guest-wait.html
+++ b/bigbluebutton-html5/private/static/guest-wait/guest-wait.html
@@ -59,12 +59,24 @@
       }
   }
   </style>
-  
+
   <script type="text/javascript">
     function updateMessage(message) {
       document.querySelector('#content > p').innerHTML = message;
     }
 
+    var lobbyMessage = '';
+    function updateLobbyMessage(message) {
+      if (message !== lobbyMessage) {
+        lobbyMessage = message;
+        if (lobbyMessage.length !== 0) {
+          updateMessage(lobbyMessage);
+        } else {
+          updateMessage('Please wait for a moderator to approve you joining the meeting.');
+        }
+      }
+    }
+
     function findSessionToken() {
       return location.search
         .substr(1)
@@ -75,7 +87,7 @@
     };
 
     function fetchGuestWait(sessionToken) {
-      const GUEST_WAIT_ENDPOINT = '/bigbluebutton/api/guestWait';      
+      const GUEST_WAIT_ENDPOINT = '/bigbluebutton/api/guestWait';
       const urlTest = new URL(`${window.location.origin}${GUEST_WAIT_ENDPOINT}`);
       const concatedParams = sessionToken.concat('&redirect=false');
       urlTest.search = concatedParams;
@@ -95,7 +107,6 @@
         fetchGuestWait(token)
         .then(async (resp) => await resp.json())
         .then((data) => {
-          console.log("data=" + JSON.stringify(data));
           var status = data.response.guestStatus;
 
           if (REDIRECT_STATUSES.includes(status)) {
@@ -104,6 +115,8 @@
             return;
           }
 
+          updateLobbyMessage(data.response.lobbyMessage);
+
           return pollGuestStatus(token, attempt + 1, limit, everyMs);
         });
       }, everyMs);
diff --git a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy
index 9e4fd1028f..77049971a3 100755
--- a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy
+++ b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy
@@ -1323,6 +1323,7 @@ class ApiController {
       // Get the client url we stored in the join api call before
       // being told to wait.
       String clientURL = us.clientUrl;
+      String lobbyMsg = meeting.getGuestLobbyMessage()
       log.info("clientURL = " + clientURL)
       log.info("redirect = ." + redirectClient)
       if (!StringUtils.isEmpty(params.redirect)) {
@@ -1412,6 +1413,7 @@ class ApiController {
               auth_token us.authToken
               session_token session[sessionToken]
               guestStatus guestWaitStatus
+              lobbyMessage lobbyMsg
               url destUrl
             }
             render(contentType: "application/json", text: builder.toPrettyString())
-- 
GitLab