Merge pull request #21368 from GuiLeme/new-server-side-architecture

refactor (plugins): Read plugins configs from a manifest file instead of client settings
This commit is contained in:
Gustavo Trott 2024-10-14 17:20:52 -03:00 committed by GitHub
commit 1eeff8d142
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 619 additions and 246 deletions

View File

@ -1,50 +1,18 @@
package org.bigbluebutton.core.apps.plugin
import org.bigbluebutton.ClientSettings
import org.bigbluebutton.common2.msgs.PluginDataChannelDeleteEntryMsg
import org.bigbluebutton.core.apps.plugin.PluginHdlrHelpers.{ checkPermission, dataChannelCheckingLogic, defaultCreatorCheck }
import org.bigbluebutton.core.db.PluginDataChannelEntryDAO
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.models.{ Roles, Users2x }
import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting }
trait PluginDataChannelDeleteEntryMsgHdlr extends HandlerHelpers {
def handle(msg: PluginDataChannelDeleteEntryMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = {
val pluginsDisabled: Boolean = liveMeeting.props.meetingProp.disabledFeatures.contains("plugins")
val meetingId = liveMeeting.props.meetingProp.intId
for {
_ <- if (!pluginsDisabled) Some(()) else None
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId)
} yield {
val pluginsConfig = ClientSettings.getPluginsFromConfig(ClientSettings.clientSettingsFromFile)
if (!pluginsConfig.contains(msg.body.pluginName)) {
println(s"Plugin '${msg.body.pluginName}' not found.")
} else if (!pluginsConfig(msg.body.pluginName).dataChannels.contains(msg.body.channelName)) {
println(s"Data channel '${msg.body.channelName}' not found in plugin '${msg.body.pluginName}'.")
} else {
val hasPermission = for {
replaceOrDeletePermission <- pluginsConfig(msg.body.pluginName).dataChannels(msg.body.channelName).replaceOrDeletePermission
} yield {
replaceOrDeletePermission.toLowerCase match {
case "all" => true
case "moderator" => user.role == Roles.MODERATOR_ROLE
case "presenter" => user.presenter
case "creator" => {
val creatorUserId = PluginDataChannelEntryDAO.getEntryCreator(
meetingId,
msg.body.pluginName,
msg.body.channelName,
msg.body.subChannelName,
msg.body.entryId
)
creatorUserId == msg.header.userId
}
case _ => false
}
}
dataChannelCheckingLogic(liveMeeting, msg.header.userId, msg.body.pluginName, msg.body.channelName, (user, dc, meetingId) => {
val hasPermission = checkPermission(user, dc.replaceOrDeletePermission, defaultCreatorCheck(
meetingId, msg.body, msg.header.userId
))
if (!hasPermission.contains(true)) {
println(s"No permission to delete in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.")
} else {
@ -56,7 +24,6 @@ trait PluginDataChannelDeleteEntryMsgHdlr extends HandlerHelpers {
msg.body.entryId
)
}
}
}
})
}
}

View File

@ -1,40 +1,16 @@
package org.bigbluebutton.core.apps.plugin
import org.bigbluebutton.common2.msgs.PluginDataChannelPushEntryMsg
import org.bigbluebutton.ClientSettings
import org.bigbluebutton.core.apps.plugin.PluginHdlrHelpers.{ checkPermission, dataChannelCheckingLogic, defaultCreatorCheck }
import org.bigbluebutton.core.db.PluginDataChannelEntryDAO
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.models.{ Roles, Users2x }
import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting }
trait PluginDataChannelPushEntryMsgHdlr extends HandlerHelpers {
def handle(msg: PluginDataChannelPushEntryMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = {
val pluginsDisabled: Boolean = liveMeeting.props.meetingProp.disabledFeatures.contains("plugins")
val meetingId = liveMeeting.props.meetingProp.intId
for {
_ <- if (!pluginsDisabled) Some(()) else None
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId)
} yield {
val pluginsConfig = ClientSettings.getPluginsFromConfig(ClientSettings.clientSettingsFromFile)
if (!pluginsConfig.contains(msg.body.pluginName)) {
println(s"Plugin '${msg.body.pluginName}' not found.")
} else if (!pluginsConfig(msg.body.pluginName).dataChannels.contains(msg.body.channelName)) {
println(s"Data channel '${msg.body.channelName}' not found in plugin '${msg.body.pluginName}'.")
} else {
val hasPermission = for {
pushPermission <- pluginsConfig(msg.body.pluginName).dataChannels(msg.body.channelName).pushPermission
} yield {
pushPermission.toLowerCase match {
case "all" => true
case "moderator" => user.role == Roles.MODERATOR_ROLE
case "presenter" => user.presenter
case _ => false
}
}
dataChannelCheckingLogic(liveMeeting, msg.header.userId, msg.body.pluginName, msg.body.channelName, (user, dc, meetingId) => {
val hasPermission = checkPermission(user, dc.pushPermission)
if (!hasPermission.contains(true)) {
println(s"No permission to write in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.")
} else {
@ -49,7 +25,6 @@ trait PluginDataChannelPushEntryMsgHdlr extends HandlerHelpers {
msg.body.toUserIds
)
}
}
}
})
}
}

View File

