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

View File

@ -1,55 +1,30 @@
package org.bigbluebutton.core.apps.plugin package org.bigbluebutton.core.apps.plugin
import org.bigbluebutton.common2.msgs.PluginDataChannelPushEntryMsg 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.db.PluginDataChannelEntryDAO
import org.bigbluebutton.core.domain.MeetingState2x import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.models.{ Roles, Users2x }
import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting } import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting }
trait PluginDataChannelPushEntryMsgHdlr extends HandlerHelpers { trait PluginDataChannelPushEntryMsgHdlr extends HandlerHelpers {
def handle(msg: PluginDataChannelPushEntryMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = { def handle(msg: PluginDataChannelPushEntryMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = {
val pluginsDisabled: Boolean = liveMeeting.props.meetingProp.disabledFeatures.contains("plugins") dataChannelCheckingLogic(liveMeeting, msg.header.userId, msg.body.pluginName, msg.body.channelName, (user, dc, meetingId) => {
val meetingId = liveMeeting.props.meetingProp.intId val hasPermission = checkPermission(user, dc.pushPermission)
if (!hasPermission.contains(true)) {
for { println(s"No permission to write in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.")
_ <- 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 { } else {
val hasPermission = for { PluginDataChannelEntryDAO.insert(
pushPermission <- pluginsConfig(msg.body.pluginName).dataChannels(msg.body.channelName).pushPermission meetingId,
} yield { msg.body.pluginName,
pushPermission.toLowerCase match { msg.body.channelName,
case "all" => true msg.body.subChannelName,
case "moderator" => user.role == Roles.MODERATOR_ROLE msg.header.userId,
case "presenter" => user.presenter msg.body.payloadJson,
case _ => false msg.body.toRoles,
} msg.body.toUserIds
} )
if (!hasPermission.contains(true)) {
println(s"No permission to write in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.")
} else {
PluginDataChannelEntryDAO.insert(
meetingId,
msg.body.pluginName,
msg.body.channelName,
msg.body.subChannelName,
msg.header.userId,
msg.body.payloadJson,
msg.body.toRoles,
msg.body.toUserIds
)
}
} }
} })
} }
} }

View File

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

View File

@ -1,51 +1,27 @@
package org.bigbluebutton.core.apps.plugin package org.bigbluebutton.core.apps.plugin
import org.bigbluebutton.ClientSettings
import org.bigbluebutton.common2.msgs.PluginDataChannelResetMsg 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.db.PluginDataChannelEntryDAO
import org.bigbluebutton.core.domain.MeetingState2x import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.models.{ Roles, Users2x }
import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting } import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting }
trait PluginDataChannelResetMsgHdlr extends HandlerHelpers { trait PluginDataChannelResetMsgHdlr extends HandlerHelpers {
def handle(msg: PluginDataChannelResetMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = { def handle(msg: PluginDataChannelResetMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = {
val pluginsDisabled: Boolean = liveMeeting.props.meetingProp.disabledFeatures.contains("plugins") dataChannelCheckingLogic(liveMeeting, msg.header.userId, msg.body.pluginName, msg.body.channelName, (user, dc, meetingId) => {
val meetingId = liveMeeting.props.meetingProp.intId val hasPermission = checkPermission(user, dc.replaceOrDeletePermission)
for { if (!hasPermission.contains(true)) {
_ <- if (!pluginsDisabled) Some(()) else None println(s"No permission to delete (reset) in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.")
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 { } else {
val hasPermission = for { PluginDataChannelEntryDAO.reset(
replaceOrDeletePermission <- pluginsConfig(msg.body.pluginName).dataChannels(msg.body.channelName).replaceOrDeletePermission meetingId,
} yield { msg.body.pluginName,
replaceOrDeletePermission.toLowerCase match { msg.body.channelName,
case "all" => true msg.body.subChannelName
case "moderator" => user.role == Roles.MODERATOR_ROLE )
case "presenter" => user.presenter
case _ => false
}
}
if (!hasPermission.contains(true)) {
println(s"No permission to delete (reset) in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.")
} else {
PluginDataChannelEntryDAO.reset(
meetingId,
msg.body.pluginName,
msg.body.channelName,
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 org.bigbluebutton.common2.domain.DefaultProps
import PostgresProfile.api._ import PostgresProfile.api._
import org.bigbluebutton.core.apps.groupchats.GroupChatApp import org.bigbluebutton.core.apps.groupchats.GroupChatApp
import org.bigbluebutton.core.models.PluginModel
case class MeetingSystemColumnsDbModel( case class MeetingSystemColumnsDbModel(
loginUrl: Option[String], loginUrl: Option[String],
@ -85,7 +86,7 @@ class MeetingDbTableDef(tag: Tag) extends Table[MeetingDbModel](tag, None, "meet
} }
object MeetingDAO { object MeetingDAO {
def insert(meetingProps: DefaultProps, clientSettings: Map[String, Object]) = { def insert(meetingProps: DefaultProps, clientSettings: Map[String, Object], pluginProps: PluginModel) = {
DatabaseConnection.enqueue( DatabaseConnection.enqueue(
TableQuery[MeetingDbTableDef].forceInsert( TableQuery[MeetingDbTableDef].forceInsert(
MeetingDbModel( MeetingDbModel(
@ -148,6 +149,7 @@ object MeetingDAO {
MeetingBreakoutDAO.insert(meetingProps.meetingProp.intId, meetingProps.breakoutProps) MeetingBreakoutDAO.insert(meetingProps.meetingProp.intId, meetingProps.breakoutProps)
LayoutDAO.insert(meetingProps.meetingProp.intId, meetingProps.usersProp.meetingLayout) LayoutDAO.insert(meetingProps.meetingProp.intId, meetingProps.usersProp.meetingLayout)
MeetingClientSettingsDAO.insert(meetingProps.meetingProp.intId, JsonUtils.mapToJson(clientSettings)) MeetingClientSettingsDAO.insert(meetingProps.meetingProp.intId, JsonUtils.mapToJson(clientSettings))
PluginModel.persistPluginsForClient(pluginProps, meetingProps.meetingProp.intId)
} }
def updateMeetingDurationByParentMeeting(parentMeetingId: String, newDurationInSeconds: Int) = { 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 users2x: Users2x,
val guestsWaiting: GuestsWaiting, val guestsWaiting: GuestsWaiting,
val clientSettings: Map[String, Object], val clientSettings: Map[String, Object],
val plugins: PluginModel,
) )

View File

@ -172,7 +172,7 @@ class MeetingActor(
outGW.send(msgEvent) outGW.send(msgEvent)
//Insert meeting into the database //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 // Create a default public group chat
state = groupChatApp.handleCreateDefaultPublicGroupChat(state, liveMeeting, msgBus) 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.apache.pekko.actor.ActorContext
import org.bigbluebutton.ClientSettings import org.bigbluebutton.ClientSettings
import org.bigbluebutton.ClientSettings.{getConfigPropertyValueByPathAsBooleanOrElse, getConfigPropertyValueByPathAsStringOrElse}
import org.bigbluebutton.common2.domain.DefaultProps import org.bigbluebutton.common2.domain.DefaultProps
import org.bigbluebutton.core.apps._ import org.bigbluebutton.core.apps._
import org.bigbluebutton.core.bus._ import org.bigbluebutton.core.bus._
import org.bigbluebutton.core.models._ import org.bigbluebutton.core.models._
import org.bigbluebutton.core.OutMessageGateway import org.bigbluebutton.core.OutMessageGateway
import org.bigbluebutton.core.apps.pads.PadslHdlrHelpers
import org.bigbluebutton.core2.MeetingStatus2x import org.bigbluebutton.core2.MeetingStatus2x
object RunningMeeting { object RunningMeeting {
@ -19,9 +17,9 @@ object RunningMeeting {
class RunningMeeting(val props: DefaultProps, outGW: OutMessageGateway, class RunningMeeting(val props: DefaultProps, outGW: OutMessageGateway,
eventBus: InternalEventBus)(implicit val context: ActorContext) { eventBus: InternalEventBus)(implicit val context: ActorContext) {
private val externalVideoModel = new ExternalVideoModel() private val externalVideoModel = new ExternalVideoModel()
private val chatModel = new ChatModel() private val chatModel = new ChatModel()
private val plugins = PluginModel.createPluginModelFromJson(props.pluginProp)
private val layouts = new Layouts() private val layouts = new Layouts()
private val pads = new Pads() private val pads = new Pads()
private val wbModel = new WhiteboardModel() private val wbModel = new WhiteboardModel()
@ -45,7 +43,7 @@ class RunningMeeting(val props: DefaultProps, outGW: OutMessageGateway,
// easy to test. // easy to test.
private val liveMeeting = new LiveMeeting(props, meetingStatux2x, deskshareModel, audioCaptions, timerModel, private val liveMeeting = new LiveMeeting(props, meetingStatux2x, deskshareModel, audioCaptions, timerModel,
chatModel, externalVideoModel, layouts, pads, registeredUsers, polls2x, wbModel, presModel, captionModel, chatModel, externalVideoModel, layouts, pads, registeredUsers, polls2x, wbModel, presModel, captionModel,
webcams, voiceUsers, users2x, guestsWaiting, clientSettings) webcams, voiceUsers, users2x, guestsWaiting, clientSettings, plugins)
GuestsWaiting.setGuestPolicy( GuestsWaiting.setGuestPolicy(
liveMeeting.props.meetingProp.intId, liveMeeting.props.meetingProp.intId,

View File

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

View File

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

View File

@ -73,6 +73,7 @@ public class ApiParams {
public static final String ROLE = "role"; public static final String ROLE = "role";
public static final String GROUPS = "groups"; public static final String GROUPS = "groups";
public static final String DISABLED_FEATURES = "disabledFeatures"; 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 DISABLED_FEATURES_EXCLUDE = "disabledFeaturesExclude";
public static final String NOTIFY_RECORDING_IS_ON = "notifyRecordingIsOn"; public static final String NOTIFY_RECORDING_IS_ON = "notifyRecordingIsOn";

View File

@ -19,7 +19,11 @@
package org.bigbluebutton.api; package org.bigbluebutton.api;
import java.io.File; import java.io.File;
import java.io.InputStream;
import java.net.URI; import java.net.URI;
import java.net.URL;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.util.*; import java.util.*;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
@ -29,7 +33,11 @@ import java.util.concurrent.Executor;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue; 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 com.google.gson.JsonObject;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.utils.URIBuilder; import org.apache.http.client.utils.URIBuilder;
import org.bigbluebutton.api.domain.*; import org.bigbluebutton.api.domain.*;
@ -57,6 +65,8 @@ import com.google.gson.Gson;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.data.domain.*; import org.springframework.data.domain.*;
@ -97,6 +107,8 @@ public class MeetingService implements MessageListener {
private HashMap<String, PresentationUploadToken> uploadAuthzTokens; private HashMap<String, PresentationUploadToken> uploadAuthzTokens;
ObjectMapper objectMapper = new ObjectMapper();
public MeetingService() { public MeetingService() {
meetings = new ConcurrentHashMap<String, Meeting>(8, 0.9f, 1); meetings = new ConcurrentHashMap<String, Meeting>(8, 0.9f, 1);
sessions = new ConcurrentHashMap<String, UserSession>(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()); : 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) { public synchronized boolean createMeeting(Meeting m) {
String internalMeetingId = paramsProcessorUtil.convertToInternalMeetingId(m.getExternalId()); String internalMeetingId = paramsProcessorUtil.convertToInternalMeetingId(m.getExternalId());
Meeting existingId = getNotEndedMeetingWithId(internalMeetingId); Meeting existingId = getNotEndedMeetingWithId(internalMeetingId);
@ -359,6 +467,8 @@ public class MeetingService implements MessageListener {
Meeting existingWebVoice = getNotEndedMeetingWithWebVoice(m.getWebVoice()); Meeting existingWebVoice = getNotEndedMeetingWithWebVoice(m.getWebVoice());
if (existingId == null && existingTelVoice == null && existingWebVoice == null) { if (existingId == null && existingTelVoice == null && existingWebVoice == null) {
meetings.put(m.getInternalId(), m); meetings.put(m.getInternalId(), m);
Map<String, Object> requestedManifests = requestPluginManifests(m);
m.setPlugins(requestedManifests);
handle(new CreateMeeting(m)); handle(new CreateMeeting(m));
return true; return true;
} }
@ -444,7 +554,7 @@ public class MeetingService implements MessageListener {
m.getMuteOnStart(), m.getAllowModsToUnmuteUsers(), m.getAllowModsToEjectCameras(), m.getMeetingKeepEvents(), m.getMuteOnStart(), m.getAllowModsToUnmuteUsers(), m.getAllowModsToEjectCameras(), m.getMeetingKeepEvents(),
m.breakoutRoomsParams, m.lockSettingsParams, m.getLoginUrl(), m.getLogoutUrl(), m.getCustomLogoURL(), m.getCustomDarkLogoURL(), m.breakoutRoomsParams, m.lockSettingsParams, m.getLoginUrl(), m.getLogoutUrl(), m.getCustomLogoURL(), m.getCustomDarkLogoURL(),
m.getBannerText(), m.getBannerColor(), m.getGroups(), m.getDisabledFeatures(), m.getNotifyRecordingIsOn(), m.getBannerText(), m.getBannerColor(), m.getGroups(), m.getDisabledFeatures(), m.getNotifyRecordingIsOn(),
m.getPresentationUploadExternalDescription(), m.getPresentationUploadExternalUrl(), m.getPresentationUploadExternalDescription(), m.getPresentationUploadExternalUrl(), m.getPlugins(),
m.getOverrideClientSettings()); m.getOverrideClientSettings());
} }

View File

@ -26,10 +26,9 @@ import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import com.google.gson.Gson;
import com.google.gson.JsonArray; import com.google.gson.*;
import com.google.gson.JsonElement; import org.bigbluebutton.api.domain.*;
import com.google.gson.JsonObject;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import org.jsoup.safety.Safelist; 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.codec.digest.DigestUtils;
import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils; 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.service.ServiceUtils;
import org.bigbluebutton.api.util.ParamsUtil; import org.bigbluebutton.api.util.ParamsUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -104,6 +99,7 @@ public class ParamsProcessorUtil {
private boolean defaultAllowModsToUnmuteUsers = false; private boolean defaultAllowModsToUnmuteUsers = false;
private boolean defaultAllowModsToEjectCameras = false; private boolean defaultAllowModsToEjectCameras = false;
private String defaultDisabledFeatures; private String defaultDisabledFeatures;
private String defaultPluginsManifests;
private boolean defaultNotifyRecordingIsOn = false; private boolean defaultNotifyRecordingIsOn = false;
private boolean defaultKeepEvents = false; private boolean defaultKeepEvents = false;
private Boolean useDefaultLogo; private Boolean useDefaultLogo;
@ -432,6 +428,33 @@ public class ParamsProcessorUtil {
return groups; 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) { public Meeting processCreateParams(Map<String, String> params) {
String meetingName = params.get(ApiParams.NAME); String meetingName = params.get(ApiParams.NAME);
@ -550,6 +573,20 @@ public class ParamsProcessorUtil {
listOfDisabledFeatures.removeAll(Arrays.asList(disabledFeaturesExcludeParam.split(","))); 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 // Check if VirtualBackgrounds is disabled
if (!StringUtils.isEmpty(params.get(ApiParams.VIRTUAL_BACKGROUNDS_DISABLED))) { if (!StringUtils.isEmpty(params.get(ApiParams.VIRTUAL_BACKGROUNDS_DISABLED))) {
boolean virtualBackgroundsDisabled = Boolean.valueOf(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) .withLearningDashboardCleanupDelayInMinutes(learningDashboardCleanupMins)
.withLearningDashboardAccessToken(learningDashboardAccessToken) .withLearningDashboardAccessToken(learningDashboardAccessToken)
.withGroups(groups) .withGroups(groups)
.withPluginManifests(listOfPluginsManifests)
.withDisabledFeatures(listOfDisabledFeatures) .withDisabledFeatures(listOfDisabledFeatures)
.withNotifyRecordingIsOn(notifyRecordingIsOn) .withNotifyRecordingIsOn(notifyRecordingIsOn)
.withPresentationUploadExternalDescription(presentationUploadExternalDescription) .withPresentationUploadExternalDescription(presentationUploadExternalDescription)
@ -1574,36 +1612,40 @@ public class ParamsProcessorUtil {
this.defaultEndWhenNoModerator = val; this.defaultEndWhenNoModerator = val;
} }
public void setEndWhenNoModeratorDelayInMinutes(Integer value) { public void setEndWhenNoModeratorDelayInMinutes(Integer value) {
this.defaultEndWhenNoModeratorDelayInMinutes = value; this.defaultEndWhenNoModeratorDelayInMinutes = value;
} }
public void setDisabledFeatures(String disabledFeatures) { public void setDisabledFeatures(String disabledFeatures) {
this.defaultDisabledFeatures = disabledFeatures; this.defaultDisabledFeatures = disabledFeatures;
} }
public void setNotifyRecordingIsOn(Boolean notifyRecordingIsOn) { public void setPluginsManifests(String pluginsManifests) {
this.defaultNotifyRecordingIsOn = notifyRecordingIsOn; this.defaultPluginsManifests = pluginsManifests;
} }
public void setPresentationUploadExternalDescription(String presentationUploadExternalDescription) { public void setNotifyRecordingIsOn(Boolean notifyRecordingIsOn) {
this.defaultPresentationUploadExternalDescription = presentationUploadExternalDescription; this.defaultNotifyRecordingIsOn = notifyRecordingIsOn;
} }
public void setPresentationUploadExternalUrl(String presentationUploadExternalUrl) { public void setPresentationUploadExternalDescription(String presentationUploadExternalDescription) {
this.defaultPresentationUploadExternalUrl = presentationUploadExternalUrl; this.defaultPresentationUploadExternalDescription = presentationUploadExternalDescription;
} }
public void setBbbVersion(String version) { public void setPresentationUploadExternalUrl(String presentationUploadExternalUrl) {
this.defaultPresentationUploadExternalUrl = presentationUploadExternalUrl;
}
public void setBbbVersion(String version) {
this.bbbVersion = this.allowRevealOfBBBVersion ? version : ""; this.bbbVersion = this.allowRevealOfBBBVersion ? version : "";
} }
public void setAllowRevealOfBBBVersion(Boolean allowVersion) { public void setAllowRevealOfBBBVersion(Boolean allowVersion) {
this.allowRevealOfBBBVersion = allowVersion; this.allowRevealOfBBBVersion = allowVersion;
} }
public void setAllowOverrideClientSettingsOnCreateCall(Boolean allowOverrideClientSettingsOnCreateCall) { public void setAllowOverrideClientSettingsOnCreateCall(Boolean allowOverrideClientSettingsOnCreateCall) {
this.allowOverrideClientSettingsOnCreateCall = allowOverrideClientSettingsOnCreateCall; this.allowOverrideClientSettingsOnCreateCall = allowOverrideClientSettingsOnCreateCall;
} }
} }

View File

@ -78,6 +78,8 @@ public class Meeting {
private String dialNumber; private String dialNumber;
private String defaultAvatarURL; private String defaultAvatarURL;
private String defaultWebcamBackgroundURL; private String defaultWebcamBackgroundURL;
private Map<String, Object> plugins;
private ArrayList<PluginsManifest> pluginsManifests;
private String guestPolicy = GuestPolicy.ASK_MODERATOR; private String guestPolicy = GuestPolicy.ASK_MODERATOR;
private String guestLobbyMessage = ""; private String guestLobbyMessage = "";
private Map<String,String> usersWithGuestLobbyMessages; private Map<String,String> usersWithGuestLobbyMessages;
@ -128,6 +130,7 @@ public class Meeting {
extMeetingId = builder.externalId; extMeetingId = builder.externalId;
intMeetingId = builder.internalId; intMeetingId = builder.internalId;
disabledFeatures = builder.disabledFeatures; disabledFeatures = builder.disabledFeatures;
pluginsManifests = builder.pluginsManifests;
notifyRecordingIsOn = builder.notifyRecordingIsOn; notifyRecordingIsOn = builder.notifyRecordingIsOn;
presentationUploadExternalDescription = builder.presentationUploadExternalDescription; presentationUploadExternalDescription = builder.presentationUploadExternalDescription;
presentationUploadExternalUrl = builder.presentationUploadExternalUrl; presentationUploadExternalUrl = builder.presentationUploadExternalUrl;
@ -441,6 +444,17 @@ public class Meeting {
public ArrayList<String> getDisabledFeatures() { public ArrayList<String> getDisabledFeatures() {
return disabledFeatures; 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() { public Boolean getNotifyRecordingIsOn() {
return notifyRecordingIsOn; return notifyRecordingIsOn;
@ -929,6 +943,7 @@ public class Meeting {
private int learningDashboardCleanupDelayInMinutes; private int learningDashboardCleanupDelayInMinutes;
private String learningDashboardAccessToken; private String learningDashboardAccessToken;
private ArrayList<String> disabledFeatures; private ArrayList<String> disabledFeatures;
private ArrayList<PluginsManifest> pluginsManifests;
private Boolean notifyRecordingIsOn; private Boolean notifyRecordingIsOn;
private String presentationUploadExternalDescription; private String presentationUploadExternalDescription;
private String presentationUploadExternalUrl; private String presentationUploadExternalUrl;
@ -1063,6 +1078,11 @@ public class Meeting {
return this; return this;
} }
public Builder withPluginManifests(ArrayList<PluginsManifest> map) {
this.pluginsManifests = map;
return this;
}
public Builder withNotifyRecordingIsOn(Boolean b) { public Builder withNotifyRecordingIsOn(Boolean b) {
this.notifyRecordingIsOn = b; this.notifyRecordingIsOn = b;
return this; 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, Boolean notifyRecordingIsOn,
String presentationUploadExternalDescription, String presentationUploadExternalDescription,
String presentationUploadExternalUrl, String presentationUploadExternalUrl,
Map<String, Object> plugins,
String overrideClientSettings); String overrideClientSettings);
void registerUser(String meetingID, String internalUserId, String fullname, String role, 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.redis._
import org.bigbluebutton.common2.bus._ import org.bigbluebutton.common2.bus._
import java.util
class BbbWebApiGWApp( class BbbWebApiGWApp(
val oldMessageReceivedGW: OldMessageReceivedGW, val oldMessageReceivedGW: OldMessageReceivedGW,
redisHost: String, redisHost: String,
@ -165,6 +167,7 @@ class BbbWebApiGWApp(
notifyRecordingIsOn: java.lang.Boolean, notifyRecordingIsOn: java.lang.Boolean,
presentationUploadExternalDescription: String, presentationUploadExternalDescription: String,
presentationUploadExternalUrl: String, presentationUploadExternalUrl: String,
plugins: util.Map[String, AnyRef],
overrideClientSettings: String): Unit = { overrideClientSettings: String): Unit = {
val disabledFeaturesAsVector: Vector[String] = disabledFeatures.asScala.toVector 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 groupsAsVector: Vector[GroupProps] = groups.asScala.toVector.map(g => GroupProps(g.getGroupId(), g.getName(), g.getUsersExtId().asScala.toVector))
val defaultProps = DefaultProps( val defaultProps = DefaultProps(
plugins,
meetingProp, meetingProp,
breakoutProps, breakoutProps,
durationProps, 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"); 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 ---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_usersPolicies.yaml"
- "!include public_v_meeting_voiceSettings.yaml" - "!include public_v_meeting_voiceSettings.yaml"
- "!include public_v_notification.yaml" - "!include public_v_notification.yaml"
- "!include public_v_plugin.yaml"
- "!include public_v_pluginDataChannelEntry.yaml" - "!include public_v_pluginDataChannelEntry.yaml"
- "!include public_v_poll.yaml" - "!include public_v_poll.yaml"
- "!include public_v_poll_option.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 IntlAdapter from '/imports/startup/client/intlAdapter';
import PresenceAdapter from '../imports/ui/components/presence-adapter/component'; import PresenceAdapter from '../imports/ui/components/presence-adapter/component';
import CustomUsersSettings from '/imports/ui/components/join-handler/custom-users-settings/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 // eslint-disable-next-line import/prefer-default-export
const Startup = () => { const Startup = () => {
@ -60,11 +62,14 @@ const Startup = () => {
}, message); }, message);
}); });
const { data: pluginConfig } = createUseSubscription(
PLUGIN_CONFIGURATION_QUERY,
)((obj) => obj);
return ( return (
<ContextProviders> <ContextProviders>
<PresenceAdapter> <PresenceAdapter>
<IntlAdapter> <IntlAdapter>
<Base /> <Base pluginConfig={pluginConfig} />
</IntlAdapter> </IntlAdapter>
</PresenceAdapter> </PresenceAdapter>
</ContextProviders> </ContextProviders>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -477,3 +477,7 @@ breakoutRoomsEnabled=true
# legacy, please use maxUserConcurrentAccesses instead # legacy, please use maxUserConcurrentAccesses instead
allowDuplicateExtUserid=true 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="defaultKeepEvents" value="${defaultKeepEvents}"/>
<property name="allowRevealOfBBBVersion" value="${allowRevealOfBBBVersion}"/> <property name="allowRevealOfBBBVersion" value="${allowRevealOfBBBVersion}"/>
<property name="allowOverrideClientSettingsOnCreateCall" value="${allowOverrideClientSettingsOnCreateCall}"/> <property name="allowOverrideClientSettingsOnCreateCall" value="${allowOverrideClientSettingsOnCreateCall}"/>
<property name="pluginsManifests" value="${pluginsManifests}"/>
</bean> </bean>
<bean id="presentationService" class="org.bigbluebutton.web.services.PresentationService"> <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-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) response.setStatus(200)
withFormat { withFormat {
json { json {
@ -109,6 +115,12 @@ class ConnectionController {
UserSessionBasicData removedUserSession = meetingService.getRemovedUserSessionWithSessionToken(sessionToken) UserSessionBasicData removedUserSession = meetingService.getRemovedUserSessionWithSessionToken(sessionToken)
if(removedUserSession) { if(removedUserSession) {
response.addHeader("Meeting-Id", removedUserSession.meetingId) 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) response.setStatus(200)
withFormat { withFormat {
json { json {