Merge remote-tracking branch 'bbb/v3.0.x-release' into oct-25

This commit is contained in:
Anton Georgiev 2024-10-25 09:36:38 -04:00
commit cac5d72a27
262 changed files with 7672 additions and 4467 deletions

View File

@ -257,24 +257,73 @@ jobs:
apt --purge -y remove apache2-bin
'
- name: Install BBB
uses: nick-fields/retry@v3
env:
NODE_EXTRA_CA_CERTS: /usr/local/share/ca-certificates/bbb-dev/bbb-dev-ca.crt
ACTIONS_RUNNER_DEBUG: true
with:
timeout_minutes: 25
max_attempts: 2
retry_on: any
command: |
sudo -i <<EOF
set -e
cd /root/ && wget -nv https://raw.githubusercontent.com/bigbluebutton/bbb-install/v3.0.x-release--no-mongo/bbb-install.sh -O bbb-install.sh
cat bbb-install.sh | sed "s|> /etc/apt/sources.list.d/bigbluebutton.list||g" | bash -s -- -v jammy-30-dev -s bbb-ci.test -j -d /certs/
bbb-conf --salt bbbci
sed -i "s/\"minify\": true,/\"minify\": false,/" /usr/share/etherpad-lite/settings.json
sudo yq e -i '.log_level = "TRACE"' /usr/share/bbb-graphql-middleware/config.yml
bbb-conf --restart
EOF
run: |
sudo -i <<'EOF'
set -e
cd /root/
wget -nv https://raw.githubusercontent.com/bigbluebutton/bbb-install/v3.0.x-release--no-mongo/bbb-install.sh -O bbb-install.sh
sed -i "s|> /etc/apt/sources.list.d/bigbluebutton.list||g" bbb-install.sh
chmod +x bbb-install.sh
COMMAND="./bbb-install.sh -v jammy-30-dev -s bbb-ci.test -j -d /certs/"
TIMEOUT=1500 # 25 minutes
MAX_RETRIES=3
RETRY_INTERVAL=60
RETRY_COUNT=0
SUCCESS=0
while [[ $RETRY_COUNT -lt $MAX_RETRIES ]]; do
echo "Attempt $((RETRY_COUNT + 1)) of $MAX_RETRIES to install BBB..."
# Run the command with timeout and handle its exit code
# Capture both stdout and stderr
COMMAND_EXIT_CODE=0
timeout $TIMEOUT $COMMAND || COMMAND_EXIT_CODE=$?
if [[ $COMMAND_EXIT_CODE -eq 0 ]]; then
SUCCESS=1
break
elif [[ $COMMAND_EXIT_CODE -eq 124 ]]; then
echo "Installation timed out after ${TIMEOUT} seconds. Retrying..."
else
echo "Installation failed with exit code $COMMAND_EXIT_CODE"
echo "Retrying installation within $RETRY_INTERVAL seconds..."
sleep $RETRY_INTERVAL
fi
echo "Stop any ongoing processes related to apt-get or dpkg that might be stuck"
# Use -q to suppress "no process found" messages
killall -q apt-get || true
killall -q dpkg || true
echo "Remove the lock files that may have been left behind"
# Group lock file removal for better readability
rm -f /var/lib/dpkg/lock-frontend
rm -f /var/lib/dpkg/lock
rm -f /var/cache/apt/archives/lock
echo "Reconfigure the package manager"
dpkg --configure -a
echo "Clean up any partially installed packages"
apt-get clean
apt-get autoremove
RETRY_COUNT=$((RETRY_COUNT + 1))
done
if [[ $SUCCESS -eq 0 ]]; then
echo "All attempts to install BBB failed."
exit 1
fi
bbb-conf --salt bbbci
sed -i "s/\"minify\": true,/\"minify\": false,/" /usr/share/etherpad-lite/settings.json
sudo yq e -i '.log_level = "TRACE"' /usr/share/bbb-graphql-middleware/config.yml
bbb-conf --restart
EOF
- name: List systemctl services
timeout-minutes: 1
run: |

View File

@ -63,13 +63,23 @@ object BreakoutRoomsUtil extends SystemConfiguration {
checksum(apiCall.concat(baseString).concat(sharedSecret))
}
def joinParams(username: String, userId: String, isBreakout: Boolean, breakoutMeetingId: String,
password: String): (collection.immutable.Map[String, String], collection.immutable.Map[String, String]) = {
def joinParams(
username: String,
userId: String,
isBreakout: Boolean,
breakoutMeetingId: String,
avatarURL: String,
role: String,
password: String
): (collection.immutable.Map[String, String], collection.immutable.Map[String, String]) = {
val moderator = role == "MODERATOR"
val params = collection.immutable.HashMap(
"fullName" -> urlEncode(username),
"userID" -> urlEncode(userId),
"isBreakout" -> urlEncode(isBreakout.toString()),
"meetingID" -> urlEncode(breakoutMeetingId),
"avatarURL" -> urlEncode(avatarURL),
"userdata-bbb_parent_room_moderator" -> urlEncode(moderator.toString()),
"password" -> urlEncode(password),
"redirect" -> urlEncode("true")
)

View File

@ -41,8 +41,15 @@ object BreakoutHdlrHelpers extends SystemConfiguration {
for {
user <- Users2x.findWithIntId(liveMeeting.users2x, userId)
apiCall = "join"
(redirectParams, redirectToHtml5Params) = BreakoutRoomsUtil.joinParams(user.name, userId + "-" + roomSequence, true,
externalMeetingId, liveMeeting.props.password.moderatorPass)
(redirectParams, redirectToHtml5Params) = BreakoutRoomsUtil.joinParams(
user.name,
userId + "-" + roomSequence,
true,
externalMeetingId,
user.avatar,
user.role,
liveMeeting.props.password.moderatorPass
)
// We generate a first url with redirect -> true
redirectBaseString = BreakoutRoomsUtil.createBaseString(redirectParams)
redirectJoinURL = BreakoutRoomsUtil.createJoinURL(bbbWebAPI, apiCall, redirectBaseString,

View File

@ -99,6 +99,7 @@ trait CreateBreakoutRoomsCmdMsgHdlr extends RightsManagementTrait {
breakout.captureSlides,
breakout.captureNotesFilename,
breakout.captureSlidesFilename,
pluginProp = liveMeeting.props.pluginProp,
)
val event = buildCreateBreakoutRoomSysCmdMsg(liveMeeting.props.meetingProp.intId, roomDetail)

View File

@ -52,9 +52,9 @@ trait DeleteGroupChatMessageReactionReqMsgHdlr extends HandlerHelpers {
val userIsAParticipant = groupChat.users.exists(u => u.id == user.intId)
if ((chatIsPrivate && userIsAParticipant) || !chatIsPrivate) {
val event = buildGroupChatMessageReactionDeletedEvtMsg(liveMeeting.props.meetingProp.intId, msg.header.userId, msg.body.chatId, gcMessage.id, msg.body.reactionEmoji)
val event = buildGroupChatMessageReactionDeletedEvtMsg(liveMeeting.props.meetingProp.intId, msg.header.userId, msg.body.chatId, gcMessage.id, msg.body.reactionEmoji, msg.body.reactionEmojiId)
bus.outGW.send(event)
ChatMessageReactionDAO.delete(liveMeeting.props.meetingProp.intId, gcMessage.id, msg.header.userId, msg.body.reactionEmoji)
ChatMessageReactionDAO.delete(liveMeeting.props.meetingProp.intId, gcMessage.id, msg.header.userId, msg.body.reactionEmoji, msg.body.reactionEmojiId)
} else {
val reason = "User isn't a participant of the chat"
PermissionCheck.ejectUserForFailedPermission(msg.header.meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)

View File

@ -27,7 +27,7 @@ trait DeleteGroupChatMessageReqMsgHdlr extends HandlerHelpers {
chatLockedForUser = true
}
val userIsModerator = user.role != Roles.MODERATOR_ROLE
val userIsModerator = user.role == Roles.MODERATOR_ROLE
if (!userIsModerator && user.locked) {
val permissions = MeetingStatus2x.getPermissions(liveMeeting.status)

View File

@ -27,7 +27,7 @@ trait EditGroupChatMessageReqMsgHdlr extends HandlerHelpers {
chatLockedForUser = true
}
val userIsModerator = user.role != Roles.MODERATOR_ROLE
val userIsModerator = user.role == Roles.MODERATOR_ROLE
if (!userIsModerator && user.locked) {
val permissions = MeetingStatus2x.getPermissions(liveMeeting.status)
@ -45,7 +45,7 @@ trait EditGroupChatMessageReqMsgHdlr extends HandlerHelpers {
}
}
if (!chatDisabled && editChatMessageDisabled && !(applyPermissionCheck && chatLocked) && !chatLockedForUser) {
if (!chatDisabled && !editChatMessageDisabled && !(applyPermissionCheck && chatLocked) && !chatLockedForUser) {
for {
gcMessage <- groupChat.msgs.find(gcm => gcm.id == msg.body.messageId)
} yield {

View File

@ -26,7 +26,7 @@ trait SendGroupChatMessageReactionReqMsgHdlr extends HandlerHelpers {
chatLockedForUser = true
}
val userIsModerator = user.role != Roles.MODERATOR_ROLE
val userIsModerator = user.role == Roles.MODERATOR_ROLE
if (!userIsModerator && user.locked) {
val permissions = MeetingStatus2x.getPermissions(liveMeeting.status)
@ -52,9 +52,9 @@ trait SendGroupChatMessageReactionReqMsgHdlr extends HandlerHelpers {
val userIsAParticipant = groupChat.users.exists(u => u.id == user.intId)
if ((chatIsPrivate && userIsAParticipant) || !chatIsPrivate) {
val event = buildGroupChatMessageReactionSentEvtMsg(liveMeeting.props.meetingProp.intId, msg.header.userId, msg.body.chatId, gcMessage.id, msg.body.reactionEmoji)
val event = buildGroupChatMessageReactionSentEvtMsg(liveMeeting.props.meetingProp.intId, msg.header.userId, msg.body.chatId, gcMessage.id, msg.body.reactionEmoji, msg.body.reactionEmojiId)
bus.outGW.send(event)
ChatMessageReactionDAO.insert(liveMeeting.props.meetingProp.intId, gcMessage.id, msg.header.userId, msg.body.reactionEmoji)
ChatMessageReactionDAO.insert(liveMeeting.props.meetingProp.intId, gcMessage.id, msg.header.userId, msg.body.reactionEmoji, msg.body.reactionEmojiId)
} else {
val reason = "User isn't a participant of the chat"
PermissionCheck.ejectUserForFailedPermission(msg.header.meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)

View File

@ -1,62 +1,29 @@
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}'.")
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 {
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)) {
println(s"No permission to delete in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.")
} else {
PluginDataChannelEntryDAO.delete(
meetingId,
msg.body.pluginName,
msg.body.channelName,
msg.body.subChannelName,
msg.body.entryId
)
}
PluginDataChannelEntryDAO.delete(
meetingId,
msg.body.pluginName,
msg.body.channelName,
msg.body.subChannelName,
msg.body.entryId
)
}
}
})
}
}

View File

@ -1,55 +1,30 @@
package org.bigbluebutton.core.apps.plugin
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}'.")
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 {
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)) {
println(s"No permission to write in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.")
} else {
PluginDataChannelEntryDAO.insert(
meetingId,
msg.body.pluginName,
msg.body.channelName,
msg.body.subChannelName,
msg.header.userId,
msg.body.payloadJson,
msg.body.toRoles,
msg.body.toUserIds
)
}
PluginDataChannelEntryDAO.insert(
meetingId,
msg.body.pluginName,
msg.body.channelName,
msg.body.subChannelName,
msg.header.userId,
msg.body.payloadJson,
msg.body.toRoles,
msg.body.toUserIds
)
}
}
})
}
}

View File

@ -1,63 +1,31 @@
package org.bigbluebutton.core.apps.plugin
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)
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 (!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}'.")
if (!hasPermission.contains(true)) {
println(s"No permission to write in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.")
} 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)) {
println(s"No permission to write in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.")
} else {
PluginDataChannelEntryDAO.replace(
msg.header.meetingId,
msg.body.pluginName,
msg.body.channelName,
msg.body.subChannelName,
msg.body.entryId,
JsonUtils.mapToJson(msg.body.payloadJson),
)
}
PluginDataChannelEntryDAO.replace(
msg.header.meetingId,
msg.body.pluginName,
msg.body.channelName,
msg.body.subChannelName,
msg.body.entryId,
JsonUtils.mapToJson(msg.body.payloadJson),
)
}
}
})
}
}

View File

@ -1,51 +1,27 @@
package org.bigbluebutton.core.apps.plugin
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
dataChannelCheckingLogic(liveMeeting, msg.header.userId, msg.body.pluginName, msg.body.channelName, (user, dc, meetingId) => {
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}'.")
if (!hasPermission.contains(true)) {
println(s"No permission to delete (reset) in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.")
} 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)) {
println(s"No permission to delete (reset) in plugin: '${msg.body.pluginName}', data channel: '${msg.body.channelName}'.")
} else {
PluginDataChannelEntryDAO.reset(
meetingId,
msg.body.pluginName,
msg.body.channelName,
msg.body.subChannelName
)
}
PluginDataChannelEntryDAO.reset(
meetingId,
msg.body.pluginName,
msg.body.channelName,
msg.body.subChannelName
)
}
}
})
}
}

View File

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

View File

@ -26,7 +26,7 @@ trait EjectUserFromMeetingCmdMsgHdlr extends RightsManagementTrait {
PermissionCheck.VIEWER_LEVEL,
liveMeeting.users2x,
msg.header.userId
) || liveMeeting.props.meetingProp.isBreakout) {
)) {
val reason = "No permission to eject user from meeting."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)

View File

@ -59,8 +59,8 @@ trait RegisterUserReqMsgHdlr {
val regUser = RegisteredUsers.create(liveMeeting.props.meetingProp.intId, msg.body.intUserId, msg.body.extUserId,
msg.body.name, msg.body.role, msg.body.authToken, Vector(msg.body.sessionToken),
msg.body.avatarURL, msg.body.webcamBackgroundURL, ColorPicker.nextColor(liveMeeting.props.meetingProp.intId), msg.body.guest, msg.body.authed,
guestStatus, msg.body.excludeFromDashboard, msg.body.enforceLayout, msg.body.userMetadata, false)
msg.body.avatarURL, msg.body.webcamBackgroundURL, ColorPicker.nextColor(liveMeeting.props.meetingProp.intId), msg.body.bot,
msg.body.guest, msg.body.authed, guestStatus, msg.body.excludeFromDashboard, msg.body.enforceLayout, msg.body.userMetadata, false)
checkUserConcurrentAccesses(regUser)
RegisteredUsers.add(liveMeeting.registeredUsers, regUser, liveMeeting.props.meetingProp.intId)

View File