@ -1,49 +1,18 @@
package org.bigbluebutton.core.apps.plugin
import org.bigbluebutton.ClientSettings
import org.bigbluebutton.common2.msgs.PluginDataChannelReplaceEntryMsg
import org.bigbluebutton.core.apps.plugin.PluginHdlrHelpers.{checkPermission, dataChannelCheckingLogic, defaultCreatorCheck}
import org.bigbluebutton.core.db.{JsonUtils, PluginDataChannelEntryDAO}
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.models.{Roles, Users2x}
import org.bigbluebutton.core.running.{HandlerHelpers, LiveMeeting}
trait PluginDataChannelReplaceEntryMsgHdlr extends HandlerHelpers {
def handle(msg: PluginDataChannelReplaceEntryMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = {
val pluginsDisabled: Boolean = liveMeeting.props.meetingProp.disabledFeatures.contains("plugins")
val meetingId = liveMeeting.props.meetingProp.intId
for {
_ <- if (!pluginsDisabled) Some(()) else None
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId)
} yield {
val pluginsConfig = ClientSettings.getPluginsFromConfig(ClientSettings.clientSettingsFromFile)
if (!pluginsConfig.contains(msg.body.pluginName)) {
println(s"Plugin '${msg.body.pluginName}' not found.")
} else if (!pluginsConfig(msg.body.pluginName).dataChannels.contains(msg.body.channelName)) {
println(s"Data channel '${msg.body.channelName}' not found in plugin '${msg.body.pluginName}'.")
} else {
val hasPermission = for {
replaceOrDeletePermission <- pluginsConfig(msg.body.pluginName).dataChannels(msg.body.channelName).replaceOrDeletePermission
} yield {
replaceOrDeletePermission.toLowerCase match {
case "all" => true
case "moderator" => user.role == Roles.MODERATOR_ROLE
case "presenter" => user.presenter
case "creator" => {
val creatorUserId = PluginDataChannelEntryDAO.getEntryCreator(
meetingId,
msg.body.pluginName,
msg.body.channelName,
msg.body.subChannelName,
msg.body.entryId
)
creatorUserId == msg.header.userId
}
case _ => false
}
}
dataChannelCheckingLogic(liveMeeting, msg.header.userId, msg.body.pluginName, msg.body.channelName, (user, dc, meetingId) => {
val hasPermission = checkPermission(user, dc.replaceOrDeletePermission, defaultCreatorCheck(
meetingId, msg.body, msg.header.userId))
if (!hasPermission.contains(true)) {
println(s"No permission to write in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.")
@ -57,7 +26,6 @@ trait PluginDataChannelReplaceEntryMsgHdlr extends HandlerHelpers {
JsonUtils.mapToJson(msg.body.payloadJson),
)
}
}
}
})
}
}

View File

@ -1,39 +1,16 @@
package org.bigbluebutton.core.apps.plugin
import org.bigbluebutton.ClientSettings
import org.bigbluebutton.common2.msgs.PluginDataChannelResetMsg
import org.bigbluebutton.core.apps.plugin.PluginHdlrHelpers.{ checkPermission, dataChannelCheckingLogic }
import org.bigbluebutton.core.db.PluginDataChannelEntryDAO
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.models.{ Roles, Users2x }
import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting }
trait PluginDataChannelResetMsgHdlr extends HandlerHelpers {
def handle(msg: PluginDataChannelResetMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = {
val pluginsDisabled: Boolean = liveMeeting.props.meetingProp.disabledFeatures.contains("plugins")
val meetingId = liveMeeting.props.meetingProp.intId
for {
_ <- if (!pluginsDisabled) Some(()) else None
user <- Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId)
} yield {
val pluginsConfig = ClientSettings.getPluginsFromConfig(ClientSettings.clientSettingsFromFile)
if (!pluginsConfig.contains(msg.body.pluginName)) {
println(s"Plugin '${msg.body.pluginName}' not found.")
} else if (!pluginsConfig(msg.body.pluginName).dataChannels.contains(msg.body.channelName)) {
println(s"Data channel '${msg.body.channelName}' not found in plugin '${msg.body.pluginName}'.")
} else {
val hasPermission = for {
replaceOrDeletePermission <- pluginsConfig(msg.body.pluginName).dataChannels(msg.body.channelName).replaceOrDeletePermission
} yield {
replaceOrDeletePermission.toLowerCase match {
case "all" => true
case "moderator" => user.role == Roles.MODERATOR_ROLE
case "presenter" => user.presenter
case _ => false
}
}
dataChannelCheckingLogic(liveMeeting, msg.header.userId, msg.body.pluginName, msg.body.channelName, (user, dc, meetingId) => {
val hasPermission = checkPermission(user, dc.replaceOrDeletePermission)
if (!hasPermission.contains(true)) {
println(s"No permission to delete (reset) in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.")
@ -45,7 +22,6 @@ trait PluginDataChannelResetMsgHdlr extends HandlerHelpers {
msg.body.subChannelName
)
}
}
}
})
}
}

View File

@ -0,0 +1,50 @@
package org.bigbluebutton.core.apps.plugin
import org.bigbluebutton.common2.msgs.PluginDataChannelReplaceOrDeleteBaseBody
import org.bigbluebutton.core.db.PluginDataChannelEntryDAO
import org.bigbluebutton.core.models.{ DataChannel, PluginModel, Roles, UserState, Users2x }
import org.bigbluebutton.core.running.LiveMeeting
object PluginHdlrHelpers {
def checkPermission(user: UserState, permissionType: List[String], creatorCheck: => Boolean = false): List[Boolean] = {
permissionType.map(_.toLowerCase).map {
case "all" => true
case "moderator" => user.role == Roles.MODERATOR_ROLE
case "presenter" => user.presenter
case "creator" => creatorCheck
case _ => false
}
}
def defaultCreatorCheck[T <: PluginDataChannelReplaceOrDeleteBaseBody](meetingId: String, msgBody: T, userId: String): Boolean = {
val creatorUserId = PluginDataChannelEntryDAO.getEntryCreator(
meetingId,
msgBody.pluginName,
msgBody.channelName,
msgBody.subChannelName,
msgBody.entryId
)
creatorUserId == userId
}
def dataChannelCheckingLogic(liveMeeting: LiveMeeting, userId: String,
pluginName: String, channelName: String,
caseSomeDataChannelAndPlugin: (UserState, DataChannel, String) => Unit): Option[Unit] = {
val pluginsDisabled: Boolean = liveMeeting.props.meetingProp.disabledFeatures.contains("plugins")
val meetingId = liveMeeting.props.meetingProp.intId
for {
_ <- if (!pluginsDisabled) Some(()) else None
user <- Users2x.findWithIntId(liveMeeting.users2x, userId)
} yield {
PluginModel.getPluginByName(liveMeeting.plugins, pluginName) match {
case Some(p) =>
p.manifest.content.dataChannels.getOrElse(List()).find(dc => dc.name == channelName) match {
case Some(dc) =>
caseSomeDataChannelAndPlugin(user, dc, meetingId)
case None => println(s"Data channel '${channelName}' not found in plugin '${pluginName}'.")
}
case None => println(s"Plugin '${pluginName}' not found.")
}
}
}
}

