Merge remote-tracking branch 'bbb/v3.0.x-release' into oct-25
This commit is contained in:
commit
cac5d72a27
79
.github/workflows/automated-tests.yml
vendored
79
.github/workflows/automated-tests.yml
vendored
@ -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: |
|
||||
|
@ -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")
|
||||
)
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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(())
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package org.bigbluebutton.core.db
|
||||
import org.bigbluebutton.common2.domain.DefaultProps
|
||||
import PostgresProfile.api._
|
||||
import org.bigbluebutton.core.apps.groupchats.GroupChatApp
|
||||
import org.bigbluebutton.core.models.PluginModel
|
||||
|
||||
case class MeetingSystemColumnsDbModel(
|
||||
loginUrl: Option[String],
|
||||
@ -85,7 +86,7 @@ class MeetingDbTableDef(tag: Tag) extends Table[MeetingDbModel](tag, None, "meet
|
||||
}
|
||||
|
||||
object MeetingDAO {
|
||||
def insert(meetingProps: DefaultProps, clientSettings: Map[String, Object]) = {
|
||||
def insert(meetingProps: DefaultProps, clientSettings: Map[String, Object], pluginProps: PluginModel) = {
|
||||
DatabaseConnection.enqueue(
|
||||
TableQuery[MeetingDbTableDef].forceInsert(
|
||||
MeetingDbModel(
|
||||
@ -148,6 +149,7 @@ object MeetingDAO {
|
||||
MeetingBreakoutDAO.insert(meetingProps.meetingProp.intId, meetingProps.breakoutProps)
|
||||
LayoutDAO.insert(meetingProps.meetingProp.intId, meetingProps.usersProp.meetingLayout)
|
||||
MeetingClientSettingsDAO.insert(meetingProps.meetingProp.intId, JsonUtils.mapToJson(clientSettings))
|
||||
PluginModel.persistPluginsForClient(pluginProps, meetingProps.meetingProp.intId)
|
||||
}
|
||||
|
||||
def updateMeetingDurationByParentMeeting(parentMeetingId: String, newDurationInSeconds: Int) = {
|
||||
|
@ -0,0 +1,33 @@
|
||||
package org.bigbluebutton.core.db
|
||||
|
||||
import PostgresProfile.api._
|
||||
|
||||
case class PluginDbModel(
|
||||
meetingId: String,
|
||||
name: String,
|
||||
javascriptEntrypointUrl: String,
|
||||
javascriptEntrypointIntegrity: String
|
||||
)
|
||||
|
||||
class PluginDbTableDef(tag: Tag) extends Table[PluginDbModel](tag, None, "plugin") {
|
||||
val meetingId = column[String]("meetingId", O.PrimaryKey)
|
||||
val name = column[String]("name", O.PrimaryKey)
|
||||
val javascriptEntrypointUrl = column[String]("javascriptEntrypointUrl")
|
||||
val javascriptEntrypointIntegrity = column[String]("javascriptEntrypointIntegrity")
|
||||
override def * = (meetingId, name, javascriptEntrypointUrl, javascriptEntrypointIntegrity) <> (PluginDbModel.tupled, PluginDbModel.unapply)
|
||||
}
|
||||
|
||||
object PluginDAO {
|
||||
def insert(meetingId: String, name: String, javascriptEntrypointUrl: String, javascriptEntrypointIntegrity: String) = {
|
||||
DatabaseConnection.enqueue(
|
||||
TableQuery[PluginDbTableDef].forceInsert(
|
||||
PluginDbModel(
|
||||
meetingId = meetingId,
|
||||
name = name,
|
||||
javascriptEntrypointUrl = javascriptEntrypointUrl,
|
||||
javascriptEntrypointIntegrity = javascriptEntrypointIntegrity,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -27,4 +27,5 @@ class LiveMeeting(
|
||||
val users2x: Users2x,
|
||||
val guestsWaiting: GuestsWaiting,
|
||||
val clientSettings: Map[String, Object],
|
||||
val plugins: PluginModel,
|
||||
)
|
||||
|
@ -172,7 +172,7 @@ class MeetingActor(
|
||||
outGW.send(msgEvent)
|
||||
|
||||
//Insert meeting into the database
|
||||
MeetingDAO.insert(liveMeeting.props, liveMeeting.clientSettings)
|
||||
MeetingDAO.insert(liveMeeting.props, liveMeeting.clientSettings, liveMeeting.plugins)
|
||||
|
||||
// Create a default public group chat
|
||||
state = groupChatApp.handleCreateDefaultPublicGroupChat(state, liveMeeting, msgBus)
|
||||
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
))
|
||||
))
|
||||
|
@ -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))
|
||||
|
@ -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,
|
||||
|
@ -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],
|
||||
)
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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" }
|
||||
|
@ -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,
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -368,6 +368,7 @@ public class SvgImageCreatorImp implements SvgImageCreator {
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void setBlankSvg(String blankSvg) {
|
||||
BLANK_SVG = blankSvg;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 = {
|
||||
|
@ -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)
|
||||
|
464
bbb-graphql-actions/package-lock.json
generated
464
bbb-graphql-actions/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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": {
|
||||
|
@ -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 };
|
||||
|
@ -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 };
|
||||
|
@ -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';
|
||||
|
3072
bbb-graphql-client-test/package-lock.json
generated
3072
bbb-graphql-client-test/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
|
@ -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"`
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,7 @@ select_permissions:
|
||||
columns:
|
||||
- createdAt
|
||||
- reactionEmoji
|
||||
- reactionEmojiId
|
||||
- userId
|
||||
filter:
|
||||
meetingId:
|
||||
|
@ -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: ""
|
@ -88,6 +88,7 @@ select_permissions:
|
||||
- disconnected
|
||||
- expired
|
||||
- extId
|
||||
- bot
|
||||
- guest
|
||||
- guestStatus
|
||||
- hasDrawPermissionOnCurrentPage
|
||||
|
@ -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: ""
|
@ -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"
|
||||
|
37
bbb-learning-dashboard/package-lock.json
generated
37
bbb-learning-dashboard/package-lock.json
generated
@ -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",
|
||||
|
@ -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">
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
||||
{ 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" />
|
||||
|
@ -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>
|
||||
|
@ -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')}"`;
|
||||
|
@ -1 +1 @@
|
||||
BIGBLUEBUTTON_RELEASE=3.0.0-beta.3
|
||||
BIGBLUEBUTTON_RELEASE=3.0.0-beta.4
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user