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:
commit
1eeff8d142
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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) = {
|
||||
|
@ -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,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
@ -27,4 +27,5 @@ class LiveMeeting(
|
||||
val users2x: Users2x,
|
||||
val guestsWaiting: GuestsWaiting,
|
||||
val clientSettings: Map[String, Object],
|
||||
val plugins: PluginModel,
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
@ -20,13 +28,13 @@ case class PluginDataChannelPushEntryMsgBody(
|
||||
|
||||
object PluginDataChannelReplaceEntryMsg { val NAME = "PluginDataChannelReplaceEntryMsg" }
|
||||
case class PluginDataChannelReplaceEntryMsg(header: BbbClientMsgHeader, body: PluginDataChannelReplaceEntryMsgBody) extends StandardMsg
|
||||
case class PluginDataChannelReplaceEntryMsgBody(
|
||||
case class PluginDataChannelReplaceEntryMsgBody (
|
||||
pluginName: String,
|
||||
channelName: String,
|
||||
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" }
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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: ""
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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 />
|
||||
|
@ -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]);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
}}
|
||||
|
@ -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;
|
@ -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;
|
||||
`;
|
||||
|
@ -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 {
|
||||
|
@ -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=
|
||||
|
@ -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">
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user