feature: Override client settings through API /create call (#18782)
* akka-with-client-configs * [akka-with-client-configs] - inserted client configs in akka * [issue-18588-create-override] - WIP * [akka-with-client-configs] - Remove unnecessary code * [issue-18588] - test some thesis * [akka-with-client-configs] - refactor to add jackson and immutable.Map * [issue-18588-create-override] - new architecture for overriding client configs] * [issue-18588-create-override] - implemented settings * Refactor on clientSettingsOverride module and add allowOverrideClientSettingsOnCreateCall conf --------- Co-authored-by: Gustavo Trott <gustavo@trott.com.br>
This commit is contained in:
parent
ab3c57b858
commit
4ef078ccf5
@ -37,6 +37,7 @@ object Dependencies {
|
||||
val scalaTest = "3.2.11"
|
||||
val mockito = "2.23.0"
|
||||
val akkaTestKit = "2.6.0"
|
||||
val jacksonDataFormat = "2.13.5"
|
||||
}
|
||||
|
||||
object Compile {
|
||||
@ -65,6 +66,8 @@ object Dependencies {
|
||||
val slickHikaricp = "com.typesafe.slick" %% "slick-hikaricp" % Versions.slick
|
||||
val slickPg = "com.github.tminglei" %% "slick-pg" % Versions.slickPg
|
||||
val postgresql = "org.postgresql" % "postgresql" % Versions.postgresql
|
||||
val jacksonDataFormat = "com.fasterxml.jackson.dataformat" % "jackson-dataformat-yaml" % Versions.jacksonDataFormat
|
||||
val snakeYaml = "org.yaml" % "snakeyaml"
|
||||
}
|
||||
|
||||
object Test {
|
||||
@ -101,5 +104,6 @@ object Dependencies {
|
||||
Compile.slick,
|
||||
Compile.slickHikaricp,
|
||||
Compile.slickPg,
|
||||
Compile.postgresql) ++ testing
|
||||
Compile.postgresql,
|
||||
Compile.jacksonDataFormat) ++ testing
|
||||
}
|
||||
|
@ -1,23 +1,17 @@
|
||||
package org.bigbluebutton
|
||||
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import org.bigbluebutton.common2.util.YamlUtil
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
import java.io.{ ByteArrayInputStream, File }
|
||||
import scala.io.BufferedSource
|
||||
import scala.util.{ Failure, Success, Try }
|
||||
|
||||
object ClientSettings {
|
||||
val config = ConfigFactory.load()
|
||||
object ClientSettings extends SystemConfiguration {
|
||||
var clientSettingsFromFile: Map[String, Object] = Map("" -> "")
|
||||
val logger = LoggerFactory.getLogger(this.getClass)
|
||||
|
||||
def loadClientSettingsFromFile() = {
|
||||
lazy val clientSettingsPath = Try(config.getString("client.clientSettingsFilePath")).getOrElse(
|
||||
"/usr/share/meteor/bundle/programs/server/assets/app/config/settings.yml"
|
||||
)
|
||||
lazy val clientSettingsPathOverride = Try(config.getString("client.clientSettingsOverrideFilePath")).getOrElse(
|
||||
"/etc/bigbluebutton/bbb-html5.yml"
|
||||
)
|
||||
|
||||
val clientSettingsFile = scala.io.Source.fromFile(clientSettingsPath, "UTF-8")
|
||||
|
||||
val clientSettingsFileOverrideToCheck = new File(clientSettingsPathOverride)
|
||||
@ -45,4 +39,17 @@ object ClientSettings {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
def getClientSettingsWithOverride(clientSettingsOverrideJson: String): Map[String, Object] = {
|
||||
if (clientSettingsOverrideJson.nonEmpty) {
|
||||
val scalaMapClientOverride = common2.util.JsonUtil.toMap[Object](clientSettingsOverrideJson)
|
||||
scalaMapClientOverride match {
|
||||
case Success(clientSettingsOverrideAsMap) => YamlUtil.mergeImmutableMaps(clientSettingsFromFile, clientSettingsOverrideAsMap)
|
||||
case Failure(msg) =>
|
||||
logger.debug("No valid JSON override of client configuration in create call: {}", msg)
|
||||
clientSettingsFromFile
|
||||
}
|
||||
} else clientSettingsFromFile
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
package org.bigbluebutton
|
||||
|
||||
import scala.util.Try
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
|
||||
|
||||
import scala.util.{ Failure, Success, Try }
|
||||
import com.typesafe.config.ConfigFactory
|
||||
|
||||
trait SystemConfiguration {
|
||||
@ -77,6 +80,13 @@ trait SystemConfiguration {
|
||||
|
||||
lazy val analyticsIncludeChat = Try(config.getBoolean("analytics.includeChat")).getOrElse(true)
|
||||
|
||||
lazy val clientSettingsPath = Try(config.getString("client.clientSettingsFilePath")).getOrElse(
|
||||
"/usr/share/meteor/bundle/programs/server/assets/app/config/settings.yml"
|
||||
)
|
||||
lazy val clientSettingsPathOverride = Try(config.getString("client.clientSettingsOverrideFilePath")).getOrElse(
|
||||
"/etc/bigbluebutton/bbb-html5.yml"
|
||||
)
|
||||
|
||||
// Grab the "interface" parameter from the http config
|
||||
val httpHost = config.getString("http.interface")
|
||||
// Grab the "port" parameter from the http config
|
||||
|
@ -19,12 +19,6 @@ import org.bigbluebutton.core.util.ColorPicker
|
||||
import org.bigbluebutton.core2.RunningMeetings
|
||||
import org.bigbluebutton.core2.message.senders.MsgBuilder
|
||||
import org.bigbluebutton.service.HealthzService
|
||||
import org.bigbluebutton.common2
|
||||
import org.bigbluebutton.common2.util.YamlUtil
|
||||
|
||||
import scala.jdk.CollectionConverters._
|
||||
import java.util
|
||||
import scala.util.{ Failure, Success }
|
||||
|
||||
object BigBlueButtonActor extends SystemConfiguration {
|
||||
def props(
|
||||
|
@ -34,6 +34,7 @@ object JsonUtils {
|
||||
case m: Map[_, _] => JsObject(m.asInstanceOf[Map[String, Any]].map { case (k, v) => k -> write(v) })
|
||||
case l: List[_] => JsArray(l.map(write).toVector)
|
||||
case a: Array[_] => JsArray(a.map(write).toVector)
|
||||
case null => JsNull
|
||||
case _ => throw new IllegalArgumentException(s"Unsupported type: ${x.getClass.getName}")
|
||||
// case _ => JsNull
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ class RunningMeeting(val props: DefaultProps, outGW: OutMessageGateway,
|
||||
private val deskshareModel = new ScreenshareModel
|
||||
private val audioCaptions = new AudioCaptions
|
||||
private val timerModel = new TimerModel
|
||||
private val clientSettings = ClientSettings.clientSettingsFromFile
|
||||
private val clientSettings: Map[String, Object] = ClientSettings.getClientSettingsWithOverride(props.overrideClientSettings)
|
||||
|
||||
// meetingModel.setGuestPolicy(props.usersProp.guestPolicy)
|
||||
|
||||
|
@ -89,7 +89,8 @@ case class DefaultProps(
|
||||
metadataProp: MetadataProp,
|
||||
lockSettingsProps: LockSettingsProps,
|
||||
systemProps: SystemProps,
|
||||
groups: Vector[GroupProps]
|
||||
groups: Vector[GroupProps],
|
||||
overrideClientSettings: String
|
||||
)
|
||||
|
||||
case class StartEndTimeStatus(startTime: Long, endTime: Long)
|
||||
|
@ -407,7 +407,8 @@ public class MeetingService implements MessageListener {
|
||||
m.getMuteOnStart(), m.getAllowModsToUnmuteUsers(), m.getAllowModsToEjectCameras(), m.getMeetingKeepEvents(),
|
||||
m.breakoutRoomsParams, m.lockSettingsParams, m.getHtml5InstanceId(),
|
||||
m.getGroups(), m.getDisabledFeatures(), m.getNotifyRecordingIsOn(),
|
||||
m.getPresentationUploadExternalDescription(), m.getPresentationUploadExternalUrl());
|
||||
m.getPresentationUploadExternalDescription(), m.getPresentationUploadExternalUrl(),
|
||||
m.getOverrideClientSettings());
|
||||
}
|
||||
|
||||
private String formatPrettyDate(Long timestamp) {
|
||||
|
@ -19,13 +19,10 @@
|
||||
|
||||
package org.bigbluebutton.api;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
@ -37,12 +34,6 @@ import com.google.gson.JsonObject;
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.ClientProtocolException;
|
||||
import org.apache.http.client.ResponseHandler;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.bigbluebutton.api.domain.BreakoutRoomsParams;
|
||||
import org.bigbluebutton.api.domain.LockSettingsParams;
|
||||
import org.bigbluebutton.api.domain.Meeting;
|
||||
@ -145,6 +136,7 @@ public class ParamsProcessorUtil {
|
||||
|
||||
private String bbbVersion = "";
|
||||
private Boolean allowRevealOfBBBVersion = false;
|
||||
private Boolean allowOverrideClientSettingsOnCreateCall = false;
|
||||
|
||||
private String formatConfNum(String s) {
|
||||
if (s.length() > 5) {
|
||||
@ -912,6 +904,10 @@ public class ParamsProcessorUtil {
|
||||
return allowRevealOfBBBVersion;
|
||||
}
|
||||
|
||||
public Boolean getAllowOverrideClientSettingsOnCreateCall() {
|
||||
return allowOverrideClientSettingsOnCreateCall;
|
||||
}
|
||||
|
||||
public String processWelcomeMessage(String message, Boolean isBreakout) {
|
||||
String welcomeMessage = message;
|
||||
if (StringUtils.isEmpty(message)) {
|
||||
@ -1515,4 +1511,8 @@ public class ParamsProcessorUtil {
|
||||
this.allowRevealOfBBBVersion = allowVersion;
|
||||
}
|
||||
|
||||
public void setAllowOverrideClientSettingsOnCreateCall(Boolean allowOverrideClientSettingsOnCreateCall) {
|
||||
this.allowOverrideClientSettingsOnCreateCall = allowOverrideClientSettingsOnCreateCall;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -118,6 +118,8 @@ public class Meeting {
|
||||
|
||||
private Integer html5InstanceId;
|
||||
|
||||
private String overrideClientSettings = "";
|
||||
|
||||
public Meeting(Meeting.Builder builder) {
|
||||
name = builder.name;
|
||||
extMeetingId = builder.externalId;
|
||||
@ -1131,4 +1133,12 @@ public class Meeting {
|
||||
return new Meeting(this);
|
||||
}
|
||||
}
|
||||
|
||||
public String getOverrideClientSettings() {
|
||||
return overrideClientSettings;
|
||||
}
|
||||
|
||||
public void setOverrideClientSettings(String overrideClientConfigs) {
|
||||
this.overrideClientSettings = overrideClientConfigs;
|
||||
}
|
||||
}
|
||||
|
@ -46,7 +46,8 @@ public interface IBbbWebApiGWApp {
|
||||
ArrayList<String> disabledFeatures,
|
||||
Boolean notifyRecordingIsOn,
|
||||
String presentationUploadExternalDescription,
|
||||
String presentationUploadExternalUrl);
|
||||
String presentationUploadExternalUrl,
|
||||
String overrideClientSettings);
|
||||
|
||||
void registerUser(String meetingID, String internalUserId, String fullname, String role,
|
||||
String externUserID, String authToken, String sessionToken, String avatarURL,
|
||||
|
@ -153,7 +153,8 @@ class BbbWebApiGWApp(
|
||||
disabledFeatures: java.util.ArrayList[String],
|
||||
notifyRecordingIsOn: java.lang.Boolean,
|
||||
presentationUploadExternalDescription: String,
|
||||
presentationUploadExternalUrl: String): Unit = {
|
||||
presentationUploadExternalUrl: String,
|
||||
overrideClientSettings: String): Unit = {
|
||||
|
||||
val disabledFeaturesAsVector: Vector[String] = disabledFeatures.asScala.toVector
|
||||
|
||||
@ -246,7 +247,8 @@ class BbbWebApiGWApp(
|
||||
metadataProp,
|
||||
lockSettingsProps,
|
||||
systemProps,
|
||||
groupsAsVector
|
||||
groupsAsVector,
|
||||
overrideClientSettings
|
||||
)
|
||||
|
||||
//meetingManagerActorRef ! new CreateMeetingMsg(defaultProps)
|
||||
|
@ -155,6 +155,7 @@ export default async function addMeeting(meeting) {
|
||||
html5InstanceId: Number,
|
||||
},
|
||||
groups: Array,
|
||||
overrideClientSettings: String,
|
||||
});
|
||||
|
||||
const {
|
||||
|
@ -348,6 +348,9 @@ allowFetchAllRecordings=true
|
||||
# The directory where the pre-built configs are stored
|
||||
configDir=/var/bigbluebutton/configs
|
||||
|
||||
# Disable this option to avoid overriding client settings through /create call
|
||||
allowOverrideClientSettingsOnCreateCall=true
|
||||
|
||||
# The directory to export Json with Meeting activities (used in Learning Dashboard)
|
||||
learningDashboardFilesDir=/var/bigbluebutton/learning-dashboard
|
||||
|
||||
|
@ -197,6 +197,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
<property name="notifyRecordingIsOn" value="${notifyRecordingIsOn}"/>
|
||||
<property name="defaultKeepEvents" value="${defaultKeepEvents}"/>
|
||||
<property name="allowRevealOfBBBVersion" value="${allowRevealOfBBBVersion}"/>
|
||||
<property name="allowOverrideClientSettingsOnCreateCall" value="${allowOverrideClientSettingsOnCreateCall}"/>
|
||||
</bean>
|
||||
|
||||
<bean id="presentationService" class="org.bigbluebutton.web.services.PresentationService">
|
||||
|
@ -44,6 +44,7 @@ import org.bigbluebutton.web.services.turn.StunServer
|
||||
import org.bigbluebutton.web.services.turn.RemoteIceCandidate
|
||||
import org.json.JSONArray
|
||||
|
||||
|
||||
import javax.servlet.ServletRequest
|
||||
|
||||
class ApiController {
|
||||
@ -63,6 +64,7 @@ class ApiController {
|
||||
ResponseBuilder responseBuilder = initResponseBuilder()
|
||||
ValidationService validationService
|
||||
|
||||
|
||||
def initResponseBuilder = {
|
||||
String protocol = this.getClass().getResource("").getProtocol();
|
||||
if (Objects.equals(protocol, "jar")) {
|
||||
@ -150,11 +152,25 @@ class ApiController {
|
||||
|
||||
Meeting newMeeting = paramsProcessorUtil.processCreateParams(params)
|
||||
|
||||
String requestBody = request.inputStream == null ? null : request.inputStream.text
|
||||
requestBody = StringUtils.isEmpty(requestBody) ? null : requestBody
|
||||
|
||||
def xmlModules = processRequestXmlModules(requestBody)
|
||||
|
||||
// Set Client Settings Override
|
||||
if(xmlModules.containsKey("clientSettingsOverride")) {
|
||||
if(paramsProcessorUtil.getAllowOverrideClientSettingsOnCreateCall()) {
|
||||
newMeeting.setOverrideClientSettings(xmlModules.get("clientSettingsOverride").text())
|
||||
} else {
|
||||
log.warn("Module `clientSettingsOverride` provided but this options is disabled by `allowOverrideClientSettingsOnCreateCall=false` config.");
|
||||
}
|
||||
}
|
||||
|
||||
ApiErrors errors = new ApiErrors()
|
||||
|
||||
if (meetingService.createMeeting(newMeeting)) {
|
||||
// See if the request came with pre-uploading of presentation.
|
||||
uploadDocuments(newMeeting, false); //
|
||||
uploadDocuments(xmlModules, newMeeting, false); //
|
||||
respondWithConference(newMeeting, null, null)
|
||||
} else {
|
||||
// Translate the external meeting id into an internal meeting id.
|
||||
@ -1106,8 +1122,12 @@ class ApiController {
|
||||
|
||||
Meeting meeting = ServiceUtils.findMeetingFromMeetingID(params.meetingID);
|
||||
|
||||
if (meeting != null){
|
||||
if (uploadDocuments(meeting, true)) {
|
||||
if (meeting != null) {
|
||||
String requestBody = request.inputStream == null ? null : request.inputStream.text
|
||||
requestBody = StringUtils.isEmpty(requestBody) ? null : requestBody
|
||||
|
||||
def xmlModules = processRequestXmlModules(requestBody)
|
||||
if (uploadDocuments(xmlModules, meeting, true)) {
|
||||
withFormat {
|
||||
xml {
|
||||
render(text: responseBuilder.buildInsertDocumentResponse("Presentation is being uploaded", RESP_CODE_SUCCESS)
|
||||
@ -1345,7 +1365,7 @@ class ApiController {
|
||||
}
|
||||
}
|
||||
|
||||
def uploadDocuments(conf, isFromInsertAPI) {
|
||||
def uploadDocuments(xmlModules, conf, isFromInsertAPI) {
|
||||
if (conf.getDisabledFeatures().contains("presentation")) {
|
||||
log.warn("Presentation feature is disabled.")
|
||||
return false
|
||||
@ -1365,8 +1385,6 @@ class ApiController {
|
||||
}
|
||||
|
||||
Boolean isDefaultPresentationUsed = false;
|
||||
String requestBody = request.inputStream == null ? null : request.inputStream.text;
|
||||
requestBody = StringUtils.isEmpty(requestBody) ? null : requestBody;
|
||||
Boolean isDefaultPresentationCurrent = false;
|
||||
def listOfPresentation = []
|
||||
def presentationListHasCurrent = false
|
||||
@ -1374,40 +1392,43 @@ class ApiController {
|
||||
// This part of the code is responsible for organize the presentations in a certain order
|
||||
// It selects the one that has the current=true, and put it in the 0th place.
|
||||
// Afterwards, the 0th presentation is going to be uploaded first, which spares processing time
|
||||
if (requestBody == null) {
|
||||
if (!xmlModules.containsKey("presentation")) {
|
||||
if (isFromInsertAPI) {
|
||||
log.warn("Insert Document API called without a payload - ignoring")
|
||||
return;
|
||||
}
|
||||
listOfPresentation << [name: "default", current: true];
|
||||
} else {
|
||||
def xml = new XmlSlurper().parseText(requestBody);
|
||||
Boolean hasCurrent = false;
|
||||
xml.children().each { module ->
|
||||
log.debug("module config found: [${module.@name}]");
|
||||
|
||||
if ("presentation".equals(module.@name.toString())) {
|
||||
for (document in module.children()) {
|
||||
if (!StringUtils.isEmpty(document.@current.toString()) && java.lang.Boolean.parseBoolean(
|
||||
document.@current.toString()) && !hasCurrent) {
|
||||
listOfPresentation.add(0, document)
|
||||
hasCurrent = true;
|
||||
} else {
|
||||
listOfPresentation << document
|
||||
}
|
||||
}
|
||||
Boolean uploadDefault = !preUploadedPresentationOverrideDefault && !isDefaultPresentationUsed && !isFromInsertAPI;
|
||||
if (uploadDefault) {
|
||||
isDefaultPresentationCurrent = !hasCurrent;
|
||||
hasCurrent = true
|
||||
isDefaultPresentationUsed = true
|
||||
if (isDefaultPresentationCurrent) {
|
||||
listOfPresentation.add(0, [name: "default", current: true])
|
||||
} else {
|
||||
listOfPresentation << [name: "default", current: false];
|
||||
}
|
||||
Boolean hasPresentationModule = false;
|
||||
if (xmlModules.containsKey("presentation")) {
|
||||
def modulePresentation = xmlModules.get("presentation")
|
||||
hasPresentationModule = true
|
||||
for (document in modulePresentation.children()) {
|
||||
if (!StringUtils.isEmpty(document.@current.toString()) && java.lang.Boolean.parseBoolean(
|
||||
document.@current.toString()) && !hasCurrent) {
|
||||
listOfPresentation.add(0, document)
|
||||
hasCurrent = true;
|
||||
} else {
|
||||
listOfPresentation << document
|
||||
}
|
||||
}
|
||||
|
||||
Boolean uploadDefault = !preUploadedPresentationOverrideDefault && !isDefaultPresentationUsed && !isFromInsertAPI;
|
||||
if (uploadDefault) {
|
||||
isDefaultPresentationCurrent = !hasCurrent;
|
||||
hasCurrent = true
|
||||
isDefaultPresentationUsed = true
|
||||
if (isDefaultPresentationCurrent) {
|
||||
listOfPresentation.add(0, [name: "default", current: true])
|
||||
} else {
|
||||
listOfPresentation << [name: "default", current: false];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasPresentationModule) {
|
||||
hasCurrent = true
|
||||
listOfPresentation.add(0, [name: "default", current: true])
|
||||
}
|
||||
presentationListHasCurrent = hasCurrent;
|
||||
}
|
||||
@ -1470,6 +1491,20 @@ class ApiController {
|
||||
return true
|
||||
}
|
||||
|
||||
def processRequestXmlModules(String requestBody) {
|
||||
def xmlModules = [:]
|
||||
|
||||
if (requestBody != null && requestBody != "") {
|
||||
def xml = new XmlSlurper().parseText(requestBody)
|
||||
xml.children().each { module ->
|
||||
log.debug("module found: [${module.@name}]")
|
||||
xmlModules.put(module.@name.toString(), module);
|
||||
}
|
||||
}
|
||||
|
||||
return xmlModules
|
||||
}
|
||||
|
||||
def processDocumentFromRawBytes(bytes, presOrigFilename, meetingId, current, isDownloadable, isRemovable,
|
||||
isInitialPresentation) {
|
||||
def uploadFailed = false
|
||||
|
Loading…
Reference in New Issue
Block a user