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
|
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
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasPermission.contains(true)) {
|
if (!hasPermission.contains(true)) {
|
||||||
println(s"No permission to delete in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.")
|
println(s"No permission to delete in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.")
|
||||||
} else {
|
} else {
|
||||||
@ -56,7 +24,6 @@ trait PluginDataChannelDeleteEntryMsgHdlr extends HandlerHelpers {
|
|||||||
msg.body.entryId
|
msg.body.entryId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,40 +1,16 @@
|
|||||||
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)
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasPermission.contains(true)) {
|
if (!hasPermission.contains(true)) {
|
||||||
println(s"No permission to write in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.")
|
println(s"No permission to write in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.")
|
||||||
} else {
|
} else {
|
||||||
@ -49,7 +25,6 @@ trait PluginDataChannelPushEntryMsgHdlr extends HandlerHelpers {
|
|||||||
msg.body.toUserIds
|
msg.body.toUserIds
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,49 +1,18 @@
|
|||||||
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)) {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasPermission.contains(true)) {
|
if (!hasPermission.contains(true)) {
|
||||||
println(s"No permission to write in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.")
|
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),
|
JsonUtils.mapToJson(msg.body.payloadJson),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,39 +1,16 @@
|
|||||||
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 (!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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasPermission.contains(true)) {
|
if (!hasPermission.contains(true)) {
|
||||||
println(s"No permission to delete (reset) in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.")
|
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
|
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 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) = {
|
||||||
|
@ -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 users2x: Users2x,
|
||||||
val guestsWaiting: GuestsWaiting,
|
val guestsWaiting: GuestsWaiting,
|
||||||
val clientSettings: Map[String, Object],
|
val clientSettings: Map[String, Object],
|
||||||
|
val plugins: PluginModel,
|
||||||
)
|
)
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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(
|
||||||
@ -26,7 +34,7 @@ case class PluginDataChannelReplaceEntryMsgBody(
|
|||||||
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" }
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
@ -1582,6 +1620,10 @@ public class ParamsProcessorUtil {
|
|||||||
this.defaultDisabledFeatures = disabledFeatures;
|
this.defaultDisabledFeatures = disabledFeatures;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setPluginsManifests(String pluginsManifests) {
|
||||||
|
this.defaultPluginsManifests = pluginsManifests;
|
||||||
|
}
|
||||||
|
|
||||||
public void setNotifyRecordingIsOn(Boolean notifyRecordingIsOn) {
|
public void setNotifyRecordingIsOn(Boolean notifyRecordingIsOn) {
|
||||||
this.defaultNotifyRecordingIsOn = notifyRecordingIsOn;
|
this.defaultNotifyRecordingIsOn = notifyRecordingIsOn;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
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,
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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_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"
|
||||||
|
@ -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>
|
||||||
|
@ -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 />
|
||||||
|
@ -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]);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
pluginConfig?.map((p) => ({
|
||||||
...p,
|
...p,
|
||||||
|
name: p.name,
|
||||||
|
url: p.javascriptEntrypointUrl,
|
||||||
uuid: uuidLib.v4(),
|
uuid: uuidLib.v4(),
|
||||||
} as EffectivePluginConfig)), [
|
} as EffectivePluginConfig)),
|
||||||
PLUGINS_CONFIG,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
}, [
|
||||||
|
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,
|
||||||
}}
|
}}
|
||||||
|
@ -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
|
// 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;
|
||||||
`;
|
`;
|
||||||
|
@ -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 {
|
||||||
|
@ -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=
|
||||||
|
@ -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">
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user