View File

@ -3,6 +3,7 @@ package org.bigbluebutton.core.db
import org.bigbluebutton.common2.domain.DefaultProps
import PostgresProfile.api._
import org.bigbluebutton.core.apps.groupchats.GroupChatApp
import org.bigbluebutton.core.models.PluginModel
case class MeetingSystemColumnsDbModel(
loginUrl: Option[String],
@ -85,7 +86,7 @@ class MeetingDbTableDef(tag: Tag) extends Table[MeetingDbModel](tag, None, "meet
}
object MeetingDAO {
def insert(meetingProps: DefaultProps, clientSettings: Map[String, Object]) = {
def insert(meetingProps: DefaultProps, clientSettings: Map[String, Object], pluginProps: PluginModel) = {
DatabaseConnection.enqueue(
TableQuery[MeetingDbTableDef].forceInsert(
MeetingDbModel(
@ -148,6 +149,7 @@ object MeetingDAO {
MeetingBreakoutDAO.insert(meetingProps.meetingProp.intId, meetingProps.breakoutProps)
LayoutDAO.insert(meetingProps.meetingProp.intId, meetingProps.usersProp.meetingLayout)
MeetingClientSettingsDAO.insert(meetingProps.meetingProp.intId, JsonUtils.mapToJson(clientSettings))
PluginModel.persistPluginsForClient(pluginProps, meetingProps.meetingProp.intId)
}
def updateMeetingDurationByParentMeeting(parentMeetingId: String, newDurationInSeconds: Int) = {

View File

@ -0,0 +1,33 @@
package org.bigbluebutton.core.db
import PostgresProfile.api._
case class PluginDbModel(
meetingId: String,
name: String,
javascriptEntrypointUrl: String,
javascriptEntrypointIntegrity: String
)
class PluginDbTableDef(tag: Tag) extends Table[PluginDbModel](tag, None, "plugin") {
val meetingId = column[String]("meetingId", O.PrimaryKey)
val name = column[String]("name", O.PrimaryKey)
val javascriptEntrypointUrl = column[String]("javascriptEntrypointUrl")
val javascriptEntrypointIntegrity = column[String]("javascriptEntrypointIntegrity")
override def * = (meetingId, name, javascriptEntrypointUrl, javascriptEntrypointIntegrity) <> (PluginDbModel.tupled, PluginDbModel.unapply)
}
object PluginDAO {
def insert(meetingId: String, name: String, javascriptEntrypointUrl: String, javascriptEntrypointIntegrity: String) = {
DatabaseConnection.enqueue(
TableQuery[PluginDbTableDef].forceInsert(
PluginDbModel(
meetingId = meetingId,
name = name,
javascriptEntrypointUrl = javascriptEntrypointUrl,
javascriptEntrypointIntegrity = javascriptEntrypointIntegrity,
)
)
)
}
}

View File

@ -0,0 +1,90 @@
package org.bigbluebutton.core.models
import com.fasterxml.jackson.annotation.{ JsonIgnoreProperties, JsonProperty }
import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.{ JsonMappingException, ObjectMapper }
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import org.bigbluebutton.core.db.PluginDAO
import java.util
case class RateLimiting(
messagesAllowedPerSecond: Int,
messagesAllowedPerMinute: Int
)
case class EventPersistence(
isEnabled: Boolean,
maximumPayloadSizeInBytes: Int,
rateLimiting: RateLimiting
)
case class DataChannel(
name: String,
pushPermission: List[String],
replaceOrDeletePermission: List[String]
)
case class RemoteDataSource(
name: String,
url: String,
fetchMode: String,
permissions: List[String]
)
case class PluginManifestContent(
requiredSdkVersion: String,
name: String,
javascriptEntrypointUrl: String,
javascriptEntrypointIntegrity: Option[String] = None,
localesBaseUrl: Option[String] = None,
eventPersistence: Option[EventPersistence] = None,
dataChannels: Option[List[DataChannel]] = None,
remoteDataSources: Option[List[RemoteDataSource]] = None
)
case class PluginManifest(
url: String,
content: PluginManifestContent
)
case class Plugin(
manifest: PluginManifest
)
object PluginModel {
val objectMapper: ObjectMapper = new ObjectMapper()
objectMapper.registerModule(new DefaultScalaModule())
def getPluginByName(instance: PluginModel, pluginName: String): Option[Plugin] = {
instance.plugins.get(pluginName)
}
def getPlugins(instance: PluginModel): Map[String, Plugin] = {
instance.plugins
}
def createPluginModelFromJson(json: util.Map[String, AnyRef]): PluginModel = {
val instance = new PluginModel()
var pluginsMap: Map[String, Plugin] = Map.empty[String, Plugin]
json.forEach { case (pluginName, plugin) =>
try {
val pluginObject = objectMapper.readValue(objectMapper.writeValueAsString(plugin), classOf[Plugin])
pluginsMap = pluginsMap + (pluginName -> pluginObject)
} catch {
case err @ (_: JsonProcessingException | _: JsonMappingException) => println("Error while processing plugin " +
pluginName + ": ", err)
}
}
instance.plugins = pluginsMap
instance
}
def persistPluginsForClient(instance: PluginModel, meetingId: String): Unit = {
instance.plugins.foreach { case (_, plugin) =>
PluginDAO.insert(meetingId, plugin.manifest.content.name, plugin.manifest.content.javascriptEntrypointUrl,
plugin.manifest.content.javascriptEntrypointIntegrity.getOrElse(""))
}
}
}
class PluginModel {
private var plugins: Map[String, Plugin] = Map()
}

View File

@ -27,4 +27,5 @@ class LiveMeeting(
val users2x: Users2x,
val guestsWaiting: GuestsWaiting,
val clientSettings: Map[String, Object],
val plugins: PluginModel,
)

View File

@ -172,7 +172,7 @@ class MeetingActor(
outGW.send(msgEvent)
//Insert meeting into the database
MeetingDAO.insert(liveMeeting.props, liveMeeting.clientSettings)
MeetingDAO.insert(liveMeeting.props, liveMeeting.clientSettings, liveMeeting.plugins)
// Create a default public group chat
state = groupChatApp.handleCreateDefaultPublicGroupChat(state, liveMeeting, msgBus)

View File

@ -2,13 +2,11 @@ package org.bigbluebutton.core.running
import org.apache.pekko.actor.ActorContext
import org.bigbluebutton.ClientSettings
import org.bigbluebutton.ClientSettings.{getConfigPropertyValueByPathAsBooleanOrElse, getConfigPropertyValueByPathAsStringOrElse}
import org.bigbluebutton.common2.domain.DefaultProps
import org.bigbluebutton.core.apps._
import org.bigbluebutton.core.bus._
import org.bigbluebutton.core.models._
import org.bigbluebutton.core.OutMessageGateway
import org.bigbluebutton.core.apps.pads.PadslHdlrHelpers
import org.bigbluebutton.core2.MeetingStatus2x
object RunningMeeting {
@ -19,9 +17,9 @@ object RunningMeeting {
class RunningMeeting(val props: DefaultProps, outGW: OutMessageGateway,
eventBus: InternalEventBus)(implicit val context: ActorContext) {
private val externalVideoModel = new ExternalVideoModel()
private val chatModel = new ChatModel()
private val plugins = PluginModel.createPluginModelFromJson(props.pluginProp)
private val layouts = new Layouts()
private val pads = new Pads()
private val wbModel = new WhiteboardModel()
@ -45,7 +43,7 @@ class RunningMeeting(val props: DefaultProps, outGW: OutMessageGateway,
// easy to test.
private val liveMeeting = new LiveMeeting(props, meetingStatux2x, deskshareModel, audioCaptions, timerModel,
chatModel, externalVideoModel, layouts, pads, registeredUsers, polls2x, wbModel, presModel, captionModel,
webcams, voiceUsers, users2x, guestsWaiting, clientSettings)
webcams, voiceUsers, users2x, guestsWaiting, clientSettings, plugins)
GuestsWaiting.setGuestPolicy(
liveMeeting.props.meetingProp.intId,

View File

@ -1,5 +1,7 @@
package org.bigbluebutton.common2.domain
import java.util
case class DurationProps(duration: Int, createdTime: Long, createdDate: String,
meetingExpireIfNoUserJoinedInMinutes: Int, meetingExpireWhenLastUserLeftInMinutes: Int,
userInactivityInspectTimerInMinutes: Int, userInactivityThresholdInMinutes: Int,
@ -85,6 +87,7 @@ case class GroupProps(
)
case class DefaultProps(
pluginProp: util.Map[String, AnyRef],
meetingProp: MeetingProp,
breakoutProps: BreakoutProps,
durationProps: DurationProps,

View File

@ -7,6 +7,14 @@ import org.bigbluebutton.common2.domain.PluginLearningAnalyticsDashboardGenericD
/**
* Sent from graphql-actions to bbb-akka
*/
trait PluginDataChannelReplaceOrDeleteBaseBody{
val pluginName: String
val channelName: String
val subChannelName: String
val entryId: String
}
object PluginDataChannelPushEntryMsg { val NAME = "PluginDataChannelPushEntryMsg" }
case class PluginDataChannelPushEntryMsg(header: BbbClientMsgHeader, body: PluginDataChannelPushEntryMsgBody) extends StandardMsg
case class PluginDataChannelPushEntryMsgBody(
@ -26,7 +34,7 @@ case class PluginDataChannelReplaceEntryMsgBody(
subChannelName: String,
payloadJson: Map[String, Any],
entryId: String,
)
) extends PluginDataChannelReplaceOrDeleteBaseBody
object PluginDataChannelDeleteEntryMsg { val NAME = "PluginDataChannelDeleteEntryMsg" }
case class PluginDataChannelDeleteEntryMsg(header: BbbClientMsgHeader, body: PluginDataChannelDeleteEntryMsgBody) extends StandardMsg
@ -35,7 +43,7 @@ case class PluginDataChannelDeleteEntryMsgBody(
subChannelName: String,
channelName: String,
entryId: String
)
) extends PluginDataChannelReplaceOrDeleteBaseBody
object PluginDataChannelResetMsg { val NAME = "PluginDataChannelResetMsg" }

View File

@ -73,6 +73,7 @@ public class ApiParams {
public static final String ROLE = "role";
public static final String GROUPS = "groups";
public static final String DISABLED_FEATURES = "disabledFeatures";
public static final String PLUGINS_MANIFESTS = "pluginsManifests";
public static final String DISABLED_FEATURES_EXCLUDE = "disabledFeaturesExclude";
public static final String NOTIFY_RECORDING_IS_ON = "notifyRecordingIsOn";

View File

@ -19,7 +19,11 @@
package org.bigbluebutton.api;
import java.io.File;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.BlockingQueue;
@ -29,7 +33,11 @@ import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.JsonObject;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.utils.URIBuilder;
import org.bigbluebutton.api.domain.*;
@ -57,6 +65,8 @@ import com.google.gson.Gson;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.data.domain.*;
@ -97,6 +107,8 @@ public class MeetingService implements MessageListener {
private HashMap<String, PresentationUploadToken> uploadAuthzTokens;
ObjectMapper objectMapper = new ObjectMapper();
public MeetingService() {
meetings = new ConcurrentHashMap<String, Meeting>(8, 0.9f, 1);
sessions = new ConcurrentHashMap<String, UserSession>(8, 0.9f, 1);
@ -352,6 +364,102 @@ public class MeetingService implements MessageListener {
: Collections.unmodifiableCollection(sessions.values());
}
public String replaceMetaParametersIntoManifestTemplate(String manifestContent, Map<String, String> metadata)
throws NoSuchFieldException {
// Pattern to match ${variable} in the input string
Pattern pattern = Pattern.compile("\\$\\{(\\w+)}");
Matcher matcher = pattern.matcher(manifestContent);
StringBuilder result = new StringBuilder();
// Iterate over all matches
while (matcher.find()) {
String variableName = matcher.group(1);
if (variableName.startsWith("meta_") && variableName.length() > 5) {
// Remove "meta_" and convert to lower case
variableName = variableName.substring(5).toLowerCase();
} else {
throw new NoSuchFieldException("Metadata " + variableName + " is malformed, please provide a valid one");
}
String replacement;
if (metadata.containsKey(variableName))
replacement = metadata.get(variableName);
else throw new NoSuchFieldException("Metadata " + variableName + " not found in URL parameters");
// Replace the placeholder with the value from the map
matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
}
matcher.appendTail(result);
return result.toString();
}
public Map<String, Object> requestPluginManifests(Meeting m) {
Map<String, Object> urlContents = new HashMap<>();
Map<String, String> metadata = m.getMetadata();
// Fetch content for each URL and store in the map
for (PluginsManifest pluginsManifest : m.getPluginsManifests()) {
try {
String urlString = pluginsManifest.getUrl();
URL url = new URL(urlString);
StringBuilder content = new StringBuilder();
try (BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()))) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
content.append(inputLine).append("\n");
}
}
// Parse the JSON content
JsonNode jsonNode = objectMapper.readTree(content.toString());
// Validate checksum if any:
String paramChecksum = pluginsManifest.getChecksum();
if (!StringUtils.isEmpty(paramChecksum)) {
String hash = DigestUtils.sha256Hex(content.toString());
if (!paramChecksum.equals(hash)) {
log.info("Plugin's manifest.json checksum mismatch with that of the URL parameter for {}.",
pluginsManifest.getUrl()
);
log.info("Plugin {} is not going to be loaded",
pluginsManifest.getUrl()
);
continue;
}
}
// Get the "name" field
String name;
if (jsonNode.has("name")) {
name = jsonNode.get("name").asText();
} else {
throw new NoSuchFieldException("For url " + urlString + "there is no name field configured.");
}
String pluginKey = name;
HashMap<String, Object> manifestObject = new HashMap<>();
manifestObject.put("url", urlString);
String manifestContent = replaceMetaParametersIntoManifestTemplate(content.toString(), metadata);
Map<String, Object> mappedManifestContent = objectMapper.readValue(manifestContent, new TypeReference<>() {});
manifestObject.put("content", mappedManifestContent);
Map<String, Object> manifestWrapper = new HashMap<String, Object>();
manifestWrapper.put(
"manifest", manifestObject
);
urlContents.put(pluginKey, manifestWrapper);
} catch(Exception e) {
log.error("Failed with the following plugin manifest URL: {}. Error: ",
pluginsManifest.getUrl(), e);
log.error("Therefore this plugin will not be loaded");
}
}
return urlContents;
}
public synchronized boolean createMeeting(Meeting m) {
String internalMeetingId = paramsProcessorUtil.convertToInternalMeetingId(m.getExternalId());
Meeting existingId = getNotEndedMeetingWithId(internalMeetingId);
@ -359,6 +467,8 @@ public class MeetingService implements MessageListener {
Meeting existingWebVoice = getNotEndedMeetingWithWebVoice(m.getWebVoice());
if (existingId == null && existingTelVoice == null && existingWebVoice == null) {
meetings.put(m.getInternalId(), m);
Map<String, Object> requestedManifests = requestPluginManifests(m);
m.setPlugins(requestedManifests);
handle(new CreateMeeting(m));
return true;
}
@ -444,7 +554,7 @@ public class MeetingService implements MessageListener {
m.getMuteOnStart(), m.getAllowModsToUnmuteUsers(), m.getAllowModsToEjectCameras(), m.getMeetingKeepEvents(),
m.breakoutRoomsParams, m.lockSettingsParams, m.getLoginUrl(), m.getLogoutUrl(), m.getCustomLogoURL(), m.getCustomDarkLogoURL(),
m.getBannerText(), m.getBannerColor(), m.getGroups(), m.getDisabledFeatures(), m.getNotifyRecordingIsOn(),
m.getPresentationUploadExternalDescription(), m.getPresentationUploadExternalUrl(),
m.getPresentationUploadExternalDescription(), m.getPresentationUploadExternalUrl(), m.getPlugins(),
m.getOverrideClientSettings());
}

View File

@ -26,10 +26,9 @@ import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.*;
import org.bigbluebutton.api.domain.*;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.safety.Safelist;
@ -39,10 +38,6 @@ import org.jsoup.select.Elements;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.bigbluebutton.api.domain.BreakoutRoomsParams;
import org.bigbluebutton.api.domain.LockSettingsParams;
import org.bigbluebutton.api.domain.Meeting;
import org.bigbluebutton.api.domain.Group;
import org.bigbluebutton.api.service.ServiceUtils;
import org.bigbluebutton.api.util.ParamsUtil;
import org.slf4j.Logger;
@ -104,6 +99,7 @@ public class ParamsProcessorUtil {
private boolean defaultAllowModsToUnmuteUsers = false;
private boolean defaultAllowModsToEjectCameras = false;
private String defaultDisabledFeatures;
private String defaultPluginsManifests;
private boolean defaultNotifyRecordingIsOn = false;
private boolean defaultKeepEvents = false;
private Boolean useDefaultLogo;
@ -432,6 +428,33 @@ public class ParamsProcessorUtil {
return groups;
}
private ArrayList<PluginsManifest> processPluginsManifests(String pluginsManifestsParam) {
ArrayList<PluginsManifest> pluginsManifests = new ArrayList<PluginsManifest>();
JsonElement pluginsManifestsJsonElement = new Gson().fromJson(pluginsManifestsParam, JsonElement.class);
try {
if (pluginsManifestsJsonElement != null && pluginsManifestsJsonElement.isJsonArray()) {
JsonArray pluginsManifestsJson = pluginsManifestsJsonElement.getAsJsonArray();
for (JsonElement pluginsManifestJson : pluginsManifestsJson) {
if (pluginsManifestJson.isJsonObject()) {
JsonObject pluginsManifestJsonObj = pluginsManifestJson.getAsJsonObject();
if (pluginsManifestJsonObj.has("url")) {
String url = pluginsManifestJsonObj.get("url").getAsString();
PluginsManifest newPlugin = new PluginsManifest(url);
if (pluginsManifestJsonObj.has("checksum")) {
newPlugin.setChecksum(pluginsManifestJsonObj.get("checksum").getAsString());
}
pluginsManifests.add(newPlugin);
}
}
}
}
} catch(JsonSyntaxException err){
log.error("Error in pluginsManifests URL parameter's json structure.");
}
return pluginsManifests;
}
public Meeting processCreateParams(Map<String, String> params) {
String meetingName = params.get(ApiParams.NAME);
@ -550,6 +573,20 @@ public class ParamsProcessorUtil {
listOfDisabledFeatures.removeAll(Arrays.asList(disabledFeaturesExcludeParam.split(",")));
}
// Parse Plugins Manifests from config and param
ArrayList<PluginsManifest> listOfPluginsManifests = new ArrayList<PluginsManifest>();
//Process plugins from config
if(defaultPluginsManifests != null && !defaultPluginsManifests.isEmpty()) {
ArrayList<PluginsManifest> pluginsManifestsFromConfig = processPluginsManifests(defaultPluginsManifests);
listOfPluginsManifests.addAll(pluginsManifestsFromConfig);
}
//Process plugins from /create param
String pluginsManifestsParam = params.get(ApiParams.PLUGINS_MANIFESTS);
if (!StringUtils.isEmpty(pluginsManifestsParam)) {
ArrayList<PluginsManifest> pluginsManifestsFromParam = processPluginsManifests(pluginsManifestsParam);
listOfPluginsManifests.addAll(pluginsManifestsFromParam);
}
// Check if VirtualBackgrounds is disabled
if (!StringUtils.isEmpty(params.get(ApiParams.VIRTUAL_BACKGROUNDS_DISABLED))) {
boolean virtualBackgroundsDisabled = Boolean.valueOf(params.get(ApiParams.VIRTUAL_BACKGROUNDS_DISABLED));
@ -790,6 +827,7 @@ public class ParamsProcessorUtil {
.withLearningDashboardCleanupDelayInMinutes(learningDashboardCleanupMins)
.withLearningDashboardAccessToken(learningDashboardAccessToken)
.withGroups(groups)
.withPluginManifests(listOfPluginsManifests)
.withDisabledFeatures(listOfDisabledFeatures)
.withNotifyRecordingIsOn(notifyRecordingIsOn)
.withPresentationUploadExternalDescription(presentationUploadExternalDescription)
@ -1582,6 +1620,10 @@ public class ParamsProcessorUtil {
this.defaultDisabledFeatures = disabledFeatures;
}
public void setPluginsManifests(String pluginsManifests) {
this.defaultPluginsManifests = pluginsManifests;
}
public void setNotifyRecordingIsOn(Boolean notifyRecordingIsOn) {
this.defaultNotifyRecordingIsOn = notifyRecordingIsOn;
}

View File

@ -78,6 +78,8 @@ public class Meeting {
private String dialNumber;
private String defaultAvatarURL;
private String defaultWebcamBackgroundURL;
private Map<String, Object> plugins;
private ArrayList<PluginsManifest> pluginsManifests;
private String guestPolicy = GuestPolicy.ASK_MODERATOR;
private String guestLobbyMessage = "";
private Map<String,String> usersWithGuestLobbyMessages;
@ -128,6 +130,7 @@ public class Meeting {
extMeetingId = builder.externalId;
intMeetingId = builder.internalId;
disabledFeatures = builder.disabledFeatures;
pluginsManifests = builder.pluginsManifests;
notifyRecordingIsOn = builder.notifyRecordingIsOn;
presentationUploadExternalDescription = builder.presentationUploadExternalDescription;
presentationUploadExternalUrl = builder.presentationUploadExternalUrl;
@ -441,6 +444,17 @@ public class Meeting {
public ArrayList<String> getDisabledFeatures() {
return disabledFeatures;
}
public Map<String, Object> getPlugins() {
return plugins;
}
public void setPlugins(Map<String, Object> p) {
plugins = p;
}
public ArrayList<PluginsManifest> getPluginsManifests() {
return pluginsManifests;
}
public Boolean getNotifyRecordingIsOn() {
return notifyRecordingIsOn;
@ -929,6 +943,7 @@ public class Meeting {
private int learningDashboardCleanupDelayInMinutes;
private String learningDashboardAccessToken;
private ArrayList<String> disabledFeatures;
private ArrayList<PluginsManifest> pluginsManifests;
private Boolean notifyRecordingIsOn;
private String presentationUploadExternalDescription;
private String presentationUploadExternalUrl;
@ -1063,6 +1078,11 @@ public class Meeting {
return this;
}
public Builder withPluginManifests(ArrayList<PluginsManifest> map) {
this.pluginsManifests = map;
return this;
}
public Builder withNotifyRecordingIsOn(Boolean b) {
this.notifyRecordingIsOn = b;
return this;

View File

@ -0,0 +1,35 @@
package org.bigbluebutton.api.domain;
import java.util.Vector;
public class PluginsManifest {
private String url = "";
private String checksum = "";
public PluginsManifest(
String url,
String checksum) {
this.url = url;
this.checksum = checksum;
}
public PluginsManifest(
String url) {
this.url = url;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getChecksum() {
return checksum;
}
public void setChecksum(String checksum) {
this.checksum = checksum;
}
}

View File

@ -69,6 +69,7 @@ public interface IBbbWebApiGWApp {
Boolean notifyRecordingIsOn,
String presentationUploadExternalDescription,
String presentationUploadExternalUrl,
Map<String, Object> plugins,
String overrideClientSettings);
void registerUser(String meetingID, String internalUserId, String fullname, String role,

View File

@ -18,6 +18,8 @@ import scala.concurrent.duration._
import org.bigbluebutton.common2.redis._
import org.bigbluebutton.common2.bus._
import java.util
class BbbWebApiGWApp(
val oldMessageReceivedGW: OldMessageReceivedGW,
redisHost: String,
@ -165,6 +167,7 @@ class BbbWebApiGWApp(
notifyRecordingIsOn: java.lang.Boolean,
presentationUploadExternalDescription: String,
presentationUploadExternalUrl: String,
plugins: util.Map[String, AnyRef],
overrideClientSettings: String): Unit = {
val disabledFeaturesAsVector: Vector[String] = disabledFeatures.asScala.toVector
@ -262,6 +265,7 @@ class BbbWebApiGWApp(
val groupsAsVector: Vector[GroupProps] = groups.asScala.toVector.map(g => GroupProps(g.getGroupId(), g.getName(), g.getUsersExtId().asScala.toVector))
val defaultProps = DefaultProps(
plugins,
meetingProp,
breakoutProps,
durationProps,

View File

@ -2063,6 +2063,18 @@ and n."createdAt" > current_timestamp - '5 seconds'::interval;
create index idx_notification on notification("meetingId","userId","role","createdAt");
-- ========== Plugin tables
create table "plugin" (
"meetingId" varchar(100),
"name" varchar(100),
"javascriptEntrypointUrl" varchar(500),
"javascriptEntrypointIntegrity" varchar(500),
CONSTRAINT "plugin_pk" PRIMARY KEY ("meetingId","name"),
FOREIGN KEY ("meetingId") REFERENCES "meeting"("meetingId") ON DELETE CASCADE
);
create view "v_plugin" as select * from "plugin";
--------------------------------
---Plugins Data Channel

View File

@ -0,0 +1,29 @@
table:
name: v_plugin
schema: public
configuration:
column_config: {}
custom_column_names: {}
custom_name: plugin
custom_root_fields: {}
select_permissions:
- role: bbb_client
permission:
columns:
- javascriptEntrypointIntegrity
- javascriptEntrypointUrl
- name
filter:
meetingId:
_eq: X-Hasura-MeetingId
comment: ""
- role: bbb_client_not_in_meeting
permission:
columns:
- javascriptEntrypointIntegrity
- javascriptEntrypointUrl
- name
filter:
meetingId:
_eq: X-Hasura-MeetingId
comment: ""

View File

@ -26,6 +26,7 @@
- "!include public_v_meeting_usersPolicies.yaml"
- "!include public_v_meeting_voiceSettings.yaml"
- "!include public_v_notification.yaml"
- "!include public_v_plugin.yaml"
- "!include public_v_pluginDataChannelEntry.yaml"
- "!include public_v_poll.yaml"
- "!include public_v_poll_option.yaml"

View File

@ -31,6 +31,8 @@ import { LoadingContext } from '/imports/ui/components/common/loading-screen/loa
import IntlAdapter from '/imports/startup/client/intlAdapter';
import PresenceAdapter from '../imports/ui/components/presence-adapter/component';
import CustomUsersSettings from '/imports/ui/components/join-handler/custom-users-settings/component';
import createUseSubscription from '/imports/ui/core/hooks/createUseSubscription';
import PLUGIN_CONFIGURATION_QUERY from '/imports/ui/components/plugins-engine/query';
// eslint-disable-next-line import/prefer-default-export
const Startup = () => {
@ -60,11 +62,14 @@ const Startup = () => {
}, message);
});
const { data: pluginConfig } = createUseSubscription(
PLUGIN_CONFIGURATION_QUERY,
)((obj) => obj);
return (
<ContextProviders>
<PresenceAdapter>
<IntlAdapter>
<Base />
<Base pluginConfig={pluginConfig} />
</IntlAdapter>
</PresenceAdapter>
</ContextProviders>

View File

@ -249,6 +249,7 @@ class App extends Component {
presentationIsOpen,
darkTheme,
intl,
pluginConfig,
genericMainContentId,
} = this.props;
@ -260,7 +261,7 @@ class App extends Component {
return (
<>
<ScreenReaderAlertAdapter />
<PluginsEngineManager />
<PluginsEngineManager pluginConfig={pluginConfig} />
<FloatingWindowContainer />
<TimeSync />
<Notifications />

View File

@ -6,7 +6,7 @@ const PluginLoaderManager = (props: PluginLoaderManagerProps) => {
const {
uuid,
containerRef,
loadedPlugins,
setNumberOfLoadedPlugins,
setLastLoadedPlugin,
pluginConfig: plugin,
} = props;
@ -22,7 +22,7 @@ const PluginLoaderManager = (props: PluginLoaderManagerProps) => {
const script: HTMLScriptElement = document.createElement('script');
script.onload = () => {
loadedPlugins.current += 1;
setNumberOfLoadedPlugins((current) => current + 1);
setLastLoadedPlugin(script);
logger.info({
logCode: 'plugin_loaded',
@ -40,8 +40,8 @@ const PluginLoaderManager = (props: PluginLoaderManagerProps) => {
script.src = plugin.url;
script.setAttribute('uuid', div.id);
script.setAttribute('pluginName', plugin.name);
if (plugin.checksum) {
script.setAttribute('integrity', plugin.checksum);
if (plugin.javascriptEntrypointIntegrity) {
script.setAttribute('integrity', plugin.javascriptEntrypointIntegrity);
}
document.head.appendChild(script);
}, [plugin, containerRef]);

View File

@ -3,7 +3,7 @@ import { PluginConfig } from '../types';
export interface PluginLoaderManagerProps {
uuid: string;
containerRef: React.RefObject<HTMLDivElement>;
loadedPlugins: React.MutableRefObject<number>;
setNumberOfLoadedPlugins: React.Dispatch<React.SetStateAction<number>>;
setLastLoadedPlugin: React.Dispatch<React.SetStateAction<HTMLScriptElement | undefined>>;
pluginConfig: PluginConfig;
}

View File

@ -1,5 +1,5 @@
import React, {
useEffect, useRef, useState, useMemo,
useEffect, useRef, useState,
} from 'react';
import logger from '/imports/startup/client/logger';
import {
@ -9,7 +9,7 @@ import * as PluginSdk from 'bigbluebutton-html-plugin-sdk';
import * as uuidLib from 'uuid';
import PluginDataConsumptionManager from './data-consumption/manager';
import PluginsEngineComponent from './component';
import { PluginConfig, EffectivePluginConfig } from './types';
import { EffectivePluginConfig, PluginsEngineManagerProps } from './types';
import PluginLoaderManager from './loader/manager';
import ExtensibleAreaStateManager from './extensible-areas/manager';
import PluginDataChannelManager from './data-channel/manager';
@ -18,34 +18,38 @@ import PluginDomElementManipulationManager from './dom-element-manipulation/mana
import PluginServerCommandsHandler from './server-commands/handler';
import PluginLearningAnalyticsDashboardManager from './learning-analytics-dashboard/manager';
const PluginsEngineManager = () => {
const PluginsEngineManager = (props: PluginsEngineManagerProps) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - temporary, while meteor exists in the project
const PLUGINS_CONFIG = window.meetingClientSettings.public.plugins;
const { pluginConfig } = props;
// If there is no plugin to load, the engine simply returns null
if (!PLUGINS_CONFIG) return null;
const containerRef = useRef<HTMLDivElement>(null);
const [lastLoadedPlugin, setLastLoadedPlugin] = useState<HTMLScriptElement | undefined>();
const loadedPlugins = useRef<number>(0);
const [effectivePluginsConfig, setEffectivePluginsConfig] = useState<EffectivePluginConfig[] | undefined>();
const [numberOfLoadedPlugins, setNumberOfLoadedPlugins] = useState<number>(0);
const effectivePluginsConfig: EffectivePluginConfig[] = useMemo<EffectivePluginConfig[]>(
() => PLUGINS_CONFIG.map((p: PluginConfig) => ({
useEffect(() => {
setEffectivePluginsConfig(
pluginConfig?.map((p) => ({
...p,
name: p.name,
url: p.javascriptEntrypointUrl,
uuid: uuidLib.v4(),
} as EffectivePluginConfig)), [
PLUGINS_CONFIG,
],
} as EffectivePluginConfig)),
);
}, [
pluginConfig,
]);
const totalNumberOfPlugins = PLUGINS_CONFIG?.length;
const totalNumberOfPlugins = pluginConfig?.length;
window.React = React;
useEffect(() => {
logger.info(`${loadedPlugins.current}/${totalNumberOfPlugins} plugins loaded`);
if (totalNumberOfPlugins) logger.info(`${numberOfLoadedPlugins}/${totalNumberOfPlugins} plugins loaded`);
},
[loadedPlugins.current, lastLoadedPlugin]);
[numberOfLoadedPlugins, lastLoadedPlugin]);
return (
<>
@ -59,7 +63,7 @@ const PluginsEngineManager = () => {
<PluginUiCommandsHandler />
<PluginDomElementManipulationManager />
{
effectivePluginsConfig.map((effectivePluginConfig: EffectivePluginConfig) => {
effectivePluginsConfig?.map((effectivePluginConfig: EffectivePluginConfig) => {
const { uuid, name: pluginName } = effectivePluginConfig;
const pluginApi: PluginSdk.PluginApi = BbbPluginSdk.getPluginApi(uuid, pluginName);
return (
@ -68,7 +72,7 @@ const PluginsEngineManager = () => {
{...{
uuid,
containerRef,
loadedPlugins,
setNumberOfLoadedPlugins,
setLastLoadedPlugin,
pluginConfig: effectivePluginConfig,
}}

View File

@ -0,0 +1,11 @@
import { gql } from '@apollo/client';
const PLUGIN_CONFIGURATION_QUERY = gql`query PluginConfigurationQuery {
plugin {
name,
javascriptEntrypointUrl,
javascriptEntrypointIntegrity,
}
}`;
export default PLUGIN_CONFIGURATION_QUERY;

View File

@ -2,10 +2,10 @@ import styled from 'styled-components';
// eslint-disable-next-line import/prefer-default-export
export const PluginsEngine = styled.div`
position: 'absolute',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
zIndex: -1,
position: 'absolute';
top: 0;
left: 0;
width: '100vw';
height: '100vh';
z-index: -1;
`;

View File

@ -4,10 +4,19 @@ export interface PluginsEngineComponentProps {
containerRef: React.RefObject<HTMLDivElement>;
}
export interface PluginConfigFromGraphql {
javascriptEntrypointUrl: string;
name: string;
}
export interface PluginsEngineManagerProps {
pluginConfig: PluginConfigFromGraphql[] | undefined;
}
export interface PluginConfig {
name: string;
url: string;
checksum?: string;
javascriptEntrypointIntegrity?: string;
}
export interface EffectivePluginConfig extends PluginConfig {

View File

@ -477,3 +477,7 @@ breakoutRoomsEnabled=true
# legacy, please use maxUserConcurrentAccesses instead
allowDuplicateExtUserid=true
# list of plugins manifests (json array)
# e.g: [{url: "https://plugin_manifest.json"}]
pluginsManifests=

View File

@ -201,6 +201,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<property name="defaultKeepEvents" value="${defaultKeepEvents}"/>
<property name="allowRevealOfBBBVersion" value="${allowRevealOfBBBVersion}"/>
<property name="allowOverrideClientSettingsOnCreateCall" value="${allowOverrideClientSettingsOnCreateCall}"/>
<property name="pluginsManifests" value="${pluginsManifests}"/>
</bean>
<bean id="presentationService" class="org.bigbluebutton.web.services.PresentationService">

View File

@ -90,6 +90,12 @@ class ConnectionController {
}
response.addHeader("Meeting-Id", userSession.meetingID)
response.addHeader("Meeting-External-Id", userSession.externMeetingID)
response.addHeader("User-Id", userSession.internalUserId)
response.addHeader("User-External-Id", userSession.externUserID)
response.addHeader("User-Name", URLEncoder.encode(userSession.fullname, StandardCharsets.UTF_8.name()))
response.addHeader("User-Is-Moderator", u && u.isModerator() ? "true" : "false")
response.addHeader("User-Is-Presenter", u && u.isPresenter() ? "true" : "false")
response.setStatus(200)
withFormat {
json {
@ -109,6 +115,12 @@ class ConnectionController {
UserSessionBasicData removedUserSession = meetingService.getRemovedUserSessionWithSessionToken(sessionToken)
if(removedUserSession) {
response.addHeader("Meeting-Id", removedUserSession.meetingId)
response.addHeader("Meeting-External-Id", removedUserSession.externMeetingID)
response.addHeader("User-Id", removedUserSession.internalUserId)
response.addHeader("User-External-Id", removedUserSession.externUserID)
response.addHeader("User-Name", URLEncoder.encode(removedUserSession.fullname, StandardCharsets.UTF_8.name()))
response.addHeader("User-Is-Moderator", removedUserSession.isModerator() ? "true" : "false")
response.addHeader("User-Is-Presenter", "false")
response.setStatus(200)
withFormat {
json {