@ -9,6 +9,7 @@ import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
import org.bigbluebutton.core2.message.senders.MsgBuilder
import org.bigbluebutton.core.apps.voice.VoiceApp
import org.bigbluebutton.core.db.{ MeetingRecordingDAO, NotificationDAO }
import org.bigbluebutton.core.models.{ Users2x }
trait SetRecordingStatusCmdMsgHdlr extends RightsManagementTrait {
this: UsersApp =>
@ -29,9 +30,19 @@ trait SetRecordingStatusCmdMsgHdlr extends RightsManagementTrait {
BbbCommonEnvCoreMsg(envelope, event)
}
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
// Retrieve custom record permission from metadata
val customRecordPermission: Option[Boolean] = Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId).flatMap { user =>
user.userMetadata.get("bbb_record_permission").map(_.toBoolean)
}
// Determine final permission using metadata or fallback
val hasPermission: Boolean = customRecordPermission.getOrElse {
!permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)
}
if (!hasPermission) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to clear chat in meeting."
val reason = "No permission to set recording status in meeting."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)
state
} else {

View File

@ -83,7 +83,7 @@ trait UserJoinMeetingReqMsgHdlr extends HandlerHelpers {
if (maxParticipants > 0 && //0 = no limit
RegisteredUsers.numUniqueJoinedUsers(liveMeeting.registeredUsers) >= maxParticipants &&
!userHasJoinedAlready) {
!userHasJoinedAlready && !regUser.bot) {
Left(("The maximum number of participants allowed for this meeting has been reached.", EjectReasonCode.MAX_PARTICIPANTS))
} else {
Right(())

View File

@ -33,7 +33,7 @@ trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration {
def registerUserInRegisteredUsers() = {
val regUser = RegisteredUsers.create(liveMeeting.props.meetingProp.intId, msg.body.intId, msg.body.voiceUserId,
msg.body.callerIdName, Roles.VIEWER_ROLE, msg.body.intId, Vector(""), "", "", userColor,
msg.body.callerIdName, Roles.VIEWER_ROLE, msg.body.intId, Vector(""), "", "", userColor, false,
true, true, GuestStatus.WAIT, true, "", Map(), false)
RegisteredUsers.add(liveMeeting.registeredUsers, regUser, liveMeeting.props.meetingProp.intId)
}
@ -45,6 +45,7 @@ trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration {
meetingId = liveMeeting.props.meetingProp.intId,
name = msg.body.callerIdName,
role = Roles.VIEWER_ROLE,
bot = false,
guest = true,
authed = true,
guestStatus = GuestStatus.WAIT,

View File

@ -4,11 +4,12 @@ import slick.jdbc.PostgresProfile.api._
import slick.lifted.{ ProvenShape }
case class ChatMessageReactionDbModel(
meetingId: String,
messageId: String,
userId: String,
reactionEmoji: String,
createdAt: java.sql.Timestamp
meetingId: String,
messageId: String,
userId: String,
reactionEmoji: String,
reactionEmojiId: String,
createdAt: java.sql.Timestamp
)
class ChatMessageReactionDbTableDef(tag: Tag) extends Table[ChatMessageReactionDbModel](tag, "chat_message_reaction") {
@ -16,13 +17,14 @@ class ChatMessageReactionDbTableDef(tag: Tag) extends Table[ChatMessageReactionD
val messageId = column[String]("messageId", O.PrimaryKey)
val userId = column[String]("userId", O.PrimaryKey)
val reactionEmoji = column[String]("reactionEmoji", O.PrimaryKey)
val reactionEmojiId = column[String]("reactionEmojiId")
val createdAt = column[java.sql.Timestamp]("createdAt")
override def * : ProvenShape[ChatMessageReactionDbModel] = (meetingId, messageId, userId, reactionEmoji, createdAt) <> (ChatMessageReactionDbModel.tupled, ChatMessageReactionDbModel.unapply)
override def * : ProvenShape[ChatMessageReactionDbModel] = (meetingId, messageId, userId, reactionEmoji, reactionEmojiId, createdAt) <> (ChatMessageReactionDbModel.tupled, ChatMessageReactionDbModel.unapply)
}
object ChatMessageReactionDAO {
def insert(meetingId: String, messageId: String, userId: String, reactionEmoji: String) = {
def insert(meetingId: String, messageId: String, userId: String, reactionEmoji: String, reactionEmojiId: String) = {
DatabaseConnection.enqueue(
TableQuery[ChatMessageReactionDbTableDef].forceInsert(
ChatMessageReactionDbModel(
@ -30,19 +32,21 @@ object ChatMessageReactionDAO {
messageId = messageId,
userId = userId,
reactionEmoji = reactionEmoji,
reactionEmojiId = reactionEmojiId,
createdAt = new java.sql.Timestamp(System.currentTimeMillis())
)
)
)
}
def delete(meetingId: String, messageId: String, userId: String, reactionEmoji: String) = {
def delete(meetingId: String, messageId: String, userId: String, reactionEmoji: String, reactionEmojiId: String) = {
DatabaseConnection.enqueue(
TableQuery[ChatMessageReactionDbTableDef]
.filter(_.meetingId === meetingId)
.filter(_.messageId === messageId)
.filter(_.userId === userId)
.filter(_.reactionEmoji === reactionEmoji)
.filter(_.reactionEmojiId === reactionEmojiId)
.delete
)
}

View File

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

View File

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

View File

@ -18,6 +18,7 @@ case class UserDbModel(
joinErrorCode: Option[String],
banned: Boolean = false,
loggedOut: Boolean = false,
bot: Boolean,
guest: Boolean,
guestStatus: String,
registeredOn: Long,
@ -30,7 +31,7 @@ case class UserDbModel(
class UserDbTableDef(tag: Tag) extends Table[UserDbModel](tag, None, "user") {
override def * = (
meetingId,userId,extId,name,role,avatar,webcamBackground,color, authToken, authed,joined,joinErrorCode,
joinErrorMessage, banned,loggedOut,guest,guestStatus,registeredOn,excludeFromDashboard, enforceLayout) <> (UserDbModel.tupled, UserDbModel.unapply)
joinErrorMessage, banned,loggedOut,bot, guest,guestStatus,registeredOn,excludeFromDashboard, enforceLayout) <> (UserDbModel.tupled, UserDbModel.unapply)
val meetingId = column[String]("meetingId", O.PrimaryKey)
val userId = column[String]("userId", O.PrimaryKey)
val extId = column[String]("extId")
@ -46,6 +47,7 @@ class UserDbTableDef(tag: Tag) extends Table[UserDbModel](tag, None, "user") {
val joinErrorMessage = column[Option[String]]("joinErrorMessage")
val banned = column[Boolean]("banned")
val loggedOut = column[Boolean]("loggedOut")
val bot = column[Boolean]("bot")
val guest = column[Boolean]("guest")
val guestStatus = column[String]("guestStatus")
val registeredOn = column[Long]("registeredOn")
@ -73,6 +75,7 @@ object UserDAO {
joinErrorMessage = None,
banned = regUser.banned,
loggedOut = regUser.loggedOut,
bot = regUser.bot,
guest = regUser.guest,
guestStatus = regUser.guestStatus,
registeredOn = regUser.registeredOn,

View File

@ -0,0 +1,103 @@
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 replaceRelativeJavascriptEntrypoint(plugin: Plugin): Plugin = {
val jsEntrypoint = plugin.manifest.content.javascriptEntrypointUrl
if (jsEntrypoint.startsWith("http://") || jsEntrypoint.startsWith("https://")) {
plugin
} else {
val baseUrl = plugin.manifest.url.substring(0, plugin.manifest.url.lastIndexOf('/') + 1)
val absoluteJavascriptEntrypoint = baseUrl + jsEntrypoint
val newPluginManifestContent = plugin.manifest.content.copy(javascriptEntrypointUrl = absoluteJavascriptEntrypoint)
val newPluginManifest = plugin.manifest.copy(content = newPluginManifestContent)
plugin.copy(manifest = newPluginManifest)
}
}
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])
val pluginObjectWithAbsoluteJavascriptEntrypoint = replaceRelativeJavascriptEntrypoint(pluginObject)
pluginsMap = pluginsMap + (pluginName -> pluginObjectWithAbsoluteJavascriptEntrypoint)
} catch {
case err @ (_: JsonProcessingException | _: JsonMappingException) => println("Error while processing plugin " +
pluginName + ": ", err)
}
}
instance.plugins = pluginsMap
instance
}
def persistPluginsForClient(instance: PluginModel, meetingId: String): Unit = {
instance.plugins.foreach { case (_, plugin) =>
PluginDAO.insert(meetingId, plugin.manifest.content.name, plugin.manifest.content.javascriptEntrypointUrl,
plugin.manifest.content.javascriptEntrypointIntegrity.getOrElse(""))
}
}
}
class PluginModel {
private var plugins: Map[String, Plugin] = Map()
}

View File

@ -6,8 +6,8 @@ import org.bigbluebutton.core.domain.BreakoutRoom2x
object RegisteredUsers {
def create(meetingId: String, userId: String, extId: String, name: String, roles: String,
authToken: String, sessionToken: Vector[String], avatar: String, webcamBackground: String, color: String, guest: Boolean, authenticated: Boolean,
guestStatus: String, excludeFromDashboard: Boolean, enforceLayout: String,
authToken: String, sessionToken: Vector[String], avatar: String, webcamBackground: String, color: String, bot: Boolean,
guest: Boolean, authenticated: Boolean, guestStatus: String, excludeFromDashboard: Boolean, enforceLayout: String,
userMetadata: Map[String, String], loggedOut: Boolean): RegisteredUser = {
new RegisteredUser(
userId,
@ -20,6 +20,7 @@ object RegisteredUsers {
avatar,
webcamBackground,
color,
bot,
guest,
authenticated,
guestStatus,
@ -256,6 +257,7 @@ case class RegisteredUser(
avatarURL: String,
webcamBackgroundURL: String,
color: String,
bot: Boolean,
guest: Boolean,
authed: Boolean,
guestStatus: String,

View File

@ -67,7 +67,7 @@ object Users2x {
}
def numUsers(users: Users2x): Int = {
users.toVector.length
users.toVector.filter(u => !u.bot).length
}
def numActiveModerators(users: Users2x): Int = {
@ -432,6 +432,7 @@ case class UserState(
meetingId: String,
name: String,
role: String,
bot: Boolean,
guest: Boolean,
pin: Boolean,
mobile: Boolean,

View File

@ -48,6 +48,7 @@ trait HandlerHelpers extends SystemConfiguration {
meetingId = regUser.meetingId,
name = regUser.name,
role = regUser.role,
bot = regUser.bot,
guest = regUser.guest,
authed = regUser.authed,
guestStatus = regUser.guestStatus,
@ -327,20 +328,20 @@ trait HandlerHelpers extends SystemConfiguration {
BbbCommonEnvCoreMsg(envelope, event)
}
def buildGroupChatMessageReactionSentEvtMsg(meetingId: String, userId: String, chatId: String, messageId: String, reactionEmoji: String): BbbCommonEnvCoreMsg = {
def buildGroupChatMessageReactionSentEvtMsg(meetingId: String, userId: String, chatId: String, messageId: String, reactionEmoji: String, reactionEmojiId: String): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId)
val envelope = BbbCoreEnvelope(GroupChatMessageReactionSentEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(GroupChatMessageReactionSentEvtMsg.NAME, meetingId, userId)
val body = GroupChatMessageReactionSentEvtMsgBody(chatId, messageId, reactionEmoji)
val body = GroupChatMessageReactionSentEvtMsgBody(chatId, messageId, reactionEmoji, reactionEmojiId)
val event = GroupChatMessageReactionSentEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
def buildGroupChatMessageReactionDeletedEvtMsg(meetingId: String, userId: String, chatId: String, messageId: String, reactionEmoji: String): BbbCommonEnvCoreMsg = {
def buildGroupChatMessageReactionDeletedEvtMsg(meetingId: String, userId: String, chatId: String, messageId: String, reactionEmoji: String, reactionEmojiId: String): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId)
val envelope = BbbCoreEnvelope(GroupChatMessageReactionDeletedEvtMsg.NAME, routing)
val header = BbbClientMsgHeader(GroupChatMessageReactionDeletedEvtMsg.NAME, meetingId, userId)
val body = GroupChatMessageReactionDeletedEvtMsgBody(chatId, messageId, reactionEmoji)
val body = GroupChatMessageReactionDeletedEvtMsgBody(chatId, messageId, reactionEmoji, reactionEmojiId)
val event = GroupChatMessageReactionDeletedEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}

View File

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

View File

@ -172,7 +172,7 @@ class MeetingActor(
outGW.send(msgEvent)
//Insert meeting into the database
MeetingDAO.insert(liveMeeting.props, liveMeeting.clientSettings)
MeetingDAO.insert(liveMeeting.props, liveMeeting.clientSettings, liveMeeting.plugins)
// Create a default public group chat
state = groupChatApp.handleCreateDefaultPublicGroupChat(state, liveMeeting, msgBus)
@ -1134,7 +1134,7 @@ class MeetingActor(
val hasActivityAfterWarning = u.lastInactivityInspect < u.lastActivityTime
val hasActivityRecently = (lastUsersInactivityInspection - expiryTracker.userInactivityThresholdInMs) < u.lastActivityTime
if (hasActivityAfterWarning && !hasActivityRecently) {
if (hasActivityAfterWarning && !hasActivityRecently && !u.bot) {
log.info("User has been inactive for " + TimeUnit.MILLISECONDS.toMinutes(expiryTracker.userInactivityThresholdInMs) + " minutes. Sending inactivity warning. meetingId=" + props.meetingProp.intId + " userId=" + u.intId + " user=" + u)
val secsToDisconnect = TimeUnit.MILLISECONDS.toSeconds(expiryTracker.userActivitySignResponseDelayInMs);

View File

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

View File

@ -9,7 +9,7 @@ object UserJoinedMeetingEvtMsgBuilder {
val envelope = BbbCoreEnvelope(UserJoinedMeetingEvtMsg.NAME, routing)
val body = UserJoinedMeetingEvtMsgBody(intId = userState.intId, extId = userState.extId, name = userState.name,
role = userState.role, guest = userState.guest, authed = userState.authed,
role = userState.role, bot = userState.bot, guest = userState.guest, authed = userState.authed,
guestStatus = userState.guestStatus,
reactionEmoji = userState.reactionEmoji,
raiseHand = userState.raiseHand,

View File

@ -9,21 +9,21 @@ import org.bigbluebutton.core.running.LiveMeeting
trait FakeTestData {
def createFakeUsers(liveMeeting: LiveMeeting): Unit = {
val mod1 = createUserVoiceAndCam(liveMeeting, Roles.MODERATOR_ROLE, false, false, CallingWith.WEBRTC, muted = false,
val mod1 = createUserVoiceAndCam(liveMeeting, Roles.MODERATOR_ROLE, bot = false, false, false, CallingWith.WEBRTC, muted = false,
talking = true, listenOnly = false)
Users2x.add(liveMeeting.users2x, mod1)
val mod2 = createUserVoiceAndCam(liveMeeting, Roles.MODERATOR_ROLE, guest = false, authed = true, CallingWith.WEBRTC, muted = false,
val mod2 = createUserVoiceAndCam(liveMeeting, Roles.MODERATOR_ROLE, bot = false, guest = false, authed = true, CallingWith.WEBRTC, muted = false,
talking = false, listenOnly = false)
Users2x.add(liveMeeting.users2x, mod2)
val guest1 = createUserVoiceAndCam(liveMeeting, Roles.VIEWER_ROLE, guest = true, authed = true, CallingWith.WEBRTC, muted = false,
val guest1 = createUserVoiceAndCam(liveMeeting, Roles.VIEWER_ROLE, bot = false, guest = true, authed = true, CallingWith.WEBRTC, muted = false,
talking = false, listenOnly = false)
Users2x.add(liveMeeting.users2x, guest1)
val guestWait1 = GuestWaiting(guest1.intId, guest1.name, guest1.role, guest1.guest, "", "", "#ff6242", guest1.authed, System.currentTimeMillis())
GuestsWaiting.add(liveMeeting.guestsWaiting, guestWait1)
val guest2 = createUserVoiceAndCam(liveMeeting, Roles.VIEWER_ROLE, guest = true, authed = true, CallingWith.FLASH, muted = false,
val guest2 = createUserVoiceAndCam(liveMeeting, Roles.VIEWER_ROLE, bot = false, guest = true, authed = true, CallingWith.FLASH, muted = false,
talking = false, listenOnly = false)
Users2x.add(liveMeeting.users2x, guest2)
val guestWait2 = GuestWaiting(guest2.intId, guest2.name, guest2.role, guest2.guest, "", "", "#ff6242", guest2.authed, System.currentTimeMillis())
@ -44,16 +44,16 @@ trait FakeTestData {
VoiceUsers.add(liveMeeting.voiceUsers, vu5)
for (i <- 1 to 50) {
val guser = createUserVoiceAndCam(liveMeeting, Roles.MODERATOR_ROLE, guest = false, authed = true, CallingWith.WEBRTC, muted = false,
val guser = createUserVoiceAndCam(liveMeeting, Roles.MODERATOR_ROLE, bot = false, guest = false, authed = true, CallingWith.WEBRTC, muted = false,
talking = false, listenOnly = false)
Users2x.add(liveMeeting.users2x, guser)
}
}
def createUserVoiceAndCam(liveMeeting: LiveMeeting, role: String, guest: Boolean, authed: Boolean, callingWith: String,
def createUserVoiceAndCam(liveMeeting: LiveMeeting, role: String, bot: Boolean, guest: Boolean, authed: Boolean, callingWith: String,
muted: Boolean, talking: Boolean, listenOnly: Boolean): UserState = {
val ruser1 = FakeUserGenerator.createFakeRegisteredUser(liveMeeting.registeredUsers, Roles.MODERATOR_ROLE, true, false, liveMeeting.props.meetingProp.intId)
val ruser1 = FakeUserGenerator.createFakeRegisteredUser(liveMeeting.registeredUsers, Roles.MODERATOR_ROLE, bot = false, true, false, liveMeeting.props.meetingProp.intId)
val vuser1 = FakeUserGenerator.createFakeVoiceUser(ruser1, "webrtc", muted = false, talking = true, listenOnly = false)
VoiceUsers.add(liveMeeting.voiceUsers, vuser1)
@ -70,7 +70,7 @@ trait FakeTestData {
def createFakeUser(liveMeeting: LiveMeeting, regUser: RegisteredUser): UserState = {
UserState(intId = regUser.id, extId = regUser.externId, meetingId = regUser.meetingId,
name = regUser.name, role = regUser.role, pin = false,
mobile = false, guest = regUser.guest, authed = regUser.authed, guestStatus = regUser.guestStatus,
mobile = false, bot = regUser.bot, guest = regUser.guest, authed = regUser.authed, guestStatus = regUser.guestStatus,
reactionEmoji = "none", raiseHand = false, away = false, locked = false, presenter = false,
avatar = regUser.avatarURL, webcamBackground = regUser.webcamBackgroundURL, color = "#ff6242", clientType = "unknown", userLeftFlag = UserLeftFlag(false, 0))
}

View File

@ -45,7 +45,7 @@ object FakeUserGenerator {
private def getRandomElement(list: Seq[String], random: Random): String = list(random.nextInt(list.length))
def createFakeRegisteredUser(users: RegisteredUsers, role: String, guest: Boolean, authed: Boolean, meetingId: String): RegisteredUser = {
def createFakeRegisteredUser(users: RegisteredUsers, role: String, bot: Boolean, guest: Boolean, authed: Boolean, meetingId: String): RegisteredUser = {
val name = getRandomElement(firstNames, random) + " " + getRandomElement(lastNames, random)
val id = "w_" + RandomStringGenerator.randomAlphanumericString(16)
val extId = RandomStringGenerator.randomAlphanumericString(16)
@ -58,7 +58,8 @@ object FakeUserGenerator {
val color = "#ff6242"
val ru = RegisteredUsers.create(meetingId, userId = id, extId, name, role,
authToken, Vector(sessionToken), avatarURL, webcamBackgroundURL, color, guest, authed, guestStatus = GuestStatus.ALLOW, false, "", Map(), false)
authToken, Vector(sessionToken), avatarURL, webcamBackgroundURL, color, bot,
guest, authed, guestStatus = GuestStatus.ALLOW, false, "", Map(), false)
RegisteredUsers.add(users, ru, meetingId)
ru
}

View File

@ -21,6 +21,7 @@ case class Meeting(
extId: String,
name: String,
downloadSessionDataEnabled: Boolean,
other: Map[String, String] = Map(),
users: Map[String, User] = Map(),
genericDataTitles: Vector[String],
polls: Map[String, Poll] = Map(),
@ -50,9 +51,13 @@ case class User(
case class UserId(
intId: String,
sessions: Vector[UserSession] = Vector(UserSession()),
userLeftFlag: Boolean = false,
)
case class UserSession(
registeredOn: Long = System.currentTimeMillis(),
leftOn: Long = 0,
userLeftFlag: Boolean = false,
)
case class Poll(
@ -286,12 +291,12 @@ class LearningDashboardActor(
}
private def findUserByIntId(meeting: Meeting, intId: String): Option[User] = {
meeting.users.values.find(u => u.currentIntId == intId || (u.currentIntId == null && u.intIds.exists(uId => uId._2.intId == intId && uId._2.leftOn == 0)))
meeting.users.values.find(u => u.currentIntId == intId || (u.currentIntId == null && u.intIds.exists(uId => uId._2.intId == intId && uId._2.sessions.last.leftOn == 0)))
}
private def findUserByExtId(meeting: Meeting, extId: String, filterUserLeft: Boolean = false): Option[User] = {
meeting.users.values.find(u => {
u.extId == extId && (filterUserLeft == false || !u.intIds.exists(uId => uId._2.leftOn == 0 && uId._2.userLeftFlag == false))
u.extId == extId && (filterUserLeft == false || !u.intIds.exists(uId => uId._2.sessions.last.leftOn == 0 && uId._2.userLeftFlag == false))
})
}
@ -309,14 +314,21 @@ class LearningDashboardActor(
for {
userId <- user.intIds.get(msg.body.userId)
} yield {
val updatedUser = user.copy(currentIntId = userId.intId, intIds = user.intIds + (userId.intId -> userId.copy(leftOn = 0, userLeftFlag = false)))
val updatedUserId = userId.copy(
sessions = userId.sessions.init :+ userId.sessions.last.copy(leftOn = 0),
userLeftFlag = false
)
val updatedUser = user.copy(
currentIntId = userId.intId,
intIds = user.intIds + (userId.intId -> updatedUserId)
)
val updatedMeeting = meeting.copy(users = meeting.users + (updatedUser.userKey -> updatedUser))
meetings += (updatedMeeting.intId -> updatedMeeting)
}
} else {
val userLeftFlagged = meeting.users.values.filter(u => u.intIds.exists(uId => {
uId._2.intId == msg.body.userId && uId._2.userLeftFlag == true && uId._2.leftOn == 0
uId._2.intId == msg.body.userId && uId._2.userLeftFlag == true && uId._2.sessions.last.leftOn == 0
}))
//Flagged user must be reactivated, once UserJoinedMeetingEvtMsg won't be sent
@ -353,12 +365,17 @@ class LearningDashboardActor(
private def handleUserLeftMeetingEvtMsg(msg: UserLeftMeetingEvtMsg): Unit = {
for {
meeting <- meetings.values.find(m => m.intId == msg.header.meetingId)
user <- meeting.users.values.find(u => u.intIds.exists(uId => uId._2.intId == msg.body.intId && uId._2.leftOn == 0))
user <- meeting.users.values.find(u => u.intIds.exists(uId => uId._2.intId == msg.body.intId && uId._2.sessions.last.leftOn == 0))
userId <- user.intIds.get(msg.body.intId)
} yield {
val updatedSessions = userId.sessions.init :+ userId.sessions.last.copy(leftOn = System.currentTimeMillis())
val updatedUserId = userId.copy(
userLeftFlag = true,
sessions = updatedSessions
)
val updatedUser = user.copy(
currentIntId = if(user.currentIntId == userId.intId) null else user.currentIntId,
intIds = user.intIds + (userId.intId -> userId.copy(leftOn = System.currentTimeMillis()))
intIds = user.intIds + (userId.intId -> updatedUserId)
)
val updatedMeeting = meeting.copy(users = meeting.users + (updatedUser.userKey -> updatedUser))
@ -478,7 +495,11 @@ class LearningDashboardActor(
endUserTalk(meeting, user)
if(user.isDialIn) {
val updatedUser = user.copy(intIds = user.intIds + (userId.intId -> userId.copy(leftOn = System.currentTimeMillis())))
val updatedSessions = userId.sessions.init :+ userId.sessions.last.copy(leftOn = System.currentTimeMillis())
val updatedUserId = userId.copy(
sessions = updatedSessions
)
val updatedUser = user.copy(intIds = user.intIds + (userId.intId -> updatedUserId))
val updatedMeeting = meeting.copy(users = meeting.users + (updatedUser.userKey -> updatedUser))
meetings += (updatedMeeting.intId -> updatedMeeting)
@ -606,7 +627,11 @@ class LearningDashboardActor(
msg.body.props.meetingProp.extId,
msg.body.props.meetingProp.name,
downloadSessionDataEnabled = !msg.body.props.meetingProp.disabledFeatures.contains("learningDashboardDownloadSessionData"),
genericDataTitles = Vector()
genericDataTitles = Vector(),
other = Map(
"learning-dashboard-learn-more-link" -> msg.body.props.metadataProp.metadata.get("learning-dashboard-learn-more-link").getOrElse(""),
"learning-dashboard-feedback-link" -> msg.body.props.metadataProp.metadata.get("learning-dashboard-feedback-link").getOrElse("")
),
)
meetings += (newMeeting.intId -> newMeeting)
@ -653,9 +678,15 @@ class LearningDashboardActor(
user.copy(
currentIntId = null,
intIds = user.intIds.map(uId => {
if(uId._2.leftOn > 0) (uId._1 -> uId._2)
if(uId._2.sessions.last.leftOn > 0) (uId._1 -> uId._2)
else if(forceFlaggedIdsToLeft == false && uId._2.userLeftFlag == true) (uId._1 -> uId._2)
else (uId._1 -> uId._2.copy(leftOn = endedOn))
else {
val updatedSessions = uId._2.sessions.init :+ uId._2.sessions.last.copy(leftOn = endedOn)
val updatedUserId = uId._2.copy(
sessions = updatedSessions
)
(uId._1 -> updatedUserId)
}
}),
talk = user.talk.copy(
totalTime = user.talk.totalTime + (if (user.talk.lastTalkStartedOn > 0) (endedOn - user.talk.lastTalkStartedOn) else 0),
@ -691,20 +722,25 @@ class LearningDashboardActor(
)
, currentTime, false)
val currentUserId = user.intIds.get(intId).getOrElse(UserId(intId, sessions = Vector()))
meetings += (meeting.intId -> meeting.copy(
//Set leftOn to same intId in past user records
users = meeting.users.map(u => {
(u._1 -> u._2.copy(
intIds = u._2.intIds.map(uId => {
(uId._1 -> {
if (uId._2.intId == intId && uId._2.leftOn == 0) uId._2.copy(leftOn = currentTime)
if (uId._2.intId == intId && uId._2.sessions.last.leftOn == 0) {
val updatedSessions = uId._2.sessions.init :+ uId._2.sessions.last.copy(leftOn = currentTime)
uId._2.copy(sessions = updatedSessions)
}
else uId._2
})
})))
}) + (user.userKey -> user.copy(
currentIntId = intId,
intIds = user.intIds + (intId -> user.intIds.get(intId).getOrElse(UserId(intId, currentTime)).copy(
leftOn = 0,
intIds = user.intIds + (intId -> currentUserId.copy(
sessions = currentUserId.sessions :+ UserSession(currentTime),
userLeftFlag = false
))
))

View File

@ -6,7 +6,7 @@ import org.bigbluebutton.core.util.RandomStringGenerator
object TestDataGen {
def createRegisteredUser(meetingId: String, users: RegisteredUsers, name: String, role: String,
guest: Boolean, authed: Boolean, waitForApproval: Boolean): RegisteredUser = {
bot: Boolean, guest: Boolean, authed: Boolean, waitForApproval: Boolean): RegisteredUser = {
val id = "w_" + RandomStringGenerator.randomAlphanumericString(16)
val extId = RandomStringGenerator.randomAlphanumericString(16)
val authToken = RandomStringGenerator.randomAlphanumericString(16)
@ -18,7 +18,8 @@ object TestDataGen {
val color = "#ff6242"
val ru = RegisteredUsers.create(meetingId, userId = id, extId, name, role,
authToken, Vector(sessionToken), avatarURL, webcamBackgroundURL, color, guest, authed, GuestStatus.ALLOW, false, "", Map(), false)
authToken, Vector(sessionToken), avatarURL, webcamBackgroundURL, color, bot,
guest, authed, GuestStatus.ALLOW, false, "", Map(), false)
RegisteredUsers.add(users, ru, meetingId = "test")
ru
@ -76,7 +77,7 @@ object TestDataGen {
def createUserFor(liveMeeting: LiveMeeting, regUser: RegisteredUser, presenter: Boolean): UserState = {
val u = UserState(intId = regUser.id, extId = regUser.externId, meetingId = regUser.meetingId, name = regUser.name,
role = regUser.role, guest = regUser.guest, authed = regUser.authed, guestStatus = regUser.guestStatus,
role = regUser.role,bot = regUser.bot, guest = regUser.guest, authed = regUser.authed, guestStatus = regUser.guestStatus,
reactionEmoji = "none", raiseHand = false, away = false, pin = false, mobile = false,
locked = false, presenter = false, avatar = regUser.avatarURL, regUser.webcamBackgroundURL, color = "#ff6242",
clientType = "unknown", userLeftFlag = UserLeftFlag(false, 0))

View File

@ -1,5 +1,7 @@
package org.bigbluebutton.common2.domain
import java.util
case class DurationProps(duration: Int, createdTime: Long, createdDate: String,
meetingExpireIfNoUserJoinedInMinutes: Int, meetingExpireWhenLastUserLeftInMinutes: Int,
userInactivityInspectTimerInMinutes: Int, userInactivityThresholdInMinutes: Int,
@ -85,6 +87,7 @@ case class GroupProps(
)
case class DefaultProps(
pluginProp: util.Map[String, AnyRef],
meetingProp: MeetingProp,
breakoutProps: BreakoutProps,
durationProps: DurationProps,
@ -118,7 +121,7 @@ case class AnswerVO(id: Int, key: String, text: Option[String], responders: Opti
case class QuestionVO(id: Int, questionType: String, multiResponse: Boolean, questionText: Option[String], answers: Option[Array[AnswerVO]])
case class PollVO(id: String, questions: Array[QuestionVO], title: Option[String], started: Boolean, stopped: Boolean, showResult: Boolean, isSecret: Boolean)
case class UserVO(id: String, externalId: String, name: String, role: String,
case class UserVO(id: String, externalId: String, name: String, role: String, bot: Boolean,
guest: Boolean, authed: Boolean, guestStatus: String, emojiStatus: String,
presenter: Boolean, hasStream: Boolean, locked: Boolean, webcamStreams: Set[String],
phoneUser: Boolean, voiceUser: VoiceUserVO, listenOnly: Boolean, avatarURL: String,

View File

@ -1,5 +1,7 @@
package org.bigbluebutton.common2.msgs
import java.util
object BreakoutRoomEndedEvtMsg { val NAME = "BreakoutRoomEndedEvtMsg" }
case class BreakoutRoomEndedEvtMsg(header: BbbClientMsgHeader, body: BreakoutRoomEndedEvtMsgBody) extends BbbCoreMsg
case class BreakoutRoomEndedEvtMsgBody(parentId: String, breakoutId: String)
@ -56,6 +58,7 @@ case class BreakoutRoomDetail(
captureSlides: Boolean,
captureNotesFilename: String,
captureSlidesFilename: String,
pluginProp: util.Map[String, AnyRef],
)
/**

View File

@ -137,19 +137,19 @@ case class GroupChatMessageDeletedEvtMsgBody(chatId: String, messageId: String)
object SendGroupChatMessageReactionReqMsg { val NAME = "SendGroupChatMessageReactionReqMsg" }
case class SendGroupChatMessageReactionReqMsg(header: BbbClientMsgHeader, body: SendGroupChatMessageReactionReqMsgBody) extends StandardMsg
case class SendGroupChatMessageReactionReqMsgBody(chatId: String, messageId: String, reactionEmoji: String)
case class SendGroupChatMessageReactionReqMsgBody(chatId: String, messageId: String, reactionEmoji: String, reactionEmojiId: String)
object GroupChatMessageReactionSentEvtMsg { val NAME = "GroupChatMessageReactionSentEvtMsg" }
case class GroupChatMessageReactionSentEvtMsg(header: BbbClientMsgHeader, body: GroupChatMessageReactionSentEvtMsgBody) extends BbbCoreMsg
case class GroupChatMessageReactionSentEvtMsgBody(chatId: String, messageId: String, reactionEmoji: String)
case class GroupChatMessageReactionSentEvtMsgBody(chatId: String, messageId: String, reactionEmoji: String, reactionEmojiId: String)
object DeleteGroupChatMessageReactionReqMsg { val NAME = "DeleteGroupChatMessageReactionReqMsg" }
case class DeleteGroupChatMessageReactionReqMsg(header: BbbClientMsgHeader, body: DeleteGroupChatMessageReactionReqMsgBody) extends StandardMsg
case class DeleteGroupChatMessageReactionReqMsgBody(chatId: String, messageId: String, reactionEmoji: String)
case class DeleteGroupChatMessageReactionReqMsgBody(chatId: String, messageId: String, reactionEmoji: String, reactionEmojiId: String)
object GroupChatMessageReactionDeletedEvtMsg { val NAME = "GroupChatMessageReactionDeletedEvtMsg" }
case class GroupChatMessageReactionDeletedEvtMsg(header: BbbClientMsgHeader, body: GroupChatMessageReactionDeletedEvtMsgBody) extends BbbCoreMsg
case class GroupChatMessageReactionDeletedEvtMsgBody(chatId: String, messageId: String, reactionEmoji: String)
case class GroupChatMessageReactionDeletedEvtMsgBody(chatId: String, messageId: String, reactionEmoji: String, reactionEmojiId: String)
object UserTypingPubMsg { val NAME = "UserTypingPubMsg" }
case class UserTypingPubMsg(header: BbbClientMsgHeader, body: UserTypingPubMsgBody) extends StandardMsg

View File

@ -7,6 +7,14 @@ import org.bigbluebutton.common2.domain.PluginLearningAnalyticsDashboardGenericD
/**
* Sent from graphql-actions to bbb-akka
*/
trait PluginDataChannelReplaceOrDeleteBaseBody{
val pluginName: String
val channelName: String
val subChannelName: String
val entryId: String
}
object PluginDataChannelPushEntryMsg { val NAME = "PluginDataChannelPushEntryMsg" }
case class PluginDataChannelPushEntryMsg(header: BbbClientMsgHeader, body: PluginDataChannelPushEntryMsgBody) extends StandardMsg
case class PluginDataChannelPushEntryMsgBody(
@ -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" }

View File

@ -7,8 +7,9 @@ case class RegisterUserReqMsg(
) extends BbbCoreMsg
case class RegisterUserReqMsgBody(meetingId: String, intUserId: String, name: String, role: String,
extUserId: String, authToken: String, sessionToken: String, avatarURL: String,
webcamBackgroundURL: String, guest: Boolean, authed: Boolean, guestStatus: String,
excludeFromDashboard: Boolean, enforceLayout: String, userMetadata: Map[String, String])
webcamBackgroundURL: String, bot: Boolean, guest: Boolean, authed: Boolean,
guestStatus: String, excludeFromDashboard: Boolean, enforceLayout: String,
userMetadata: Map[String, String])
object UserRegisteredRespMsg { val NAME = "UserRegisteredRespMsg" }
case class UserRegisteredRespMsg(
@ -88,6 +89,7 @@ case class UserJoinedMeetingEvtMsgBody(
extId: String,
name: String,
role: String,
bot: Boolean,
guest: Boolean,
authed: Boolean,
guestStatus: String,

View File

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

View File

@ -19,7 +19,11 @@
package org.bigbluebutton.api;
import java.io.File;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.BlockingQueue;
@ -29,7 +33,11 @@ import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.JsonObject;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.utils.URIBuilder;
import org.bigbluebutton.api.domain.*;
@ -57,6 +65,8 @@ import com.google.gson.Gson;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.data.domain.*;
@ -97,6 +107,8 @@ public class MeetingService implements MessageListener {
private HashMap<String, PresentationUploadToken> uploadAuthzTokens;
ObjectMapper objectMapper = new ObjectMapper();
public MeetingService() {
meetings = new ConcurrentHashMap<String, Meeting>(8, 0.9f, 1);
sessions = new ConcurrentHashMap<String, UserSession>(8, 0.9f, 1);
@ -122,12 +134,12 @@ public class MeetingService implements MessageListener {
public void registerUser(String meetingID, String internalUserId,
String fullname, String role, String externUserID,
String authToken, String sessionToken, String avatarURL, String webcamBackgroundURL, Boolean guest,
Boolean authed, String guestStatus, Boolean excludeFromDashboard, Boolean leftGuestLobby,
String authToken, String sessionToken, String avatarURL, String webcamBackgroundURL, Boolean bot,
Boolean guest, Boolean authed, String guestStatus, Boolean excludeFromDashboard, Boolean leftGuestLobby,
String enforceLayout, Map<String, String> userMetadata) {
handle(
new RegisterUser(meetingID, internalUserId, fullname, role,
externUserID, authToken, sessionToken, avatarURL, webcamBackgroundURL, guest, authed, guestStatus,
externUserID, authToken, sessionToken, avatarURL, webcamBackgroundURL, bot, guest, authed, guestStatus,
excludeFromDashboard, leftGuestLobby, enforceLayout, userMetadata
)
);
@ -190,7 +202,10 @@ public class MeetingService implements MessageListener {
UserSessionBasicData removedUser = new UserSessionBasicData();
removedUser.meetingId = us.meetingID;
removedUser.extMeetingId = us.externMeetingID;
removedUser.userId = us.internalUserId;
removedUser.extUserId = us.externUserID;
removedUser.userFullName = us.fullname;
removedUser.sessionToken = us.authToken;
removedUser.role = us.role;
removedSessions.put(token, removedUser);
@ -352,13 +367,124 @@ 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 (PluginManifest pluginManifest : m.getPluginManifests()) {
try {
String urlString = pluginManifest.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 = pluginManifest.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 {}.",
pluginManifest.getUrl()
);
log.info("Plugin {} is not going to be loaded",
pluginManifest.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: ",
pluginManifest.getUrl(), e);
log.error("Therefore this plugin will not be loaded");
}
}
return urlContents;
}
public synchronized boolean createMeeting(Meeting m) {
Map<String, Object> pluginsMap = new HashMap<>();
return createMeeting(m, pluginsMap);
}
public synchronized boolean createMeeting(Meeting m, Map<String, Object> plugins) {
String internalMeetingId = paramsProcessorUtil.convertToInternalMeetingId(m.getExternalId());
Meeting existingId = getNotEndedMeetingWithId(internalMeetingId);
Meeting existingTelVoice = getNotEndedMeetingWithTelVoice(m.getTelVoice());
Meeting existingWebVoice = getNotEndedMeetingWithWebVoice(m.getWebVoice());
if (existingId == null && existingTelVoice == null && existingWebVoice == null) {
meetings.put(m.getInternalId(), m);
Map<String, Object> pluginsMap;
if (m.isBreakout()) {
pluginsMap = plugins;
} else {
pluginsMap = requestPluginManifests(m);
}
m.setPlugins(pluginsMap);
handle(new CreateMeeting(m));
return true;
}
@ -444,7 +570,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());
}
@ -459,8 +585,8 @@ public class MeetingService implements MessageListener {
private void processRegisterUser(RegisterUser message) {
gw.registerUser(message.meetingID,
message.internalUserId, message.fullname, message.role,
message.externUserID, message.authToken, message.sessionToken, message.avatarURL, message.webcamBackgroundURL, message.guest,
message.authed, message.guestStatus, message.excludeFromDashboard, message.enforceLayout, message.userMetadata);
message.externUserID, message.authToken, message.sessionToken, message.avatarURL, message.webcamBackgroundURL, message.bot,
message.guest, message.authed, message.guestStatus, message.excludeFromDashboard, message.enforceLayout, message.userMetadata);
}
private void processRegisterUserSessionToken(RegisterUserSessionToken message) {
@ -694,7 +820,7 @@ public class MeetingService implements MessageListener {
Meeting breakout = paramsProcessorUtil.processCreateParams(params);
createMeeting(breakout);
createMeeting(breakout, message.pluginProp);
presDownloadService.extractPresentationPage(message.parentMeetingId,
message.sourcePresentationId,
@ -960,10 +1086,10 @@ public class MeetingService implements MessageListener {
}
User user = new User(message.userId, message.externalUserId,
message.name, message.role, message.locked, message.avatarURL, message.webcamBackgroundURL, message.guest, message.guestStatus,
message.clientType);
message.name, message.role, message.locked, message.avatarURL, message.webcamBackgroundURL, message.bot,
message.guest, message.guestStatus, message.clientType);
if(m.getMaxUsers() > 0 && m.countUniqueExtIds() >= m.getMaxUsers()) {
if(m.getMaxUsers() > 0 && m.countUniqueExtIds() >= m.getMaxUsers() && !user.isBot()) {
m.removeEnteredUser(user.getInternalUserId());
return;
}
@ -983,6 +1109,7 @@ public class MeetingService implements MessageListener {
logData.put("externalUserId", user.getExternalUserId());
logData.put("username", user.getFullname());
logData.put("role", user.getRole());
logData.put("bot", user.isBot());
logData.put("guest", user.isGuest());
logData.put("guestStatus", user.getGuestStatus());
logData.put("logCode", "user_joined_message");
@ -1079,9 +1206,10 @@ public class MeetingService implements MessageListener {
user.setVoiceJoined(true);
} else {
if (message.userId.startsWith("v_")) {
Boolean bot = false;
// A dial-in user joined the meeting. Dial-in users by convention has userId that starts with "v_".
User vuser = new User(message.userId, message.userId, message.name, "DIAL-IN-USER", true, "", "",
true, GuestPolicy.ALLOW, "DIAL-IN");
bot, true, GuestPolicy.ALLOW, "DIAL-IN");
vuser.setVoiceJoined(true);
m.userJoined(vuser);
}

View File

@ -26,10 +26,9 @@ import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.*;
import org.bigbluebutton.api.domain.*;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.safety.Safelist;
@ -39,10 +38,6 @@ import org.jsoup.select.Elements;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.bigbluebutton.api.domain.BreakoutRoomsParams;
import org.bigbluebutton.api.domain.LockSettingsParams;
import org.bigbluebutton.api.domain.Meeting;
import org.bigbluebutton.api.domain.Group;
import org.bigbluebutton.api.service.ServiceUtils;
import org.bigbluebutton.api.util.ParamsUtil;
import org.slf4j.Logger;
@ -82,6 +77,7 @@ public class ParamsProcessorUtil {
private Integer defaultHttpSessionTimeout = 14400;
private Boolean useDefaultAvatar = false;
private String defaultAvatarURL;
private String defaultBotAvatarURL;
private Boolean useDefaultWebcamBackground = false;
private String defaultWebcamBackgroundURL;
private String defaultGuestPolicy;
@ -104,6 +100,7 @@ public class ParamsProcessorUtil {
private boolean defaultAllowModsToUnmuteUsers = false;
private boolean defaultAllowModsToEjectCameras = false;
private String defaultDisabledFeatures;
private String defaultPluginManifests;
private boolean defaultNotifyRecordingIsOn = false;
private boolean defaultKeepEvents = false;
private Boolean useDefaultLogo;
@ -432,6 +429,33 @@ public class ParamsProcessorUtil {
return groups;
}
private ArrayList<PluginManifest> processPluginManifests(String pluginManifestsParam) {
ArrayList<PluginManifest> pluginManifests = new ArrayList<PluginManifest>();
JsonElement pluginManifestsJsonElement = new Gson().fromJson(pluginManifestsParam, JsonElement.class);
try {
if (pluginManifestsJsonElement != null && pluginManifestsJsonElement.isJsonArray()) {
JsonArray pluginManifestsJson = pluginManifestsJsonElement.getAsJsonArray();
for (JsonElement pluginManifestJson : pluginManifestsJson) {
if (pluginManifestJson.isJsonObject()) {
JsonObject pluginManifestJsonObj = pluginManifestJson.getAsJsonObject();
if (pluginManifestJsonObj.has("url")) {
String url = pluginManifestJsonObj.get("url").getAsString();
PluginManifest newPlugin = new PluginManifest(url);
if (pluginManifestJsonObj.has("checksum")) {
newPlugin.setChecksum(pluginManifestJsonObj.get("checksum").getAsString());
}
pluginManifests.add(newPlugin);
}
}
}
}
} catch(JsonSyntaxException err){
log.error("Error in pluginManifests URL parameter's json structure.");
}
return pluginManifests;
}
public Meeting processCreateParams(Map<String, String> params) {
String meetingName = params.get(ApiParams.NAME);
@ -550,6 +574,22 @@ public class ParamsProcessorUtil {
listOfDisabledFeatures.removeAll(Arrays.asList(disabledFeaturesExcludeParam.split(",")));
}
// Parse Plugins Manifests from config and param
ArrayList<PluginManifest> listOfPluginManifests = new ArrayList<PluginManifest>();
if (!isBreakout){
//Process plugins from config
if (defaultPluginManifests != null && !defaultPluginManifests.isEmpty()) {
ArrayList<PluginManifest> pluginManifestsFromConfig = processPluginManifests(defaultPluginManifests);
listOfPluginManifests.addAll(pluginManifestsFromConfig);
}
//Process plugins from /create param
String pluginManifestsParam = params.get(ApiParams.PLUGIN_MANIFESTS);
if (!StringUtils.isEmpty(pluginManifestsParam)) {
ArrayList<PluginManifest> pluginManifestsFromParam = processPluginManifests(pluginManifestsParam);
listOfPluginManifests.addAll(pluginManifestsFromParam);
}
}
// Check if VirtualBackgrounds is disabled
if (!StringUtils.isEmpty(params.get(ApiParams.VIRTUAL_BACKGROUNDS_DISABLED))) {
boolean virtualBackgroundsDisabled = Boolean.valueOf(params.get(ApiParams.VIRTUAL_BACKGROUNDS_DISABLED));
@ -746,6 +786,7 @@ public class ParamsProcessorUtil {
}
String avatarURL = useDefaultAvatar ? defaultAvatarURL : "";
String botAvatarURL = defaultBotAvatarURL;
String webcamBackgroundURL = useDefaultWebcamBackground ? defaultWebcamBackgroundURL : "";
if(defaultAllowDuplicateExtUserid == false) {
@ -766,6 +807,7 @@ public class ParamsProcessorUtil {
.withTelVoice(telVoice).withWebVoice(webVoice)
.withDialNumber(dialNumber)
.withDefaultAvatarURL(avatarURL)
.withDefaultBotAvatarURL(botAvatarURL)
.withDefaultWebcamBackgroundURL(webcamBackgroundURL)
.withAutoStartRecording(autoStartRec)
.withAllowStartStopRecording(allowStartStoptRec)
@ -790,6 +832,7 @@ public class ParamsProcessorUtil {
.withLearningDashboardCleanupDelayInMinutes(learningDashboardCleanupMins)
.withLearningDashboardAccessToken(learningDashboardAccessToken)
.withGroups(groups)
.withPluginManifests(listOfPluginManifests)
.withDisabledFeatures(listOfDisabledFeatures)
.withNotifyRecordingIsOn(notifyRecordingIsOn)
.withPresentationUploadExternalDescription(presentationUploadExternalDescription)
@ -1355,6 +1398,10 @@ public class ParamsProcessorUtil {
this.defaultAvatarURL = url;
}
public void setDefaultBotAvatarURL(String url) {
this.defaultBotAvatarURL = url;
}
public void setUseDefaultWebcamBackground(Boolean value) {
this.useDefaultWebcamBackground = value;
}
@ -1574,36 +1621,40 @@ public class ParamsProcessorUtil {
this.defaultEndWhenNoModerator = val;
}
public void setEndWhenNoModeratorDelayInMinutes(Integer value) {
this.defaultEndWhenNoModeratorDelayInMinutes = value;
}
public void setEndWhenNoModeratorDelayInMinutes(Integer value) {
this.defaultEndWhenNoModeratorDelayInMinutes = value;
}
public void setDisabledFeatures(String disabledFeatures) {
this.defaultDisabledFeatures = disabledFeatures;
}
public void setDisabledFeatures(String disabledFeatures) {
this.defaultDisabledFeatures = disabledFeatures;
}
public void setNotifyRecordingIsOn(Boolean notifyRecordingIsOn) {
this.defaultNotifyRecordingIsOn = notifyRecordingIsOn;
}
public void setPluginManifests(String pluginManifests) {
this.defaultPluginManifests = pluginManifests;
}
public void setPresentationUploadExternalDescription(String presentationUploadExternalDescription) {
this.defaultPresentationUploadExternalDescription = presentationUploadExternalDescription;
}
public void setNotifyRecordingIsOn(Boolean notifyRecordingIsOn) {
this.defaultNotifyRecordingIsOn = notifyRecordingIsOn;
}
public void setPresentationUploadExternalUrl(String presentationUploadExternalUrl) {
this.defaultPresentationUploadExternalUrl = presentationUploadExternalUrl;
}
public void setPresentationUploadExternalDescription(String presentationUploadExternalDescription) {
this.defaultPresentationUploadExternalDescription = presentationUploadExternalDescription;
}
public void setBbbVersion(String version) {
public void setPresentationUploadExternalUrl(String presentationUploadExternalUrl) {
this.defaultPresentationUploadExternalUrl = presentationUploadExternalUrl;
}
public void setBbbVersion(String version) {
this.bbbVersion = this.allowRevealOfBBBVersion ? version : "";
}
}
public void setAllowRevealOfBBBVersion(Boolean allowVersion) {
this.allowRevealOfBBBVersion = allowVersion;
}
public void setAllowRevealOfBBBVersion(Boolean allowVersion) {
this.allowRevealOfBBBVersion = allowVersion;
}
public void setAllowOverrideClientSettingsOnCreateCall(Boolean allowOverrideClientSettingsOnCreateCall) {
this.allowOverrideClientSettingsOnCreateCall = allowOverrideClientSettingsOnCreateCall;
}
public void setAllowOverrideClientSettingsOnCreateCall(Boolean allowOverrideClientSettingsOnCreateCall) {
this.allowOverrideClientSettingsOnCreateCall = allowOverrideClientSettingsOnCreateCall;
}
}

View File

@ -77,7 +77,10 @@ public class Meeting {
private Integer maxPinnedCameras = 0;
private String dialNumber;
private String defaultAvatarURL;
private String defaultBotAvatarURL;
private String defaultWebcamBackgroundURL;
private Map<String, Object> plugins;
private ArrayList<PluginManifest> pluginManifests;
private String guestPolicy = GuestPolicy.ASK_MODERATOR;
private String guestLobbyMessage = "";
private Map<String,String> usersWithGuestLobbyMessages;
@ -128,6 +131,7 @@ public class Meeting {
extMeetingId = builder.externalId;
intMeetingId = builder.internalId;
disabledFeatures = builder.disabledFeatures;
pluginManifests = builder.pluginManifests;
notifyRecordingIsOn = builder.notifyRecordingIsOn;
presentationUploadExternalDescription = builder.presentationUploadExternalDescription;
presentationUploadExternalUrl = builder.presentationUploadExternalUrl;
@ -150,7 +154,8 @@ public class Meeting {
logoutUrl = builder.logoutUrl;
logoutTimer = builder.logoutTimer;
defaultAvatarURL = builder.defaultAvatarURL;
defaultWebcamBackgroundURL = builder.defaultWebcamBackgroundURL;
defaultBotAvatarURL = builder.defaultBotAvatarURL;
defaultWebcamBackgroundURL = builder.defaultWebcamBackgroundURL;
record = builder.record;
autoStartRecording = builder.autoStartRecording;
allowStartStopRecording = builder.allowStartStopRecording;
@ -441,6 +446,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<PluginManifest> getPluginManifests() {
return pluginManifests;
}
public Boolean getNotifyRecordingIsOn() {
return notifyRecordingIsOn;
@ -465,6 +481,10 @@ public class Meeting {
return defaultAvatarURL;
}
public String getDefaultBotAvatarURL() {
return defaultBotAvatarURL;
}
public String getDefaultWebcamBackgroundURL() {
return defaultWebcamBackgroundURL;
}
@ -929,6 +949,7 @@ public class Meeting {
private int learningDashboardCleanupDelayInMinutes;
private String learningDashboardAccessToken;
private ArrayList<String> disabledFeatures;
private ArrayList<PluginManifest> pluginManifests;
private Boolean notifyRecordingIsOn;
private String presentationUploadExternalDescription;
private String presentationUploadExternalUrl;
@ -945,6 +966,7 @@ public class Meeting {
private Map<String, String> metadata;
private String dialNumber;
private String defaultAvatarURL;
private String defaultBotAvatarURL;
private String defaultWebcamBackgroundURL;
private long createdTime;
private boolean isBreakout;
@ -1063,6 +1085,11 @@ public class Meeting {
return this;
}
public Builder withPluginManifests(ArrayList<PluginManifest> map) {
this.pluginManifests = map;
return this;
}
public Builder withNotifyRecordingIsOn(Boolean b) {
this.notifyRecordingIsOn = b;
return this;
@ -1093,6 +1120,11 @@ public class Meeting {
return this;
}
public Builder withDefaultBotAvatarURL(String w) {
defaultBotAvatarURL = w;
return this;
}
public Builder withDefaultWebcamBackgroundURL(String w) {
defaultWebcamBackgroundURL = w;
return this;

View File

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

View File

@ -34,6 +34,7 @@ public class User {
private String avatarURL;
private String webcamBackgroundURL;
private Map<String,String> status;
private Boolean bot;
private Boolean guest;
private String guestStatus;
private Boolean listeningOnly = false;
@ -49,6 +50,7 @@ public class User {
Boolean locked,
String avatarURL,
String webcamBackgroundURL,
Boolean bot,
Boolean guest,
String guestStatus,
String clientType) {
@ -59,6 +61,7 @@ public class User {
this.locked = locked;
this.avatarURL = avatarURL;
this.webcamBackgroundURL = webcamBackgroundURL;
this.bot = bot;
this.guest = guest;
this.guestStatus = guestStatus;
this.status = new ConcurrentHashMap<>();
@ -81,6 +84,14 @@ public class User {
this.externalUserId = externalUserId;
}
public void setBot(Boolean bot) {
this.bot = bot;
}
public Boolean isBot() {
return this.bot;
}
public void setGuest(Boolean guest) {
this.guest = guest;
}

View File

@ -32,6 +32,7 @@ public class UserSession {
public String role = null;
public String conference = null;
public String room = null;
public Boolean bot = false;
public Boolean guest = false;
public Boolean authed = false;
public String voicebridge = null;

View File

@ -22,7 +22,10 @@ package org.bigbluebutton.api.domain;
public class UserSessionBasicData {
public String sessionToken = null;
public String userId = null;
public String extUserId = null;
public String meetingId = null;
public String extMeetingId = null;
public String userFullName = null;
public String role = null;
public Boolean isModerator() {

View File

@ -1,5 +1,7 @@
package org.bigbluebutton.api.messaging.messages;
import java.util.Map;
public class CreateBreakoutRoom implements IMessage {
public final String meetingId;
@ -23,6 +25,7 @@ public class CreateBreakoutRoom implements IMessage {
public final Boolean captureSlides; // Upload annotated breakout slides to main room after breakout room end
public final String captureNotesFilename;
public final String captureSlidesFilename;
public final Map<String, Object> pluginProp;
public CreateBreakoutRoom(String meetingId,
String parentMeetingId,
@ -43,7 +46,8 @@ public class CreateBreakoutRoom implements IMessage {
Boolean captureNotes,
Boolean captureSlides,
String captureNotesFilename,
String captureSlidesFilename) {
String captureSlidesFilename,
Map<String, Object> pluginProp) {
this.meetingId = meetingId;
this.parentMeetingId = parentMeetingId;
this.name = name;
@ -64,5 +68,6 @@ public class CreateBreakoutRoom implements IMessage {
this.captureSlides = captureSlides;
this.captureNotesFilename = captureNotesFilename;
this.captureSlidesFilename = captureSlidesFilename;
this.pluginProp = pluginProp;
}
}

View File

@ -14,6 +14,7 @@ public class RegisterUser implements IMessage {
public final String sessionToken;
public final String avatarURL;
public final String webcamBackgroundURL;
public final Boolean bot;
public final Boolean guest;
public final Boolean authed;
public final String guestStatus;
@ -23,7 +24,7 @@ public class RegisterUser implements IMessage {
public final Map<String, String> userMetadata;
public RegisterUser(String meetingID, String internalUserId, String fullname, String role, String externUserID,
String authToken, String sessionToken, String avatarURL, String webcamBackgroundURL, Boolean guest,
String authToken, String sessionToken, String avatarURL, String webcamBackgroundURL, Boolean bot, Boolean guest,
Boolean authed, String guestStatus, Boolean excludeFromDashboard, Boolean leftGuestLobby,
String enforceLayout, Map<String, String> userMetadata) {
this.meetingID = meetingID;
@ -35,6 +36,7 @@ public class RegisterUser implements IMessage {
this.sessionToken = sessionToken;
this.avatarURL = avatarURL;
this.webcamBackgroundURL = webcamBackgroundURL;
this.bot = bot;
this.guest = guest;
this.authed = authed;
this.guestStatus = guestStatus;

View File

@ -9,6 +9,7 @@ public class UserJoined implements IMessage {
public final Boolean locked;
public final String avatarURL;
public final String webcamBackgroundURL;
public final Boolean bot;
public final Boolean guest;
public final String guestStatus;
public final String clientType;
@ -22,6 +23,7 @@ public class UserJoined implements IMessage {
Boolean locked,
String avatarURL,
String webcamBackgroundURL,
Boolean bot,
Boolean guest,
String guestStatus,
String clientType) {
@ -33,6 +35,7 @@ public class UserJoined implements IMessage {
this.locked = locked;
this.avatarURL = avatarURL;
this.webcamBackgroundURL = webcamBackgroundURL;
this.bot = bot;
this.guest = guest;
this.guestStatus = guestStatus;
this.clientType = clientType;

View File

@ -22,8 +22,8 @@ public interface IPublisherService {
void endMeeting(String meetingId);
void send(String channel, String message);
void registerUser(String meetingID, String internalUserId, String fullname, String role, String externUserID,
String authToken, String avatarURL, String webcamBackgroundURL, Boolean guest, Boolean excludeFromDashboard,
String enforceLayout, Boolean authed);
String authToken, String avatarURL, String webcamBackgroundURL, Boolean bot, Boolean guest,
Boolean excludeFromDashboard, String enforceLayout, Boolean authed);
void sendKeepAlive(String system, Long bbbWebTimestamp, Long akkaAppsTimestamp);
void sendStunTurnInfo(String meetingId, String internalUserId, Set<StunServer> stuns, Set<TurnEntry> turns);
}

View File

@ -69,11 +69,12 @@ 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,
String externUserID, String authToken, String sessionToken, String avatarURL, String webcamBackgroundURL,
Boolean guest, Boolean authed, String guestStatus, Boolean excludeFromDashboard,
Boolean bot, Boolean guest, Boolean authed, String guestStatus, Boolean excludeFromDashboard,
String enforceLayout, Map<String, String> userMetadata);
void registerUserSessionToken(String meetingID, String internalUserId, String sessionToken,
String replaceSessionToken, String enforceLayout, Map<String, String> userSessionMetadata);

View File

@ -41,6 +41,7 @@ public class PresentationUrlDownloadService {
private String defaultUploadedPresentation;
private List<String> insertDocumentSupportedProtocols;
private List<String> insertDocumentBlockedHosts;
private int presDownloadReadTimeoutInMs = 60000;
private ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
@ -194,7 +195,7 @@ public class PresentationUrlDownloadService {
HttpURLConnection conn;
try {
conn = (HttpURLConnection) presUrl.openConnection();
conn.setReadTimeout(60000);
conn.setReadTimeout(presDownloadReadTimeoutInMs);
conn.addRequestProperty("Accept-Language", "en-US,en;q=0.8");
conn.addRequestProperty("User-Agent", "Mozilla");
conn.setInstanceFollowRedirects(false);
@ -372,4 +373,7 @@ public class PresentationUrlDownloadService {
this.insertDocumentBlockedHosts = new ArrayList<>(Arrays.asList(insertDocumentBlockedHosts.split(",")));
}
public void setPresDownloadReadTimeoutInMs(int presDownloadReadTimeoutInMs) {
this.presDownloadReadTimeoutInMs = presDownloadReadTimeoutInMs;
}
}

View File

@ -33,7 +33,7 @@ import com.zaxxer.nuprocess.NuProcessBuilder;
public class ImageResizerImp implements ImageResizer {
private static Logger log = LoggerFactory.getLogger(ImageResizerImp.class);
private static int waitForSec = 7;
private int wait = 7;
public boolean resize(UploadedPresentation pres, String ratio) {
Boolean conversionSuccess = true;
@ -47,7 +47,7 @@ public class ImageResizerImp implements ImageResizer {
NuProcess process = imgResize.start();
try {
process.waitFor(waitForSec, TimeUnit.SECONDS);
process.waitFor(wait, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.error(e.getMessage());
conversionSuccess = false;
@ -56,4 +56,7 @@ public class ImageResizerImp implements ImageResizer {
return conversionSuccess;
}
public void setWait(int wait) {
this.wait = wait;
}
}

View File

@ -16,15 +16,18 @@ public class OfficeDocumentValidator2 {
private String presCheckExec;
private int presCheckTimeout = 20;
private long execTimeout = 25000;
public boolean isValid(UploadedPresentation pres) {
boolean valid = true;
if (FilenameUtils.isExtension(pres.getUploadedFile().getName(), FileTypeConstants.PPTX)) {
String COMMAND = "timeout 20 " + presCheckExec + " " + pres.getUploadedFile().getAbsolutePath();
String COMMAND = "timeout " + presCheckTimeout + " " + presCheckExec + " " + pres.getUploadedFile().getAbsolutePath();
log.info("Running pres check " + COMMAND);
boolean done = new ExternalProcessExecutor().exec(COMMAND, 25000);
boolean done = new ExternalProcessExecutor().exec(COMMAND, execTimeout);
if (done) {
return true;
@ -49,4 +52,11 @@ public class OfficeDocumentValidator2 {
this.presCheckExec = path;
}
public void setPresCheckTimeout(int presCheckTimeout) {
this.presCheckTimeout = presCheckTimeout;
}
public void setExecTimeout(long execTimeout) {
this.execTimeout = execTimeout;
}
}

View File

@ -29,11 +29,16 @@ public class PageExtractorImp implements PageExtractor {
private static Logger log = LoggerFactory.getLogger(PageExtractorImp.class);
private static final String SPACE = " ";
private static final long extractTimeout = 10000; // 10sec
private long extractTimeoutInMs = 10000; // 10sec
public boolean extractPage(File presentationFile, File output, int page) {
String COMMAND = "pdfseparate -f " + page + " -l " + page + SPACE
+ presentationFile.getAbsolutePath() + SPACE + output.getAbsolutePath();
return new ExternalProcessExecutor().exec(COMMAND, extractTimeout);
return new ExternalProcessExecutor().exec(COMMAND, extractTimeoutInMs);
}
public void setExtractTimeoutInMs(long extractTimeoutInMs) {
this.extractTimeoutInMs = extractTimeoutInMs;
}
}

View File

@ -34,7 +34,7 @@ import com.zaxxer.nuprocess.NuProcessBuilder;
public class PdfPageCounter implements PageCounter {
private static Logger log = LoggerFactory.getLogger(PdfPageCounter.class);
private static int waitForSec = 5;
private int wait = 5;
public int countNumberOfPages(File presentationFile) {
int numPages = 0; // total numbers of this pdf
@ -47,7 +47,7 @@ public class PdfPageCounter implements PageCounter {
NuProcess process = pdfInfo.start();
try {
process.waitFor(waitForSec, TimeUnit.SECONDS);
process.waitFor(wait, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.error("InterruptedException while counting PDF pages {}", presentationFile.getName(), e);
}
@ -56,4 +56,7 @@ public class PdfPageCounter implements PageCounter {
return numPages;
}
public void setWait(int wait) {
this.wait = wait;
}
}

View File

@ -5,6 +5,8 @@ import java.io.File;
public class PdfPageDownscaler {
private static final String SPACE = " ";
private long execTimeout = 10000;
public boolean downscale(File source,File dest) {
String COMMAND = "gs -sDEVICE=pdfwrite -dNOPAUSE -dQUIET -dBATCH -dFirstPage=1 -dLastPage=1 -sOutputFile="
+ dest.getAbsolutePath() + SPACE
@ -13,6 +15,10 @@ public class PdfPageDownscaler {
//System.out.println("DOWNSCALING " + COMMAND);
return new ExternalProcessExecutor().exec(COMMAND, 10000);
return new ExternalProcessExecutor().exec(COMMAND, execTimeout);
}
public void setExecTimeout(long execTimeout) {
this.execTimeout = execTimeout;
}
}

View File

@ -45,8 +45,9 @@ public class PngCreatorImp implements PngCreator {
private String BLANK_PNG;
private int slideWidth = 800;
private String convTimeout = "7s";
private int WAIT_FOR_SEC = 7;
private int convTimeout = 7;
private int wait = 7;
private long execTimeout = 10000;
private static final String TEMP_PNG_NAME = "temp-png";
@ -93,14 +94,14 @@ public class PngCreatorImp implements PngCreator {
dest = pngsDir.getAbsolutePath() + File.separator + "slide-1.pdf";
NuProcessBuilder convertImgToSvg = new NuProcessBuilder(
Arrays.asList("timeout", convTimeout, "convert", source, "-auto-orient", dest));
Arrays.asList("timeout", convTimeout + "s", "convert", source, "-auto-orient", dest));
Png2SvgConversionHandler pHandler = new Png2SvgConversionHandler();
convertImgToSvg.setProcessListener(pHandler);
NuProcess process = convertImgToSvg.start();
try {
process.waitFor(WAIT_FOR_SEC, TimeUnit.SECONDS);
process.waitFor(wait, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.error("InterruptedException while converting to PDF {}", dest, e);
return false;
@ -116,7 +117,7 @@ public class PngCreatorImp implements PngCreator {
//System.out.println("********* CREATING PNGs " + COMMAND);
boolean done = new ExternalProcessExecutor().exec(COMMAND, 10000);
boolean done = new ExternalProcessExecutor().exec(COMMAND, execTimeout);
if (done) {
return true;
@ -214,4 +215,15 @@ public class PngCreatorImp implements PngCreator {
slideWidth = width;
}
public void setConvTimeout(int convTimeout) {
this.convTimeout = convTimeout;
}
public void setWait(int wait) {
this.wait = wait;
}
public void setExecTimeout(long execTimeout) {
this.execTimeout = execTimeout;
}
}

View File

@ -368,6 +368,7 @@ public class SvgImageCreatorImp implements SvgImageCreator {
}
public void setBlankSvg(String blankSvg) {
BLANK_SVG = blankSvg;
}

View File

@ -38,6 +38,8 @@ import com.google.gson.Gson;
public class TextFileCreatorImp implements TextFileCreator {
private static Logger log = LoggerFactory.getLogger(TextFileCreatorImp.class);
private long execTimeout = 60000;
@Override
public boolean createTextFile(UploadedPresentation pres, int page) {
boolean success = false;
@ -97,7 +99,7 @@ public class TextFileCreatorImp implements TextFileCreator {
//System.out.println(COMMAND);
boolean done = new ExternalProcessExecutor().exec(COMMAND, 60000);
boolean done = new ExternalProcessExecutor().exec(COMMAND, execTimeout);
if (!done) {
success = false;
@ -130,4 +132,7 @@ public class TextFileCreatorImp implements TextFileCreator {
}
}
public void setExecTimeout(long execTimeout) {
this.execTimeout = execTimeout;
}
}

View File

@ -46,6 +46,8 @@ public class ThumbnailCreatorImp implements ThumbnailCreator {
private String BLANK_THUMBNAIL;
private long execTimeout = 10000;
@Override
public boolean createThumbnail(UploadedPresentation pres, int page, File pageFile) {
boolean success = false;
@ -88,7 +90,7 @@ public class ThumbnailCreatorImp implements ThumbnailCreator {
//System.out.println(COMMAND);
boolean done = new ExternalProcessExecutor().exec(COMMAND, 10000);
boolean done = new ExternalProcessExecutor().exec(COMMAND, execTimeout);
if (done) {
return true;
@ -194,4 +196,7 @@ public class ThumbnailCreatorImp implements ThumbnailCreator {
BLANK_THUMBNAIL = blankThumbnail;
}
public void setExecTimeout(long execTimeout) {
this.execTimeout = execTimeout;
}
}

View File

@ -18,6 +18,8 @@ import scala.concurrent.duration._
import org.bigbluebutton.common2.redis._
import org.bigbluebutton.common2.bus._
import java.util
class BbbWebApiGWApp(
val oldMessageReceivedGW: OldMessageReceivedGW,
redisHost: String,
@ -165,6 +167,7 @@ class BbbWebApiGWApp(
notifyRecordingIsOn: java.lang.Boolean,
presentationUploadExternalDescription: String,
presentationUploadExternalUrl: String,
plugins: util.Map[String, AnyRef],
overrideClientSettings: String): Unit = {
val disabledFeaturesAsVector: Vector[String] = disabledFeatures.asScala.toVector
@ -262,6 +265,7 @@ class BbbWebApiGWApp(
val groupsAsVector: Vector[GroupProps] = groups.asScala.toVector.map(g => GroupProps(g.getGroupId(), g.getName(), g.getUsersExtId().asScala.toVector))
val defaultProps = DefaultProps(
plugins,
meetingProp,
breakoutProps,
durationProps,
@ -286,8 +290,8 @@ class BbbWebApiGWApp(
def registerUser(meetingId: String, intUserId: String, name: String,
role: String, extUserId: String, authToken: String, sessionToken: String,
avatarURL: String, webcamBackgroundURL: String, guest: java.lang.Boolean, authed: java.lang.Boolean,
guestStatus: String, excludeFromDashboard: java.lang.Boolean,
avatarURL: String, webcamBackgroundURL: String, bot: java.lang.Boolean, guest: java.lang.Boolean,
authed: java.lang.Boolean, guestStatus: String, excludeFromDashboard: java.lang.Boolean,
enforceLayout: String, userMetadata: java.util.Map[String, String]): Unit = {
// meetingManagerActorRef ! new RegisterUser(meetingId = meetingId, intUserId = intUserId, name = name,
@ -296,9 +300,9 @@ class BbbWebApiGWApp(
val regUser = new RegisterUser(meetingId = meetingId, intUserId = intUserId, name = name,
role = role, extUserId = extUserId, authToken = authToken, sessionToken = sessionToken,
avatarURL = avatarURL, webcamBackgroundURL = webcamBackgroundURL, guest = guest.booleanValue(), authed = authed.booleanValue(),
guestStatus = guestStatus, excludeFromDashboard = excludeFromDashboard, enforceLayout = enforceLayout,
userMetadata = (userMetadata).asScala.toMap)
avatarURL = avatarURL, webcamBackgroundURL = webcamBackgroundURL, bot = bot.booleanValue(), guest = guest.booleanValue(),
authed = authed.booleanValue(), guestStatus = guestStatus, excludeFromDashboard = excludeFromDashboard,
enforceLayout = enforceLayout, userMetadata = (userMetadata).asScala.toMap)
val event = MsgBuilder.buildRegisterUserRequestToAkkaApps(regUser)
msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event))

View File

@ -50,8 +50,9 @@ object MsgBuilder {
val header = BbbCoreHeaderWithMeetingId(RegisterUserReqMsg.NAME, msg.meetingId)
val body = RegisterUserReqMsgBody(meetingId = msg.meetingId, intUserId = msg.intUserId,
name = msg.name, role = msg.role, extUserId = msg.extUserId, authToken = msg.authToken, sessionToken = msg.sessionToken,
avatarURL = msg.avatarURL, webcamBackgroundURL = msg.webcamBackgroundURL, guest = msg.guest, authed = msg.authed, guestStatus = msg.guestStatus,
excludeFromDashboard = msg.excludeFromDashboard, enforceLayout = msg.enforceLayout, userMetadata = msg.userMetadata)
avatarURL = msg.avatarURL, webcamBackgroundURL = msg.webcamBackgroundURL, bot = msg.bot, guest = msg.guest, authed = msg.authed,
guestStatus = msg.guestStatus, excludeFromDashboard = msg.excludeFromDashboard, enforceLayout = msg.enforceLayout,
userMetadata = msg.userMetadata)
val req = RegisterUserReqMsg(header, body)
BbbCommonEnvCoreMsg(envelope, req)
}

View File

@ -17,7 +17,7 @@ case class CreateBreakoutRoomMsg(meetingId: String, parentMeetingId: String,
case class AddUserSession(token: String, session: UserSession)
case class RegisterUser(meetingId: String, intUserId: String, name: String, role: String,
extUserId: String, authToken: String, sessionToken: String, avatarURL: String, webcamBackgroundURL: String,
guest: Boolean, authed: Boolean, guestStatus: String, excludeFromDashboard: Boolean,
bot: Boolean, guest: Boolean, authed: Boolean, guestStatus: String, excludeFromDashboard: Boolean,
enforceLayout: String, userMetadata: Map[String, String])
case class CreateMeetingMsg(defaultProps: DefaultProps)

View File

@ -127,6 +127,7 @@ class OldMeetingMsgHdlrActor(val olgMsgGW: OldMessageReceivedGW)
msg.body.room.captureSlides,
msg.body.room.captureNotesFilename,
msg.body.room.captureSlidesFilename,
msg.body.room.pluginProp,
))
}
@ -143,7 +144,7 @@ class OldMeetingMsgHdlrActor(val olgMsgGW: OldMessageReceivedGW)
def handleUserJoinedMeetingEvtMsg(msg: UserJoinedMeetingEvtMsg): Unit = {
olgMsgGW.handle(new UserJoined(msg.header.meetingId, msg.body.intId,
msg.body.extId, msg.body.name, msg.body.role, msg.body.locked, msg.body.avatar, msg.body.webcamBackground,
msg.body.guest, msg.body.guestStatus, msg.body.clientType))
msg.body.bot, msg.body.guest, msg.body.guestStatus, msg.body.clientType))
}
def handlePresenterUnassignedEvtMsg(msg: PresenterUnassignedEvtMsg): Unit = {

View File

@ -28,8 +28,8 @@ trait ToAkkaAppsSendersTrait extends SystemConfiguration {
val header = BbbCoreHeaderWithMeetingId(RegisterUserReqMsg.NAME, msg.meetingId)
val body = RegisterUserReqMsgBody(meetingId = msg.meetingId, intUserId = msg.intUserId,
name = msg.name, role = msg.role, extUserId = msg.extUserId, authToken = msg.authToken,
sessionToken = msg.sessionToken, avatarURL = msg.avatarURL, webcamBackgroundURL = msg.webcamBackgroundURL, guest = msg.guest, authed = msg.authed,
guestStatus = msg.guestStatus, excludeFromDashboard = msg.excludeFromDashboard,
sessionToken = msg.sessionToken, avatarURL = msg.avatarURL, webcamBackgroundURL = msg.webcamBackgroundURL, bot = msg.bot,
guest = msg.guest, authed = msg.authed, guestStatus = msg.guestStatus, excludeFromDashboard = msg.excludeFromDashboard,
enforceLayout = msg.enforceLayout, userMetadata = msg.userMetadata)
val req = RegisterUserReqMsg(header, body)
val message = BbbCommonEnvCoreMsg(envelope, req)

File diff suppressed because it is too large Load Diff

View File

@ -29,7 +29,7 @@
"@types/node": "^20.7.0",
"@types/redis": "^4.0.11",
"axios": "^1.7.4",
"express": "^5.0.0",
"express": "^5.0.1",
"redis": "^4.6.10"
},
"devDependencies": {

View File

@ -7,6 +7,7 @@ export default function buildRedisMessage(sessionVariables: Record<string, unkno
{name: 'chatId', type: 'string', required: true},
{name: 'messageId', type: 'string', required: true},
{name: 'reactionEmoji', type: 'string', required: true},
{name: 'reactionEmojiId', type: 'string', required: true},
]
)
@ -26,6 +27,7 @@ export default function buildRedisMessage(sessionVariables: Record<string, unkno
chatId: input.chatId,
messageId: input.messageId,
reactionEmoji: input.reactionEmoji,
reactionEmojiId: input.reactionEmojiId,
};
return { eventName, routing, header, body };

View File

@ -7,6 +7,7 @@ export default function buildRedisMessage(sessionVariables: Record<string, unkno
{name: 'chatId', type: 'string', required: true},
{name: 'messageId', type: 'string', required: true},
{name: 'reactionEmoji', type: 'string', required: true},
{name: 'reactionEmojiId', type: 'string', required: true},
]
)
@ -26,6 +27,7 @@ export default function buildRedisMessage(sessionVariables: Record<string, unkno
chatId: input.chatId,
messageId: input.messageId,
reactionEmoji: input.reactionEmoji,
reactionEmojiId: input.reactionEmojiId,
};
return { eventName, routing, header, body };

View File

@ -1,12 +1,11 @@
import { RedisMessage } from '../types';
import {throwErrorIfInvalidInput, throwErrorIfNotModerator} from "../imports/validation";
import { throwErrorIfInvalidInput } from "../imports/validation";
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
throwErrorIfNotModerator(sessionVariables);
throwErrorIfInvalidInput(input,
[
{name: 'recording', type: 'boolean', required: true},
]
[
{ name: 'recording', type: 'boolean', required: true },
]
)
const eventName = 'SetRecordingStatusCmdMsg';

File diff suppressed because it is too large Load Diff

View File

@ -40,6 +40,9 @@ func main() {
log.Infof("Json Patch Disabled!")
}
// Routine to check for idle connections and close them
go websrv.InvalidateIdleBrowserConnectionsRoutine()
// Websocket listener
rateLimiter := common.NewCustomRateLimiter(cfg.Server.MaxConnectionsPerSecond)

View File

@ -29,6 +29,7 @@ type Config struct {
JsonPatchDisabled bool `yaml:"json_patch_disabled"`
SubscriptionAllowedList string `yaml:"subscriptions_allowed_list"`
SubscriptionsDeniedList string `yaml:"subscriptions_denied_list"`
WebsocketIdleTimeoutSeconds int `yaml:"websocket_idle_timeout_seconds"`
} `yaml:"server"`
Redis struct {
Host string `yaml:"host"`

View File

@ -12,6 +12,7 @@ server:
json_patch_disabled: false
subscriptions_allowed_list:
subscriptions_denied_list:
websocket_idle_timeout_seconds: 60
redis:
host: 127.0.0.1
port: 6379

View File

@ -2,6 +2,7 @@ package common
import (
"sync"
"time"
)
var GlobalCacheLocks = NewCacheLocks()
@ -32,6 +33,17 @@ func (c *CacheLocks) Unlock(id uint32) {
c.mutex.Lock()
if mtx, exists := c.locks[id]; exists {
mtx.Unlock()
go c.RemoveLockId(id, 30)
}
c.mutex.Unlock()
}
func (c *CacheLocks) RemoveLockId(id uint32, delayInSecs time.Duration) {
time.Sleep(delayInSecs * time.Second)
c.mutex.Lock()
if _, exists := c.locks[id]; exists {
delete(c.locks, id)
}
c.mutex.Unlock()
}

View File

@ -45,6 +45,10 @@ func (s *SafeChannelByte) Closed() bool {
}
func (s *SafeChannelByte) Close() {
if s.Frozen() {
s.UnfreezeChannel()
}
s.mux.Lock()
defer s.mux.Unlock()

View File

@ -6,6 +6,7 @@ import (
"github.com/sirupsen/logrus"
"net/http"
"sync"
"time"
"nhooyr.io/websocket"
)
@ -56,6 +57,8 @@ type BrowserConnection struct {
FromBrowserToHasuraChannel *SafeChannelByte // channel to transmit messages from Browser to Hasura
FromBrowserToGqlActionsChannel *SafeChannelByte // channel to transmit messages from Browser to Graphq-Actions
FromHasuraToBrowserChannel *SafeChannelByte // channel to transmit messages from Hasura/GqlActions to Browser
LastBrowserMessageTime time.Time // stores the time of the last message to control browser idleness
LastBrowserMessageTimeMutex sync.RWMutex // mutex for LastBrowserMessageTime
Logger *logrus.Entry // connection logger populated with connection info
}

View File

@ -70,6 +70,7 @@ func HasuraClient(
defer func() {
//When Hasura sends an CloseError, it will forward the error to the browser and close the connection
if thisConnection.WebsocketCloseError != nil {
browserConnection.Logger.Infof("Closing browser connection because Hasura connection was closed, reason: %s", thisConnection.WebsocketCloseError.Reason)
browserConnection.Websocket.Close(thisConnection.WebsocketCloseError.Code, thisConnection.WebsocketCloseError.Reason)
browserConnection.ContextCancelFunc()
}

View File

@ -98,6 +98,7 @@ func ConnectionHandler(w http.ResponseWriter, r *http.Request) {
FromBrowserToHasuraChannel: common.NewSafeChannelByte(bufferSize),
FromBrowserToGqlActionsChannel: common.NewSafeChannelByte(bufferSize),
FromHasuraToBrowserChannel: common.NewSafeChannelByte(bufferSize),
LastBrowserMessageTime: time.Now(),
Logger: connectionLogger,
}
@ -494,3 +495,27 @@ func disconnectWithError(
return
}
var websocketIdleTimeoutSeconds = config.GetConfig().Server.WebsocketIdleTimeoutSeconds
func InvalidateIdleBrowserConnectionsRoutine() {
for {
time.Sleep(15 * time.Second)
BrowserConnectionsMutex.RLock()
for _, browserConnection := range BrowserConnections {
browserConnection.LastBrowserMessageTimeMutex.RLock()
browserIdleSince := time.Since(browserConnection.LastBrowserMessageTime)
browserConnection.LastBrowserMessageTimeMutex.RUnlock()
if browserIdleSince > time.Duration(websocketIdleTimeoutSeconds)*time.Second {
browserConnection.Logger.Info("Closing browser connection, reason: idle timeout")
errCloseWs := browserConnection.Websocket.Close(websocket.StatusNormalClosure, "idle timeout")
if errCloseWs != nil {
browserConnection.Logger.Debugf("Error on close websocket: %v", errCloseWs)
}
}
}
BrowserConnectionsMutex.RUnlock()
}
}

View File

@ -35,6 +35,7 @@ func BrowserConnectionReader(
for {
messageType, message, err := browserConnection.Websocket.Read(browserConnection.Context)
if err != nil {
if errors.Is(err, context.Canceled) {
browserConnection.Logger.Debugf("Closing Browser ws connection as Context was cancelled!")
@ -45,6 +46,9 @@ func BrowserConnectionReader(
}
browserConnection.Logger.Tracef("received from browser: %s", string(message))
browserConnection.LastBrowserMessageTimeMutex.Lock()
browserConnection.LastBrowserMessageTime = time.Now()
browserConnection.LastBrowserMessageTimeMutex.Unlock()
if messageType != websocket.MessageText {
browserConnection.Logger.Warnf("received non-text message: %v", messageType)

View File

@ -173,8 +173,8 @@ SELECT "meeting_usersPolicies"."meetingId",
create table "meeting_metadata" (
"meetingId" varchar(100) references "meeting"("meetingId") ON DELETE CASCADE,
"name" varchar(100),
"value" varchar(100),
"name" varchar(255),
"value" varchar(1000),
CONSTRAINT "meeting_metadata_pkey" PRIMARY KEY ("meetingId","name")
);
create index "idx_meeting_metadata_meetingId" on "meeting_metadata"("meetingId");
@ -272,10 +272,12 @@ CREATE TABLE "user" (
"authToken" varchar(16),
"authed" bool,
"joined" bool,
"firstJoinedAt" timestamp with time zone,
"joinErrorCode" varchar(50),
"joinErrorMessage" varchar(400),
"banned" bool,
"loggedOut" bool, -- when user clicked Leave meeting button
"bot" bool, -- used to flag au
"guest" bool, --used for dialIn
"guestStatus" varchar(50),
"registeredOn" bigint,
@ -315,6 +317,32 @@ create index "idx_user_pk_reverse" on "user" ("userId", "meetingId");
CREATE INDEX "idx_user_meetingId" ON "user"("meetingId");
CREATE INDEX "idx_user_extId" ON "user"("meetingId", "extId");
-- user (on update raiseHand or away: set new time)
CREATE OR REPLACE FUNCTION update_user_raiseHand_away_time_trigger_func()
RETURNS TRIGGER AS $$
BEGIN
IF NEW."raiseHand" IS DISTINCT FROM OLD."raiseHand" THEN
IF NEW."raiseHand" is false THEN
NEW."raiseHandTime" := NULL;
ELSE
NEW."raiseHandTime" := NOW();
END IF;
END IF;
IF NEW."away" IS DISTINCT FROM OLD."away" THEN
IF NEW."away" is false THEN
NEW."awayTime" := NULL;
ELSE
NEW."awayTime" := NOW();
END IF;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_user_raiseHand_away_time_trigger BEFORE UPDATE OF "raiseHand", "away" ON "user"
FOR EACH ROW EXECUTE FUNCTION update_user_raiseHand_away_time_trigger_func();
--hasDrawPermissionOnCurrentPage is necessary to improve the performance of the order by of userlist
COMMENT ON COLUMN "user"."hasDrawPermissionOnCurrentPage" IS 'This column is dynamically populated by triggers of tables: user, pres_presentation, pres_page, pres_page_writers';
COMMENT ON COLUMN "user"."disconnected" IS 'This column is set true when the user closes the window or his with the server is over';
@ -329,6 +357,28 @@ ALTER TABLE "user" ADD COLUMN "isDenied" boolean GENERATED ALWAYS AS ("guestStat
ALTER TABLE "user" ADD COLUMN "registeredAt" timestamp with time zone GENERATED ALWAYS AS (to_timestamp("registeredOn"::double precision / 1000)) STORED;
--Populate column `firstJoinedAt` to register if the user has joined in the meeting (once column `joined` turn false when user leaves)
CREATE OR REPLACE FUNCTION "set_user_firstJoinedAt_trigger_func"()
RETURNS TRIGGER AS $$
BEGIN
IF NEW."joined" is true AND NEW."firstJoinedAt" IS NULL THEN
NEW."firstJoinedAt" := NOW();
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_user_firstJoinedAt_ins_trigger"
BEFORE INSERT ON "user"
FOR EACH ROW
EXECUTE FUNCTION "set_user_firstJoinedAt_trigger_func"();
CREATE TRIGGER "set_user_firstJoinedAt_upd_trigger"
BEFORE UPDATE ON "user"
FOR EACH ROW
WHEN (OLD."joined" IS DISTINCT FROM NEW."joined")
EXECUTE FUNCTION "set_user_firstJoinedAt_trigger_func"();
--Used to sort the Userlist
ALTER TABLE "user" ADD COLUMN "nameSortable" varchar(255) GENERATED ALWAYS AS (trim(remove_emojis(immutable_lower_unaccent("name")))) STORED;
@ -359,6 +409,7 @@ AS SELECT "user"."userId",
"user"."raiseHandTime",
"user"."reactionEmoji",
"user"."reactionEmojiTime",
"user"."bot",
"user"."guest",
"user"."guestStatus",
"user"."mobile",
@ -506,11 +557,22 @@ AS SELECT
"user"."currentlyInMeeting"
FROM "user";
--Provide users that have joined in the meeting, either who is currently in meeting or has left
CREATE OR REPLACE VIEW "v_user_presenceLog"
AS SELECT
"user"."meetingId",
"user"."userId",
"user"."extId",
CASE WHEN "user"."role" = 'MODERATOR' THEN true ELSE false END "isModerator",
"user"."currentlyInMeeting"
FROM "user"
where "firstJoinedAt" is not null;
create table "user_metadata"(
"meetingId" varchar(100),
"userId" varchar(50),
"parameter" varchar(255),
"value" varchar(255),
"value" varchar(1000),
CONSTRAINT "user_metadata_pkey" PRIMARY KEY ("meetingId", "userId","parameter"),
FOREIGN KEY ("meetingId", "userId") REFERENCES "user"("meetingId","userId") ON DELETE CASCADE
);
@ -1140,6 +1202,7 @@ CREATE TABLE "chat_message_reaction" (
"messageId" varchar(100) REFERENCES "chat_message"("messageId") ON DELETE CASCADE,
"userId" varchar(100) not null,
"reactionEmoji" varchar(25),
"reactionEmojiId" varchar(50),
"createdAt" timestamp with time zone,
CONSTRAINT chat_message_reaction_pk PRIMARY KEY ("messageId", "userId", "reactionEmoji"),
FOREIGN KEY ("meetingId", "userId") REFERENCES "user"("meetingId","userId") ON DELETE CASCADE
@ -1561,6 +1624,7 @@ FROM poll
JOIN v_user u ON u."meetingId" = poll."meetingId" AND "isDialIn" IS FALSE AND presenter IS FALSE
LEFT JOIN poll_response r ON r."pollId" = poll."pollId" AND r."userId" = u."userId"
LEFT JOIN poll_option o ON o."pollId" = r."pollId" AND o."optionId" = r."optionId"
WHERE u."bot" IS FALSE
GROUP BY poll."pollId", u."meetingId", u."userId";
CREATE VIEW "v_poll" AS SELECT * FROM "poll";
@ -2063,6 +2127,18 @@ and n."createdAt" > current_timestamp - '5 seconds'::interval;
create index idx_notification on notification("meetingId","userId","role","createdAt");
-- ========== Plugin tables
create table "plugin" (
"meetingId" varchar(100),
"name" varchar(100),
"javascriptEntrypointUrl" varchar(500),
"javascriptEntrypointIntegrity" varchar(500),
CONSTRAINT "plugin_pk" PRIMARY KEY ("meetingId","name"),
FOREIGN KEY ("meetingId") REFERENCES "meeting"("meetingId") ON DELETE CASCADE
);
create view "v_plugin" as select * from "plugin";
--------------------------------
---Plugins Data Channel

View File

@ -114,6 +114,7 @@ type Mutation {
chatId: String!
messageId: String!
reactionEmoji: String!
reactionEmojiId: String!
): Boolean
}
@ -149,6 +150,7 @@ type Mutation {
chatId: String!
messageId: String!
reactionEmoji: String!
reactionEmojiId: String!
): Boolean
}

View File

@ -23,6 +23,7 @@ select_permissions:
columns:
- createdAt
- reactionEmoji
- reactionEmojiId
- userId
filter:
meetingId:

View File

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

View File

@ -88,6 +88,7 @@ select_permissions:
- disconnected
- expired
- extId
- bot
- guest
- guestStatus
- hasDrawPermissionOnCurrentPage

View File

@ -0,0 +1,26 @@
table:
name: v_user_presenceLog
schema: public
configuration:
column_config: {}
custom_column_names: {}
custom_name: user_presenceLog
custom_root_fields: {}
select_permissions:
- role: bbb_client
permission:
columns:
- currentlyInMeeting
- extId
- isModerator
- userId
filter:
_and:
- meetingId:
_eq: X-Hasura-MeetingId
- _or:
- isModerator:
_eq: true
- meetingId:
_eq: X-Hasura-UserListNotLockedInMeeting
comment: ""

View File

@ -26,6 +26,7 @@
- "!include public_v_meeting_usersPolicies.yaml"
- "!include public_v_meeting_voiceSettings.yaml"
- "!include public_v_notification.yaml"
- "!include public_v_plugin.yaml"
- "!include public_v_pluginDataChannelEntry.yaml"
- "!include public_v_poll.yaml"
- "!include public_v_poll_option.yaml"
@ -55,6 +56,7 @@
- "!include public_v_user_guest.yaml"
- "!include public_v_user_lockSettings.yaml"
- "!include public_v_user_metadata.yaml"
- "!include public_v_user_presenceLog.yaml"
- "!include public_v_user_reaction.yaml"
- "!include public_v_user_ref.yaml"
- "!include public_v_user_transcriptionError.yaml"

View File

@ -5393,9 +5393,9 @@
}
},
"node_modules/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"engines": {
"node": ">= 0.6"
}
@ -7013,16 +7013,16 @@
}
},
"node_modules/express": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.6.0",
"cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
@ -8039,8 +8039,9 @@
}
},
"node_modules/http-proxy-middleware": {
"version": "2.0.6",
"license": "MIT",
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz",
"integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==",
"dependencies": {
"@types/http-proxy": "^1.17.8",
"http-proxy": "^1.18.1",
@ -19150,9 +19151,9 @@
}
},
"cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="
},
"cookie-signature": {
"version": "1.0.6"
@ -20131,16 +20132,16 @@
}
},
"express": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"requires": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.6.0",
"cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
@ -20773,7 +20774,9 @@
}
},
"http-proxy-middleware": {
"version": "2.0.6",
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz",
"integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==",
"requires": {
"@types/http-proxy": "^1.17.8",
"http-proxy": "^1.18.1",

View File

@ -25,6 +25,8 @@ const TABS = {
TIMELINE: 2,
POLLING: 3,
};
const LEARNING_DASHBOARD_LEARN_MORE_LINK = 'learning-dashboard-learn-more-link';
const LEARNING_DASHBOARD_FEEDBACK_LINK = 'learning-dashboard-feedback-link';
class App extends React.Component {
constructor(props) {
@ -168,12 +170,28 @@ class App extends React.Component {
learningDashboardAccessToken, meetingId, sessionToken, invalidSessionCount,
} = this.state;
// adjust user sessions to be compatible with old json
const convertUserUsessionsFormat = (activitiesJson) => {
const newActivivies = activitiesJson;
Object.values(newActivivies.users).forEach((user) => {
Object.values(user.intIds).forEach((intId) => {
if (!intId?.sessions && intId?.registeredOn) {
const newIntId = intId;
newIntId.sessions = [
{ registeredOn: intId.registeredOn, leftOn: intId.leftOn },
];
}
});
});
return newActivivies;
};
if (learningDashboardAccessToken !== '') {
fetch(`${meetingId}/${learningDashboardAccessToken}/learning_dashboard_data.json`)
.then((response) => response.json())
.then((json) => {
this.setState({
activitiesJson: json,
activitiesJson: convertUserUsessionsFormat(json),
loading: false,
invalidSessionCount: 0,
lastUpdated: Date.now(),
@ -254,13 +272,17 @@ class App extends React.Component {
]), []);
const minTime = Object.values(usersTimes || {}).reduce((prevVal, elem) => {
if (prevVal === 0 || elem.registeredOn < prevVal) return elem.registeredOn;
if (prevVal === 0 || elem.sessions[0].registeredOn < prevVal) {
return elem.sessions[0].registeredOn;
}
return prevVal;
}, 0);
const maxTime = Object.values(usersTimes || {}).reduce((prevVal, elem) => {
if (elem.leftOn === 0) return (new Date()).getTime();
if (elem.leftOn > prevVal) return elem.leftOn;
if (elem.sessions[elem.sessions.length - 1].leftOn === 0) return (new Date()).getTime();
if (elem.sessions[elem.sessions.length - 1].leftOn > prevVal) {
return elem.sessions[elem.sessions.length - 1].leftOn;
}
return prevVal;
}, 0);
@ -341,7 +363,7 @@ class App extends React.Component {
return (
<div className="mx-10">
<div className="flex flex-col sm:flex-row items-start justify-between pb-3">
<h1 className="mt-3 text-2xl font-semibold whitespace-nowrap inline-block">
<h1 className="mt-3 text-2xl font-semibold inline-block">
<FormattedMessage id="app.learningDashboard.dashboardTitle" defaultMessage="Learning Dashboard" />
{
ldAccessTokenCopied === true
@ -353,6 +375,27 @@ class App extends React.Component {
: null
}
<br />
{ activitiesJson?.other
&& activitiesJson.other[LEARNING_DASHBOARD_LEARN_MORE_LINK] !== ''
&& (
<>
<span className="text-sm font-light font-base mt-0">
{intl.formatMessage({ id: 'app.learningDashboard.learnMore', defaultMessage: 'Learn more about the use of the Dashboard in {0} from our Knowledge Base.' }, {
0: (
<a
target="_blank"
rel="noreferrer"
href={activitiesJson.other[LEARNING_DASHBOARD_LEARN_MORE_LINK]}
className="underline"
>
{intl.formatMessage({ id: 'app.learningDashboard.learnMoreLinkText', defaultMessage: 'this article' })}
</a>
),
})}
</span>
<br />
</>
)}
<span className="text-sm font-medium">{activitiesJson.name || ''}</span>
</h1>
<div className="mt-3 col-text-right py-1 text-gray-500 inline-block">
@ -609,6 +652,27 @@ class App extends React.Component {
</TabsUnstyled>
<UserDetails dataJson={activitiesJson} />
<hr className="my-8" />
{ activitiesJson?.other
&& activitiesJson.other[LEARNING_DASHBOARD_FEEDBACK_LINK] !== ''
&& (
<>
<div className="mt-6 mb-4 text-sm font-light font-base text-gray-500">
{ intl.formatMessage({ id: 'app.learningDashboard.feedback', defaultMessage: 'How has your experience been with this feature? We would love to hear your opinion and even suggestions on how we can improve it. Share with us by clicking {0}.' }, {
0: (
<a
target="_blank"
rel="noreferrer"
href={activitiesJson.other[LEARNING_DASHBOARD_FEEDBACK_LINK]}
className="underline"
>
{intl.formatMessage({ id: 'app.learningDashboard.feedbackLinkText', defaultMessage: 'here' })}
</a>
),
})}
</div>
<hr className="mb-8" />
</>
)}
<div className="flex justify-between pb-8 text-xs text-gray-800 dark:text-gray-400 whitespace-nowrap flex-col sm:flex-row">
<div className="flex flex-col justify-center mb-4 sm:mb-0">
<p className="text-gray-700">

View File

@ -66,29 +66,39 @@ class StatusTable extends React.Component {
Object.values(allUsers || {}).forEach((user) => {
usersPeriods[user.userKey] = [];
Object.values(user.intIds || {}).forEach((intId, index, intIdsArray) => {
let { leftOn } = intId;
const nextPeriod = intIdsArray[index + 1];
if (nextPeriod && Math.abs(leftOn - nextPeriod.registeredOn) <= 30000) {
leftOn = nextPeriod.leftOn;
intIdsArray.splice(index + 1, 1);
}
usersPeriods[user.userKey].push({
registeredOn: intId.registeredOn,
leftOn,
intId.sessions.forEach((session, sessionIndex, sessionArray) => {
let { leftOn } = session;
const nextSession = sessionArray[sessionIndex + 1];
if (nextSession && Math.abs(leftOn - nextSession.registeredOn) <= 30000) {
leftOn = nextSession.leftOn;
sessionArray.splice(sessionIndex + 1, 1);
}
if (!nextSession) {
const nextPeriod = intIdsArray[index + 1];
if (nextPeriod && Math.abs(leftOn - nextPeriod.sessions[0].registeredOn) <= 30000) {
leftOn = nextPeriod.sessions[0].leftOn;
intIdsArray.splice(index + 1, 1);
}
}
usersPeriods[user.userKey].push({
registeredOn: session.registeredOn,
leftOn,
});
});
});
});
const usersRegisteredTimes = Object
.values(allUsers || {})
.map((user) => Object.values(user.intIds).map((intId) => intId.registeredOn))
.map((user) => Object.values(user.intIds)
.map((intId) => intId.sessions.map((session) => session.registeredOn)).flat())
.flat();
const usersLeftTimes = Object
.values(allUsers || {})
.map((user) => Object.values(user.intIds).map((intId) => {
if (intId.leftOn === 0) return (new Date()).getTime();
return intId.leftOn;
}))
.map((user) => Object.values(user.intIds).map((intId) => intId.sessions.map((session) => {
if (session.leftOn === 0) return (new Date()).getTime();
return session.leftOn;
})).flat())
.flat();
const firstRegisteredOnTime = Math.min(...usersRegisteredTimes);

View File

@ -53,11 +53,14 @@ const UserDatailsComponent = (props) => {
const currTime = () => new Date().getTime();
// Join and left times.
const registeredTimes = Object.values(user.intIds).map((intId) => intId.registeredOn);
const leftTimes = Object.values(user.intIds).map((intId) => intId.leftOn);
const registeredTimes = Object.values(user.intIds)
.map((intId) => intId.sessions.map((session) => session.registeredOn)).flat();
const leftTimes = Object.values(user.intIds)
.map((intId) => intId.sessions.map((session) => session.leftOn)).flat();
const joinTime = Math.min(...registeredTimes);
const leftTime = Math.max(...leftTimes);
const currentlyInMeeting = Object.values(user.intIds).some((intId) => intId.leftOn === 0);
const currentlyInMeeting = Object.values(user.intIds)
.some((intId) => intId.sessions.some((session) => session.leftOn === 0));
// Used in the calculation of the online loader.
const sessionDuration = (endedOn || currTime()) - createdOn;

View File

@ -100,15 +100,17 @@ class UsersTable extends React.Component {
},
onlineTimeOrder(a, b) {
const onlineTimeA = Object.values(a.intIds).reduce((prev, intId) => (
prev + ((intId.leftOn > 0
? intId.leftOn
: (new Date()).getTime()) - intId.registeredOn)
prev + intId.sessions.reduce((prev2, session) => (
prev2 + (session.leftOn > 0
? session.leftOn
: (new Date()).getTime()) - session.registeredOn), 0)
), 0);
const onlineTimeB = Object.values(b.intIds).reduce((prev, intId) => (
prev + ((intId.leftOn > 0
? intId.leftOn
: (new Date()).getTime()) - intId.registeredOn)
prev + intId.sessions.reduce((prev2, session) => (
prev2 + (session.leftOn > 0
? session.leftOn
: (new Date()).getTime()) - session.registeredOn), 0)
), 0);
if (onlineTimeA < onlineTimeB) {
@ -251,68 +253,70 @@ class UsersTable extends React.Component {
>
{user.name}
</button>
{ Object.values(user.intIds || {}).map((intId, index) => (
<>
<p className="text-xs text-gray-700 dark:text-gray-400">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
/>
</svg>
<FormattedDate
value={intId.registeredOn}
month="short"
day="numeric"
hour="2-digit"
minute="2-digit"
second="2-digit"
/>
</p>
{ intId.leftOn > 0
? (
<p className="text-xs text-gray-700 dark:text-gray-400">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
<FormattedDate
value={intId.leftOn}
month="short"
day="numeric"
hour="2-digit"
minute="2-digit"
second="2-digit"
{ Object.values(user.intIds || {}).map((intId, index) => intId.sessions
.map((session, sessionIndex) => (
<>
<p className="text-xs text-gray-700 dark:text-gray-400">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
/>
</p>
)
: null }
{ index === Object.values(user.intIds).length - 1
? null
: (
<hr className="my-1" />
) }
</>
)) }
</svg>
<FormattedDate
value={session.registeredOn}
month="short"
day="numeric"
hour="2-digit"
minute="2-digit"
second="2-digit"
/>
</p>
{ session.leftOn > 0
? (
<p className="text-xs text-gray-700 dark:text-gray-400">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
<FormattedDate
value={session.leftOn}
month="short"
day="numeric"
hour="2-digit"
minute="2-digit"
second="2-digit"
/>
</p>
)
: null }
{ index === Object.values(user.intIds).length - 1
&& sessionIndex === intId?.sessions.length - 1
? null
: (
<hr className="my-1" />
) }
</>
))) }
</div>
</td>
<td className={`px-4 py-3 text-sm text-center items-center ${opacity}`} data-test="userOnlineTimeDashboard">
@ -332,16 +336,17 @@ class UsersTable extends React.Component {
</svg>
&nbsp;
{ tsToHHmmss(Object.values(user.intIds).reduce((prev, intId) => (
prev + ((intId.leftOn > 0
? intId.leftOn
: (new Date()).getTime()) - intId.registeredOn)
), 0)) }
prev + intId.sessions.reduce((prev2, session) => ((session.leftOn > 0
? prev2 + session.leftOn
: prev2 + (new Date()).getTime()) - session.registeredOn), 0)), 0)) }
<br />
{
(function getPercentage() {
const { intIds } = user;
const percentage = Object.values(intIds || {}).reduce((prev, intId) => (
prev + getOnlinePercentage(intId.registeredOn, intId.leftOn)
prev + intId.sessions.reduce((prev2, session) => (
prev2 + getOnlinePercentage(session.registeredOn, session.leftOn)
), 0)
), 0);
return (
@ -475,7 +480,8 @@ class UsersTable extends React.Component {
}
<td className="px-3.5 2xl:px-4 py-3 text-xs text-center" data-test="userStatusDashboard">
{
Object.values(user.intIds)[Object.values(user.intIds).length - 1].leftOn > 0
Object.values(user.intIds)[Object.values(user.intIds).length - 1]
.sessions.slice(-1)[0].leftOn > 0
? (
<span className="px-2 py-1 font-semibold leading-tight text-red-700 bg-red-100 rounded-full">
<FormattedMessage id="app.learningDashboard.usersTable.userStatusOffline" defaultMessage="Offline" />

View File

@ -7,6 +7,24 @@ import { UserDetailsProvider } from './components/UserDetails/context';
const RTL_LANGUAGES = ['ar', 'dv', 'fa', 'he'];
function isValidLocale(locale) {
try {
const intl = new Intl.Locale(locale);
return !!intl;
} catch (error) {
return false;
}
}
function normalizeLocale(lang) {
const lParts = lang.split('-');
// 'pt' returns 'pt'
// 'pt-br' and 'pt-BR' return 'pt_BR'
// 'pt_BR' returns 'pt_BR'
return lParts.length > 1 ? `${lParts[0]}_${lParts[1].toUpperCase()}` : lang;
}
function getLanguage() {
let { language } = navigator;
@ -34,7 +52,7 @@ class Dashboard extends React.Component {
setMessages() {
const fetchMessages = (lang) => new Promise((resolve, reject) => {
const url = `/html5client/locales/${lang.replace('-', '_')}.json`;
const url = `/html5client/locales/${normalizeLocale(lang)}.json`;
fetch(url).then((response) => {
if (!response.ok) return reject();
return resolve(response.json());
@ -70,9 +88,11 @@ class Dashboard extends React.Component {
render() {
const { intlLocale, intlMessages } = this.state;
const locale = isValidLocale(intlLocale) ? intlLocale : undefined;
return (
<UserDetailsProvider>
<IntlProvider defaultLocale="en" locale={intlLocale} messages={intlMessages}>
<IntlProvider defaultLocale="en" locale={locale} messages={intlMessages}>
<App />
</IntlProvider>
</UserDetailsProvider>

View File

@ -46,6 +46,14 @@ export function getActivityScore(user, allUsers, totalOfPolls) {
export function getSumOfTime(eventsArr) {
return eventsArr.reduce((prevVal, elem) => {
if (elem?.sessions) {
return prevVal + elem.sessions.reduce((prevVal2, session) => {
if (session.leftOn > 0) {
return prevVal2 + (session.leftOn - session.registeredOn);
}
return prevVal2 + (new Date().getTime() - session.registeredOn);
}, 0);
}
if ((elem.stoppedOn || elem.leftOn) > 0) {
return prevVal + ((elem.stoppedOn || elem.leftOn) - (elem.startedOn || elem.registeredOn));
}
@ -55,8 +63,8 @@ export function getSumOfTime(eventsArr) {
export function getJoinTime(eventsArr) {
return eventsArr.reduce((prevVal, elem) => {
if (prevVal === 0 || elem.registeredOn < prevVal) {
return elem.registeredOn;
if (prevVal === 0 || elem.sessions[0].registeredOn < prevVal) {
return elem.sessions[0].registeredOn;
}
return prevVal;
}, 0);
@ -64,8 +72,8 @@ export function getJoinTime(eventsArr) {
export function getLeaveTime(eventsArr) {
return eventsArr.reduce((prevVal, elem) => {
if (elem.leftOn > prevVal) {
return elem.leftOn;
if (elem.sessions[elem.sessions.length - 1].leftOn > prevVal) {
return elem.sessions[elem.sessions.length - 1].leftOn;
}
return prevVal;
}, 0);
@ -203,8 +211,8 @@ export function makeUserCSVData(users, polls, intl) {
}
for (let i = 0; i < pollValues.length; i += 1) {
// Add the poll question headers
header += `,${pollValues[i].question || `Poll ${i + 1}`}`;
// Add the poll question headers (remove spaces and line breaks)
header += `,${pollValues[i].question.replace(/\s+/g, ' ').trim() || `Poll ${i + 1}`}`;
// Add the anonymous answers
anonymousRecord += `,"${pollValues[i].anonymousAnswers.join('\r\n')}"`;

View File

@ -1 +1 @@
BIGBLUEBUTTON_RELEASE=3.0.0-beta.3
BIGBLUEBUTTON_RELEASE=3.0.0-beta.4

View File

@ -3,17 +3,15 @@ import ConnectionManager from '/imports/ui/components/connection-manager/compone
import { createRoot } from 'react-dom/client';
import SettingsLoader from '/imports/ui/components/settings-loader/component';
import ErrorBoundary from '/imports/ui/components/common/error-boundary/component';
import { ErrorScreen } from '/imports/ui/components/error-screen/component';
import ErrorScreen from '/imports/ui/components/error-screen/component';
import PresenceManager from '/imports/ui/components/join-handler/presenceManager/component';
import LoadingScreenHOC from '/imports/ui/components/common/loading-screen/loading-screen-HOC/component';
import IntlLoaderContainer from '/imports/startup/client/intlLoader';
import LocatedErrorBoundary from '/imports/ui/components/common/error-boundary/located-error-boundary/component';
import CustomUsersSettings from '/imports/ui/components/join-handler/custom-users-settings/component';
import MeetingClient from '/client/meetingClient';
import CustomStyles from '/imports/ui/components/custom-styles/component';
const STARTUP_CRASH_METADATA = { logCode: 'app_startup_crash', logMessage: 'Possible startup crash' };
const APP_CRASH_METADATA = { logCode: 'app_crash', logMessage: 'Possible app crash' };
/* eslint-disable */
if (
process.env.NODE_ENV === 'production'
@ -40,30 +38,23 @@ const Main: React.FC = () => {
return (
<SettingsLoader>
<CustomUsersSettings>
<CustomStyles>
<ErrorBoundary
Fallback={ErrorScreen}
logMetadata={STARTUP_CRASH_METADATA}
isCritical
>
<LoadingScreenHOC>
<IntlLoaderContainer>
{/* from there the error messages are located */}
<LocatedErrorBoundary
Fallback={ErrorScreen}
logMetadata={APP_CRASH_METADATA}
isCritical
>
<ConnectionManager>
<PresenceManager>
<MeetingClient />
</PresenceManager>
</ConnectionManager>
</LocatedErrorBoundary>
</IntlLoaderContainer>
</LoadingScreenHOC>
</ErrorBoundary>
</CustomStyles>
<IntlLoaderContainer>
<CustomStyles>
<ErrorBoundary
Fallback={ErrorScreen}
logMetadata={STARTUP_CRASH_METADATA}
isCritical
>
<LoadingScreenHOC>
<ConnectionManager>
<PresenceManager>
<MeetingClient />
</PresenceManager>
</ConnectionManager>
</LoadingScreenHOC>
</ErrorBoundary>
</CustomStyles>
</IntlLoaderContainer>
</CustomUsersSettings>
</SettingsLoader>
);

View File

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

View File

@ -362,7 +362,7 @@ export default class SFUAudioBridge extends BaseAudioBridge {
const handleInitError = (_error) => {
mapErrorCode(_error);
if (RETRYABLE_ERRORS.includes(_error?.errorCode)
if (!RETRYABLE_ERRORS.includes(_error?.errorCode)
|| !RETRY_THROUGH_RELAY
|| this.reconnecting) {
reject(_error);

Some files were not shown because too many files have changed in this diff Show More