Merge remote-tracking branch 'upstream/master' into close

This commit is contained in:
JaeeunCho 2017-03-16 15:55:17 -07:00
commit 32a665d2fa
60 changed files with 2213 additions and 183 deletions

56
bbb-common-web/.gitignore vendored Normal file
View File

@ -0,0 +1,56 @@
.DS_Store
._.DS_Store*
.metadata
.project
.classpath
.settings
.history
.worksheet
gen
**/*.swp
**/*~.nib
**/build/
**/*.pbxuser
**/*.perspective
**/*.perspectivev3
*.xcworkspace
*.xcuserdatad
**/target
target
*.iml
project/*.ipr
project/*.iml
project/*.iws
project/out
project/*/target
project/target
project/*/bin
project/*/build
project/*.iml
project/*/*.iml
project/.idea
project/.idea/*
.idea
.idea/*
.idea/**/*
.DS_Store
project/.DS_Store
project/*/.DS_Store
tm.out
tmlog*.log
*.tm*.epoch
out/
provisioning/.vagrant
provisioning/*/.vagrant
provisioning/*/*.known
/sbt/akka-patterns-store/
/daemon/src/build/
*.lock
log/
tmp/
build/
akka-patterns-store/
lib_managed/
.cache
bin/

1
bbb-common-web/README.md Normal file
View File

@ -0,0 +1 @@
see http://code.google.com/p/bigbluebutton/wiki/DevelopingBBB

117
bbb-common-web/build.sbt Executable file
View File

@ -0,0 +1,117 @@
name := "bbb-common-web"
organization := "org.bigbluebutton"
version := "0.0.1-SNAPSHOT"
scalaVersion := "2.11.7"
scalacOptions ++= Seq(
"-unchecked",
"-deprecation",
"-Xlint",
"-Ywarn-dead-code",
"-language:_",
"-target:jvm-1.8",
"-encoding", "UTF-8"
)
// We want to have our jar files in lib_managed dir.
// This way we'll have the right path when we import
// into eclipse.
retrieveManaged := true
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "html", "console", "junitxml")
testOptions in Test += Tests.Argument(TestFrameworks.ScalaTest, "-h", "target/scalatest-reports")
val scalaV = "2.11.7"
libraryDependencies += "org.scala-lang" % "scala-compiler" % scalaV
libraryDependencies += "org.scala-lang" % "scala-library" % scalaV
libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaV
// https://mvnrepository.com/artifact/org.apache.commons/commons-lang3
libraryDependencies += "org.apache.commons" % "commons-lang3" % "3.5"
libraryDependencies += "commons-io" % "commons-io" % "2.4"
libraryDependencies += "org.freemarker" % "freemarker" % "2.3.23"
libraryDependencies += "com.fasterxml.jackson.dataformat" % "jackson-dataformat-xml" % "2.6.3"
// https://mvnrepository.com/artifact/org.codehaus.woodstox/woodstox-core-asl
libraryDependencies += "org.codehaus.woodstox" % "woodstox-core-asl" % "4.4.1"
libraryDependencies += "org.slf4j" % "slf4j-api" % "1.7.5"
libraryDependencies += "org.pegdown" % "pegdown" % "1.4.0" % "test"
libraryDependencies += "junit" % "junit" % "4.12" % "test"
libraryDependencies += "com.novocode" % "junit-interface" % "0.11" % "test"
// https://mvnrepository.com/artifact/org.mockito/mockito-core
libraryDependencies += "org.mockito" % "mockito-core" % "2.7.12" % "test"
libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.1" % "test"
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.1" % "test"
seq(Revolver.settings: _*)
//-----------
// Packaging
//
// Reference:
// http://xerial.org/blog/2014/03/24/sbt/
// http://www.scala-sbt.org/sbt-pgp/usage.html
// http://www.scala-sbt.org/0.13/docs/Using-Sonatype.html
// http://central.sonatype.org/pages/requirements.html
// http://central.sonatype.org/pages/releasing-the-deployment.html
//-----------
// Build pure Java lib (i.e. without scala)
// Do not append Scala versions to the generated artifacts
crossPaths := false
// This forbids including Scala related libraries into the dependency
autoScalaLibrary := false
/***************************
* When developing, change the version above to x.x.x-SNAPSHOT then use the file resolver to
* publish to the local maven repo using "sbt publish"
*/
// Uncomment this to publish to local maven repo while commenting out the nexus repo
publishTo := Some(Resolver.file("file", new File(Path.userHome.absolutePath+"/.m2/repository")))
// Comment this out when publishing to local maven repo using SNAPSHOT version.
// To push to sonatype "sbt publishSigned"
//publishTo := {
// val nexus = "https://oss.sonatype.org/"
// if (isSnapshot.value)
// Some("snapshots" at nexus + "content/repositories/snapshots")
// else
// Some("releases" at nexus + "service/local/staging/deploy/maven2")
//}
// Enables publishing to maven repo
publishMavenStyle := true
publishArtifact in Test := false
pomIncludeRepository := { _ => false }
pomExtra := (
<scm>
<url>git@github.com:bigbluebutton/bigbluebutton.git</url>
<connection>scm:git:git@github.com:bigbluebutton/bigbluebutton.git</connection>
</scm>
<developers>
<developer>
<id>ritzalam</id>
<name>Richard Alam</name>
<url>http://www.bigbluebutton.org</url>
</developer>
</developers>)
licenses := Seq("LGPL-3.0" -> url("http://opensource.org/licenses/LGPL-3.0"))
homepage := Some(url("http://www.bigbluebutton.org"))

View File

View File

@ -0,0 +1 @@
sbt.version=0.13.8

View File

@ -0,0 +1,9 @@
addSbtPlugin("io.spray" % "sbt-revolver" % "0.7.2")
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.2.0")
addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0")
addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.0")

View File

@ -33,7 +33,10 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.io.FileUtils;
import org.bigbluebutton.api.domain.Recording;
import org.bigbluebutton.api.domain.RecordingMetadata;
import org.bigbluebutton.api.util.RecordingMetadataReaderHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -64,6 +67,38 @@ public class RecordingService {
}
}
public List<RecordingMetadata> getRecordingsMetadata(List<String> recordIDs, List<String> states) {
List<RecordingMetadata> recs = new ArrayList<RecordingMetadata>();
Map<String, List<File>> allDirectories = getAllDirectories(states);
if (recordIDs.isEmpty()) {
for (Map.Entry<String, List<File>> entry : allDirectories.entrySet()) {
recordIDs.addAll(getAllRecordingIds(entry.getValue()));
}
}
for (String recordID : recordIDs) {
for (Map.Entry<String, List<File>> entry : allDirectories.entrySet()) {
List<File> _recs = getRecordingsForPath(recordID, entry.getValue());
Iterator<File> iterator = _recs.iterator();
while (iterator.hasNext()) {
RecordingMetadata r = getRecordingMetadata(iterator.next());
if (r != null) {
recs.add(r);
}
}
}
}
return recs;
}
private static RecordingMetadata getRecordingMetadata(File dir) {
File file = new File(dir.getPath() + File.separatorChar + "metadata.xml");
RecordingMetadata rec = RecordingMetadataReaderHelper.getRecordingMetadata(file);
return rec;
}
public List<Recording> getRecordings(List<String> recordIDs, List<String> states) {
List<Recording> recs = new ArrayList<Recording>();
@ -119,6 +154,45 @@ public class RecordingService {
return matchesMetadata;
}
public boolean recordingMatchesMetadata(RecordingMetadata recording, Map<String, String> metadataFilters) {
boolean matchesMetadata = true;
for (Map.Entry<String, String> filter : metadataFilters.entrySet()) {
String metadataValue = recording.getMeta().get().get(filter.getKey());
if ( metadataValue == null ) {
// The recording doesn't have metadata specified
matchesMetadata = false;
} else {
String filterValue = filter.getValue();
if( filterValue.charAt(0) == '%' && filterValue.charAt(filterValue.length()-1) == '%' && metadataValue.contains(filterValue.substring(1, filterValue.length()-1)) ){
// Filter value embraced by two wild cards
// AND the filter value is part of the metadata value
} else if( filterValue.charAt(0) == '%' && metadataValue.endsWith(filterValue.substring(1, filterValue.length())) ) {
// Filter value starts with a wild cards
// AND the filter value ends with the metadata value
} else if( filterValue.charAt(filterValue.length()-1) == '%' && metadataValue.startsWith(filterValue.substring(0, filterValue.length()-1)) ) {
// Filter value ends with a wild cards
// AND the filter value starts with the metadata value
} else if( metadataValue.equals(filterValue) ) {
// Filter value doesnt have wildcards
// AND the filter value is the same as metadata value
} else {
matchesMetadata = false;
}
}
}
return matchesMetadata;
}
public List<RecordingMetadata> filterRecordingsByMetadata(List<RecordingMetadata> recordings, Map<String, String> metadataFilters) {
List<RecordingMetadata> resultRecordings = new ArrayList<RecordingMetadata>();
for (RecordingMetadata entry : recordings) {
if (recordingMatchesMetadata(entry, metadataFilters))
resultRecordings.add(entry);
}
return resultRecordings;
}
public Map<String, Recording> filterRecordingsByMetadata(Map<String, Recording> recordings, Map<String, String> metadataFilters) {
Map<String, Recording> resultRecordings = new HashMap<String, Recording>();
for (Map.Entry<String, Recording> entry : recordings.entrySet()) {
@ -189,7 +263,7 @@ public class RecordingService {
return rec;
}
private void deleteRecording(String id, String path) {
private static void deleteRecording(String id, String path) {
String[] format = getPlaybackFormats(path);
for (int i = 0; i < format.length; i++) {
List<File> recordings = getDirectories(path + File.separatorChar + format[i]);
@ -202,12 +276,12 @@ public class RecordingService {
}
}
private void createDirectory(File directory) {
private static void createDirectory(File directory) {
if (!directory.exists())
directory.mkdirs();
}
private void deleteDirectory(File directory) {
private static void deleteDirectory(File directory) {
/**
* Go through each directory and check if it's not empty. We need to
* delete files inside a directory before a directory can be deleted.
@ -224,7 +298,7 @@ public class RecordingService {
directory.delete();
}
private List<File> getDirectories(String path) {
private static List<File> getDirectories(String path) {
List<File> files = new ArrayList<File>();
try {
DirectoryStream<Path> stream = Files.newDirectoryStream(FileSystems.getDefault().getPath(path));
@ -240,7 +314,7 @@ public class RecordingService {
return files;
}
private String[] getPlaybackFormats(String path) {
private static String[] getPlaybackFormats(String path) {
List<File> dirs = getDirectories(path);
String[] formats = new String[dirs.size()];
@ -315,41 +389,97 @@ public class RecordingService {
List<File> recordings = getDirectories(path + File.separatorChar + format[i]);
for (int f = 0; f < recordings.size(); f++) {
if (recordings.get(f).getName().equalsIgnoreCase(recordingId)) {
Recording r = getRecordingInfo(recordings.get(f));
if (r != null) {
File dest;
if (state.equals(Recording.STATE_PUBLISHED)) {
dest = new File(publishedDir + File.separatorChar + format[i]);
} else if (state.equals(Recording.STATE_UNPUBLISHED)) {
dest = new File(unpublishedDir + File.separatorChar + format[i]);
} else if (state.equals(Recording.STATE_DELETED)) {
dest = new File(deletedDir + File.separatorChar + format[i]);
} else {
log.debug(String.format("State: %s, is not supported", state));
return;
}
if (!dest.exists())
dest.mkdirs();
boolean moved = recordings.get(f).renameTo(new File(dest, recordings.get(f).getName()));
if (moved) {
log.debug("Recording successfully moved!");
r.setState(state);
r.setPublished(state.equals(Recording.STATE_PUBLISHED));
if (state.equals(Recording.STATE_DELETED)) {
r.setPlaybackFormat(null);
deleteRecording(recordingId, deletedDir);
}
recordingServiceHelper.writeRecordingInfo(dest.getAbsolutePath() + File.separatorChar + recordings.get(f).getName(), r);
log.debug(String.format("Recording successfully %s!", state));
} else {
log.debug("Recording was not moved");
}
File dest;
if (state.equals(Recording.STATE_PUBLISHED)) {
dest = new File(publishedDir + File.separatorChar + format[i]);
RecordingService.publishRecording(dest, recordingId, recordings.get(f));
} else if (state.equals(Recording.STATE_UNPUBLISHED)) {
dest = new File(unpublishedDir + File.separatorChar + format[i]);
RecordingService.unpublishRecording(dest, recordingId, recordings.get(f));
} else if (state.equals(Recording.STATE_DELETED)) {
dest = new File(deletedDir + File.separatorChar + format[i]);
RecordingService.deleteRecording(dest, recordingId, recordings.get(f));
} else {
log.debug(String.format("State: %s, is not supported", state));
return;
}
}
}
}
}
public static void publishRecording(File destDir, String recordingId, File recordingDir) {
File metadataXml = RecordingMetadataReaderHelper.getMetadataXmlLocation(recordingDir.getPath());
RecordingMetadata r = RecordingMetadataReaderHelper.getRecordingMetadata(metadataXml);
if (r != null) {
if (!destDir.exists()) destDir.mkdirs();
try {
FileUtils.moveDirectory(recordingDir, new File(destDir.getPath() + File.separatorChar + recordingId));
r.setState(Recording.STATE_PUBLISHED);
r.setPublished(true);
File medataXmlFile = RecordingMetadataReaderHelper.getMetadataXmlLocation(
destDir.getAbsolutePath() + File.separatorChar + recordingId);
// Process the changes by saving the recording into metadata.xml
RecordingMetadataReaderHelper.saveRecordingMetadata(medataXmlFile, r);
} catch (IOException e) {
log.error("Failed to publish recording : " + recordingId, e);
}
}
}
public static void unpublishRecording(File destDir, String recordingId, File recordingDir) {
File metadataXml = RecordingMetadataReaderHelper.getMetadataXmlLocation(recordingDir.getPath());
RecordingMetadata r = RecordingMetadataReaderHelper.getRecordingMetadata(metadataXml);
if (r != null) {
if (!destDir.exists()) destDir.mkdirs();
try {
FileUtils.moveDirectory(recordingDir, new File(destDir.getPath() + File.separatorChar + recordingId));
r.setState(Recording.STATE_UNPUBLISHED);
r.setPublished(false);
File medataXmlFile = RecordingMetadataReaderHelper.getMetadataXmlLocation(
destDir.getAbsolutePath() + File.separatorChar + recordingId);
// Process the changes by saving the recording into metadata.xml
RecordingMetadataReaderHelper.saveRecordingMetadata(medataXmlFile, r);
} catch (IOException e) {
log.error("Failed to unpublish recording : " + recordingId, e);
}
}
}
public static void deleteRecording(File destDir, String recordingId, File recordingDir) {
File metadataXml = RecordingMetadataReaderHelper.getMetadataXmlLocation(recordingDir.getPath());
RecordingMetadata r = RecordingMetadataReaderHelper.getRecordingMetadata(metadataXml);
if (r != null) {
if (!destDir.exists()) destDir.mkdirs();
try {
FileUtils.moveDirectory(recordingDir, new File(destDir.getPath() + File.separatorChar + recordingId));
r.setState(Recording.STATE_DELETED);
r.setPublished(false);
File medataXmlFile = RecordingMetadataReaderHelper.getMetadataXmlLocation(
destDir.getAbsolutePath() + File.separatorChar + recordingId);
// Process the changes by saving the recording into metadata.xml
RecordingMetadataReaderHelper.saveRecordingMetadata(medataXmlFile, r);
} catch (IOException e) {
log.error("Failed to delete recording : " + recordingId, e);
}
}
}
private List<File> getAllDirectories(String state) {
List<File> allDirectories = new ArrayList<File>();
@ -415,22 +545,8 @@ public class RecordingService {
Map<String,File> recsIndexed = indexRecordings(recs);
if ( recsIndexed.containsKey(recordID) ) {
File recFile = recsIndexed.get(recordID);
Recording rec = getRecordingInfo(recFile);
if (rec != null) {
for (Map.Entry<String,String> meta : metaParams.entrySet()) {
if ( !"".equals(meta.getValue()) ) {
// As it has a value, if the meta parameter exists update it, otherwise add it
rec.updateMetadata(meta.getKey(), meta.getValue());
} else {
// As it doesn't have a value, if it exists delete it
if ( rec.containsMetadata(meta.getKey()) ) {
rec.deleteMetadata(meta.getKey());
}
}
}
// Process the changes by saving the recording into metadata.xml
recordingServiceHelper.writeRecordingInfo(recFile.getAbsolutePath(), rec);
}
File metadataXml = RecordingMetadataReaderHelper.getMetadataXmlLocation(recFile.getPath());
updateRecordingMetadata(metadataXml, metaParams, metadataXml);
}
}
}
@ -438,6 +554,27 @@ public class RecordingService {
return;
}
public static void updateRecordingMetadata(File srxMetadataXml, Map<String,String> metaParams, File destMetadataXml) {
RecordingMetadata rec = RecordingMetadataReaderHelper.getRecordingMetadata(srxMetadataXml);
if (rec != null && rec.getMeta() != null) {
for (Map.Entry<String,String> meta : metaParams.entrySet()) {
if ( !"".equals(meta.getValue()) ) {
// As it has a value, if the meta parameter exists update it, otherwise add it
rec.getMeta().set(meta.getKey(), meta.getValue());
} else {
// As it doesn't have a value, if it exists delete it
if ( rec.getMeta().containsKey(meta.getKey()) ) {
rec.getMeta().remove(meta.getKey());
}
}
}
// Process the changes by saving the recording into metadata.xml
RecordingMetadataReaderHelper.saveRecordingMetadata(destMetadataXml, rec);
}
}
private Map<String,File> indexRecordings(List<File> recs) {
Map<String,File> indexedRecs = new HashMap<String,File>();

View File

@ -0,0 +1,44 @@
package org.bigbluebutton.api.domain;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText;
public class Breakout {
/**
* <breakout parentMeetingId="f3ffe06acedf425565cc024c8ebe89a6552e8782-1489172964374" sequence="2" meetingId="f2041d123b6a4b994e7ad87ee9d348496a73472c-1489173065780"/>
*/
@JacksonXmlProperty(isAttribute = true)
private String parentMeetingId;
@JacksonXmlProperty(isAttribute = true)
private int sequence;
@JacksonXmlProperty(isAttribute = true)
private String meetingId;
public void setParentMeetingId(String parentMeetingId) {
this.parentMeetingId = parentMeetingId;
}
public String getParentMeetingId() {
return parentMeetingId;
}
public void setSequence(int sequence) {
this.sequence = sequence;
}
public int getSequence() {
return sequence;
}
public void setMeetingId(String meetingId) {
this.meetingId = meetingId;
}
public String getMeetingId() {
return meetingId;
}
}

View File

@ -0,0 +1,18 @@
package org.bigbluebutton.api.domain;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText;
public class BreakoutRoom {
@JacksonXmlText
private String value;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}

View File

@ -0,0 +1,21 @@
package org.bigbluebutton.api.domain;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
/**
* Created by ritz on 2017-03-11.
*/
public class Extensions {
@JacksonXmlProperty(localName = "preview")
@JacksonXmlElementWrapper(useWrapping = false)
private Preview preview;
public void setPreview(Preview preview) {
this.preview = preview;
}
public Preview getPreview() {
return preview;
}
}

View File

@ -0,0 +1,59 @@
package org.bigbluebutton.api.domain;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText;
public class Image {
/**
* <image width="176" height="136" alt="Welcome to">
* http://192.168.23.22/presentation/32ee8bcccfad34f85c58a12f87fc4268130a4fd3-1489173065780/presentation/743dd59a958334b4cdcdaa302846d0c0eadcf9ff-1489173070800/thumbnails/thumb-1.png
* </image>
*/
@JacksonXmlProperty(isAttribute = true)
private String width;
@JacksonXmlProperty(isAttribute = true)
private String height;
@JacksonXmlProperty(isAttribute = true)
private String alt;
@JacksonXmlText
private String value;
public void setWidth(String width) {
this.width = width;
}
public String getWidth() {
return width;
}
public void setHeight(String height) {
this.height = height;
}
public String getHeight() {
return height;
}
public void setAlt(String alt) {
this.alt = alt;
}
public String getAlt() {
return alt;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}

View File

@ -19,20 +19,14 @@
package org.bigbluebutton.api.domain;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang.RandomStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Meeting {
private static Logger log = LoggerFactory.getLogger(Meeting.class);
private static final long MILLIS_IN_A_MINUTE = 60000;
private String name;
@ -68,6 +62,7 @@ public class Meeting {
private final ConcurrentMap<String, Long> registeredUsers;
private final ConcurrentMap<String, Config> configs;
private final Boolean isBreakout;
private final List<String> breakoutRooms = new ArrayList();
private long lastUserLeftOn = 0;
@ -102,6 +97,14 @@ public class Meeting {
configs = new ConcurrentHashMap<String, Config>();
}
public void addBreakoutRoom(String meetingId) {
breakoutRooms.add(meetingId);
}
public List<String> getBreakoutRooms() {
return breakoutRooms;
}
public String storeConfig(boolean defaultConfig, String config) {
String token = RandomStringUtils.randomAlphanumeric(8);
while (configs.containsKey(token)) {

View File

@ -0,0 +1,53 @@
package org.bigbluebutton.api.domain;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
public class MeetingInfo {
/**
* <meeting id="random-2810069" name="random-2810069" breakout="false"/>
*/
@JacksonXmlProperty(isAttribute = true)
private String id;
@JacksonXmlProperty(isAttribute = true)
private String externalId;
@JacksonXmlProperty(isAttribute = true)
private String name;
@JacksonXmlProperty(isAttribute = true)
private boolean breakout;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
public void setExternalId(String externalId) {
this.externalId = externalId;
}
public String getExternalId() {
return externalId;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setBreakout(boolean breakout) {
this.breakout = breakout;
}
public boolean isBreakout() {
return breakout;
}
}

View File

@ -0,0 +1,31 @@
package org.bigbluebutton.api.domain;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import java.util.Map;
import java.util.TreeMap;
@JacksonXmlRootElement(localName = "meta")
public class Metadata {
private Map<String,String> map = new TreeMap<String,String>();
@JsonAnyGetter
public Map<String, String> get() {
return map;
}
@JsonAnySetter
public void set(String name, String value) {
map.put(name, value);
}
public void remove(String key) {
map.remove(key);
}
public boolean containsKey(String key) {
return map.containsKey(key);
}
}

View File

@ -0,0 +1,18 @@
package org.bigbluebutton.api.domain;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
public class Preview {
@JacksonXmlElementWrapper(localName = "images")
@JacksonXmlProperty(localName = "image")
private Image[] images;
public void setImages(Image[] images) {
this.images = images;
}
public Image[] getImages() {
return images;
}
}

View File

@ -0,0 +1,150 @@
package org.bigbluebutton.api.domain;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import java.util.Map;
import java.util.TreeMap;
@JacksonXmlRootElement(localName = "recording")
public class RecordingMetadata {
/**
* <recording>
<id>32ee8bcccfad34f85c58a12f87fc4268130a4fd3-1489173065780</id>
<state>published</state>
<published>true</published>
<start_time>1489173065780</start_time>
<end_time>1489173199386</end_time>
<breakout parentMeetingId="f3ffe06acedf425565cc024c8ebe89a6552e8782-1489172964374" sequence="2" meetingId="f2041d123b6a4b994e7ad87ee9d348496a73472c-1489173065780"/>
<meta>
<meetingId>f2041d123b6a4b994e7ad87ee9d348496a73472c-1489173065780</meetingId>
<meetingName>random-2810069 (Room - 2)</meetingName>
<isBreakout>true</isBreakout>
</meta>
<playback>
<format>presentation</format>
<link>http://192.168.23.22/playback/presentation/0.9.0/playback.html?meetingId=32ee8bcccfad34f85c58a12f87fc4268130a4fd3-1489173065780</link>
<processing_time>9841</processing_time>
<duration>126376</duration>
<extensions>
<preview>
<images>
<image width="176" height="136" alt="Welcome to">http://192.168.23.22/presentation/32ee8bcccfad34f85c58a12f87fc4268130a4fd3-1489173065780/presentation/743dd59a958334b4cdcdaa302846d0c0eadcf9ff-1489173070800/thumbnails/thumb-1.png</image>
</images>
</preview>
</extensions>
</playback>
</recording>
*/
private String id;
private String state;
private boolean published;
@JacksonXmlProperty(localName = "start_time")
private String startTime;
@JacksonXmlProperty(localName = "end_time")
private String endTime;
@JacksonXmlProperty(localName = "participants")
private int participants;
@JacksonXmlProperty(localName = "meeting")
private MeetingInfo meetingInfo;
private Breakout breakout;
@JacksonXmlElementWrapper(localName = "breakoutRooms")
@JacksonXmlProperty(localName = "breakoutRoom")
private BreakoutRoom[] breakoutRooms;
private Metadata meta;
private RecordingMetadataPlayback playback;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
public void setState(String state) {
this.state = state;
}
public String getState() {
return state;
}
public void setPublished(boolean published) {
this.published = published;
}
public boolean getPublished() {
return published;
}
public void setStartTime(String startTime) {
this.startTime = startTime;
}
public String getStartTime() {
return startTime;
}
public void setEndTime(String endTime) {
this.endTime = endTime;
}
public String getEndTime() {
return endTime;
}
public void setParticipants(int participants) {
this.participants = participants;
}
public int getParticipants() {
return participants;
}
public void setMeeting(MeetingInfo meetingInfo) {
this.meetingInfo = meetingInfo;
}
public MeetingInfo getMeeting() {
return meetingInfo;
}
public void setBreakout(Breakout breakout) {
this.breakout = breakout;
}
public Breakout getBreakout() {
return breakout;
}
public void setBreakoutRooms(BreakoutRoom[] breakoutRooms) {
this.breakoutRooms = breakoutRooms;
}
public BreakoutRoom[] getBreakoutRooms() {
return breakoutRooms;
}
public void setMeta(Metadata meta) {
this.meta = meta;
}
public Metadata getMeta() {
return meta;
}
public RecordingMetadataPlayback getPlayback() {
return playback;
}
}

View File

@ -0,0 +1,57 @@
package org.bigbluebutton.api.domain;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
@JacksonXmlRootElement(localName = "playback")
public class RecordingMetadataPlayback {
private String format;
private String link;
@JacksonXmlProperty(localName = "processing_time")
private Long processingTime;
private Long duration;
private Extensions extensions;
public void setFormat(String format) {
this.format = format;
}
public String getFormat() {
return format;
}
public void setLink(String link) {
this.link = link;
}
public String getLink() {
return link;
}
public void setProcessingTime(Long processingTime) {
this.processingTime = processingTime;
}
public Long getProcessingTime() {
return processingTime;
}
public void setDuration(Long duration) {
this.duration = duration;
}
public Long getDuration() {
return duration;
}
public void setExtensions(Extensions extensions) {
this.extensions = extensions;
}
public Extensions getExtensions() {
return extensions;
}
}

View File

@ -0,0 +1,23 @@
package org.bigbluebutton.api.util;
import org.bigbluebutton.api.domain.Meeting;
public class MeetingResponseDetail {
private final String createdOn;
private final Meeting meeting;
public MeetingResponseDetail(String createdOn, Meeting meeting) {
this.createdOn = createdOn;
this.meeting = meeting;
}
public String getCreatedOn() {
return createdOn;
}
public Meeting getMeeting() {
return meeting;
}
}

View File

@ -0,0 +1,12 @@
package org.bigbluebutton.api.util;
import java.util.Collection;
public class MeetingsResponse {
public final Collection<MeetingResponseDetail> meetings;
public MeetingsResponse(Collection<MeetingResponseDetail> meetings) {
this.meetings = meetings;
}
}

View File

@ -0,0 +1,26 @@
package org.bigbluebutton.api.util;
import org.apache.commons.lang3.StringUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ParamsUtil {
private static final Pattern VALID_ID_PATTERN = Pattern.compile("[a-zA-Z][a-zA-Z0-9- ]*$");
public static final String invalidChars = ",";
public static String stripControlChars(String text) {
return text.replaceAll("\\p{Cc}", "");
}
public static boolean isValidMeetingId(String meetingId) {
//return VALID_ID_PATTERN.matcher(meetingId).matches();
return !containsChar(meetingId, invalidChars);
}
public static boolean containsChar(String text, String chars) {
return StringUtils.containsAny(text, chars);
}
}

View File

@ -0,0 +1,78 @@
package org.bigbluebutton.api.util;
import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import org.bigbluebutton.api.domain.RecordingMetadata;
import com.fasterxml.jackson.databind.SerializationFeature;
import javax.xml.stream.*;
import java.io.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RecordingMetadataReaderHelper {
private static Logger log = LoggerFactory.getLogger(RecordingMetadataReaderHelper.class);
public static String inputStreamToString(InputStream is) throws IOException {
StringBuilder sb = new StringBuilder();
String line;
BufferedReader br = new BufferedReader(new InputStreamReader(is));
while ((line = br.readLine()) != null) {
sb.append(line);
}
br.close();
return sb.toString();
}
public static RecordingMetadata getRecordingMetadata(File metadataXml) {
XMLInputFactory factory = XMLInputFactory.newInstance();
JacksonXmlModule module = new JacksonXmlModule();
// and then configure, for example:
module.setDefaultUseWrapper(false);
XmlMapper mapper = new XmlMapper(module);
//Reading from xml file and creating XMLStreamReader
XMLStreamReader reader = null;
RecordingMetadata recMeta = null;
try {
reader = factory.createXMLStreamReader(new FileInputStream(metadataXml));
recMeta = mapper.readValue(reader, RecordingMetadata.class);
} catch (XMLStreamException e) {
log.error("Failed to read metadata xml for recording: " + metadataXml.getAbsolutePath(), e);
} catch (FileNotFoundException e) {
log.error("File not found: " + metadataXml.getAbsolutePath(), e);
} catch (IOException e) {
log.error("IOException on " + metadataXml.getAbsolutePath(), e);
}
return recMeta;
}
public static File getMetadataXmlLocation(String destDir) {
return new File(destDir + File.separatorChar + "metadata.xml");
}
public static void saveRecordingMetadata(File metadataXml, RecordingMetadata recordingMetadata) {
//XMLOutputFactory factory = XMLOutputFactory.newInstance();
JacksonXmlModule module = new JacksonXmlModule();
module.setDefaultUseWrapper(false);
XmlMapper mapper = new XmlMapper(module);
//Reading from xml file and creating XMLStreamReader
//XMLStreamWriter writer = null;
try {
//writer = factory.createXMLStreamWriter(new FileOutputStream(metadataXml));
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.writeValue(metadataXml, recordingMetadata);
} catch (FileNotFoundException e) {
log.error("File not found: " + metadataXml.getAbsolutePath(), e);
} catch (IOException e) {
log.error("IOException on " + metadataXml.getAbsolutePath(), e);
}
}
}

View File

@ -0,0 +1,124 @@
package org.bigbluebutton.api.util;
import org.bigbluebutton.api.domain.Meeting;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.util.*;
import freemarker.template.*;
import org.bigbluebutton.api.domain.RecordingMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ResponseBuilder {
private static Logger log = LoggerFactory.getLogger(ResponseBuilder.class);
Configuration cfg = new Configuration(Configuration.VERSION_2_3_23);
public ResponseBuilder(File templatesLoc) {
try {
cfg.setDirectoryForTemplateLoading(templatesLoc);
} catch (IOException e) {
e.printStackTrace();
}
cfg.setDefaultEncoding("UTF-8");
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
cfg.setLogTemplateExceptions(false);
}
public String formatPrettyDate(Long timestamp) {
return new Date(timestamp).toString();
}
public String buildGetMeetingInfoResponse(Meeting meeting, String returnCode) {
String createdOn = formatPrettyDate(meeting.getCreateTime());
Template ftl = null;
try {
ftl = cfg.getTemplate("get-meeting-info.ftl");
} catch (IOException e) {
log.error("Cannot find get-meeting-info.ftl template for meeting : " + meeting.getInternalId(), e);
}
StringWriter xmlText = new StringWriter();
Map root = new HashMap();
root.put("returnCode", returnCode);
root.put("createdOn", createdOn);
root.put("meeting", meeting);
try {
ftl.process(root, xmlText);
} catch (TemplateException e) {
log.error("Template exception for meeting : " + meeting.getInternalId(), e);
} catch (IOException e) {
log.error("IO exception for meeting : " + meeting.getInternalId(), e);
}
return xmlText.toString();
}
public String buildGetMeetingsResponse(Collection<Meeting> meetings, String returnCode) {
ArrayList<MeetingResponseDetail> meetingResponseDetails = new ArrayList<MeetingResponseDetail>();
for (Meeting meeting : meetings) {
String createdOn = formatPrettyDate(meeting.getCreateTime());
MeetingResponseDetail details = new MeetingResponseDetail(createdOn, meeting);
meetingResponseDetails.add(details);
}
Template ftl = null;
try {
ftl = cfg.getTemplate("get-meetings.ftl");
} catch (IOException e) {
log.error("IO exception for get-meetings.ftl : ", e);
}
StringWriter xmlText = new StringWriter();
Map root = new HashMap();
root.put("returnCode", returnCode);
root.put("meetingDetailsList", meetingResponseDetails);
try {
ftl.process(root, xmlText);
} catch (TemplateException e) {
log.error("Template exception : ", e);
} catch (IOException e) {
log.error("IO exception for get-meetings.ftl : ", e);
}
return xmlText.toString();
}
public String buildGetRecordingsResponse(List<RecordingMetadata> recordings, String returnCode) {
Template ftl = null;
try {
ftl = cfg.getTemplate("get-recordings.ftl");
} catch (IOException e) {
log.error("IO exception for get-recordings.ftl : ", e);
}
StringWriter xmlText = new StringWriter();
Map root = new HashMap();
root.put("returnCode", returnCode);
root.put("recordings", recordings);
try {
ftl.process(root, xmlText);
} catch (TemplateException e) {
log.error("Template exception : ", e);
} catch (IOException e) {
log.error("IO exception for get-meetings.ftl : ", e);
}
return xmlText.toString();
}
}

View File

@ -0,0 +1,32 @@
<recording>
<id>32ee8bcccfad34f85c58a12f87fc4268130a4fd3-1489173065780</id>
<state>published</state>
<published>true</published>
<start_time>1489173065780</start_time>
<end_time>1489173199386</end_time>
<participants>10</participants>
<meeting id="f3ffe06acedf425565cc024c8ebe89a6552e8782-1489172964374" externalId="random-2810069" name="random-2810069" breakout="false"/>
<breakout parentMeetingId="f3ffe06acedf425565cc024c8ebe89a6552e8782-1489172964374" sequence="2" meetingId="f2041d123b6a4b994e7ad87ee9d348496a73472c-1489173065780"/>
<breakoutRooms>
<breakoutRoom>32ee8bcccfad34f85c58a12f87fc4268130a4fd3-1489173065780</breakoutRoom>
<breakoutRoom>721d83a3907548734d4a505992ebb94ec1454a91-1489173065780</breakoutRoom>
</breakoutRooms>
<meta>
<meetingId>f2041d123b6a4b994e7ad87ee9d348496a73472c-1489173065780</meetingId>
<meetingName>random-2810069 (Room - 2)</meetingName>
<isBreakout>true</isBreakout>
</meta>
<playback>
<format>presentation</format>
<link>http://192.168.23.22/playback/presentation/0.9.0/playback.html?meetingId=32ee8bcccfad34f85c58a12f87fc4268130a4fd3-1489173065780</link>
<processing_time>9841</processing_time>
<duration>126376</duration>
<extensions>
<preview>
<images>
<image width="176" height="136" alt="Welcome to">http://192.168.23.22/presentation/32ee8bcccfad34f85c58a12f87fc4268130a4fd3-1489173065780/presentation/743dd59a958334b4cdcdaa302846d0c0eadcf9ff-1489173070800/thumbnails/thumb-1.png</image>
</images>
</preview>
</extensions>
</playback>
</recording>

View File

@ -0,0 +1,78 @@
<#-- GET_RECORDINGS FreeMarker XML template -->
<response>
<#-- Where code is a 'SUCCESS' or 'FAILED' String -->
<returncode>${returnCode}</returncode>
<meetingName>${meeting.getName()}</meetingName>
<meetingID>${meeting.getExternalId()}</meetingID>
<internalMeetingID>${meeting.getInternalId()}</internalMeetingID>
<createTime>${meeting.getCreateTime()?c}</createTime>
<createDate>${createdOn}</createDate>
<voiceBridge>${meeting.getTelVoice()}</voiceBridge>
<dialNumber>${meeting.getDialNumber()}</dialNumber>
<attendeePW>${meeting.getViewerPassword()}</attendeePW>
<moderatorPW>${meeting.getModeratorPassword()}</moderatorPW>
<running>${meeting.isRunning()?c}</running>
<duration>${meeting.getDuration()}</duration>
<hasUserJoined>${meeting.hasUserJoined()?c}</hasUserJoined>
<recording>${meeting.isRecord()?c}</recording>
<hasBeenForciblyEnded>${meeting.isForciblyEnded()?c}</hasBeenForciblyEnded>
<startTime>${meeting.getStartTime()?c}</startTime>
<endTime>${meeting.getEndTime()}</endTime>
<participantCount>${meeting.getNumUsers()}</participantCount>
<listenerCount>${meeting.getNumListenOnly()}</listenerCount>
<voiceParticipantCount>${meeting.getNumVoiceJoined()}</voiceParticipantCount>
<videoCount>${meeting.getNumVideos()}</videoCount>
<maxUsers>${meeting.getMaxUsers()}</maxUsers>
<moderatorCount>${meeting.getNumModerators()}</moderatorCount>
<attendees>
<#list meeting.getUsers() as att>
<attendee>
<userID>${att.getInternalUserId()}</userID>
<fullName>${att.getFullname()}</fullName>
<role>${att.getRole()}</role>
<isPresenter>${att.isPresenter()?c}</isPresenter>
<isListeningOnly>${att.isListeningOnly()?c}</isListeningOnly>
<hasJoinedVoice>${att.isVoiceJoined()?c}</hasJoinedVoice>
<hasVideo>${att.hasVideo()?c}</hasVideo>
<#if meeting.getUserCustomData(att.getExternalUserId())??>
<#assign ucd = meeting.getUserCustomData(att.getExternalUserId())>
<customdata>
<#list ucd?keys as prop>
<${prop}><![CDATA[${ucd[prop]}]]></${prop}>
</#list>
</customdata>
</#if>
</attendee>
</#list>
</attendees>
<#assign m = meeting.getMetadata()>
<metadata>
<#list m?keys as prop>
<${prop}><![CDATA[${m[prop]}]]></${prop}>
</#list>
</metadata>
<#if messageKey?has_content>
<messageKey>${messageKey}</messageKey>
</#if>
<#if message?has_content>
<message>${message}</message>
</#if>
<#if meeting.isBreakout()>
<breakout>
<parentMeetingID>${meeting.getParentMeetingId()}</parentMeetingID>
<sequence>${meeting.getSequence()}</sequence>
</breakout>
</#if>
<#list meeting.getBreakoutRooms()>
<breakoutRooms>
<#items as room>
<breakout>${room}</breakout>
</#items>
</breakoutRooms>
</#list>
</response>

View File

@ -0,0 +1,78 @@
<#-- GET_RECORDINGS FreeMarker XML template -->
<response>
<#-- Where code is a 'SUCCESS' or 'FAILED' String -->
<returncode>${returnCode}</returncode>
<#list meetingDetailsList>
<meetings>
<#items as meetingDetail>
<#assign meeting = meetingDetail.getMeeting()>
<meeting>
<meetingName>${meeting.getName()}</meetingName>
<meetingID>${meeting.getExternalId()}</meetingID>
<internalMeetingID>${meeting.getInternalId()}</internalMeetingID>
<createTime>${meeting.getCreateTime()?c}</createTime>
<createDate>${meetingDetail.getCreatedOn()}</createDate>
<voiceBridge>${meeting.getTelVoice()}</voiceBridge>
<dialNumber>${meeting.getDialNumber()}</dialNumber>
<attendeePW>${meeting.getViewerPassword()}</attendeePW>
<moderatorPW>${meeting.getModeratorPassword()}</moderatorPW>
<running>${meeting.isRunning()?c}</running>
<duration>${meeting.getDuration()}</duration>
<hasUserJoined>${meeting.hasUserJoined()?c}</hasUserJoined>
<recording>${meeting.isRecord()?c}</recording>
<hasBeenForciblyEnded>${meeting.isForciblyEnded()?c}</hasBeenForciblyEnded>
<startTime>${meeting.getStartTime()?c}</startTime>
<endTime>${meeting.getEndTime()}</endTime>
<participantCount>${meeting.getNumUsers()}</participantCount>
<listenerCount>${meeting.getNumListenOnly()}</listenerCount>
<voiceParticipantCount>${meeting.getNumVoiceJoined()}</voiceParticipantCount>
<videoCount>${meeting.getNumVideos()}</videoCount>
<maxUsers>${meeting.getMaxUsers()}</maxUsers>
<moderatorCount>${meeting.getNumModerators()}</moderatorCount>
<attendees>
<#list meetingDetail.meeting.getUsers() as att>
<attendee>
<userID>${att.getInternalUserId()}</userID>
<fullName>${att.getFullname()}</fullName>
<role>${att.getRole()}</role>
<isPresenter>${att.isPresenter()?c}</isPresenter>
<isListeningOnly>${att.isListeningOnly()?c}</isListeningOnly>
<hasJoinedVoice>${att.isVoiceJoined()?c}</hasJoinedVoice>
<hasVideo>${att.hasVideo()?c}</hasVideo>
<#if meeting.getUserCustomData(att.getExternalUserId())??>
<#assign ucd = meetingDetail.meeting.getUserCustomData(att.getExternalUserId())>
<customdata>
<#list ucd?keys as prop>
<${prop}><![CDATA[${ucd[prop]}]]></${prop}>
</#list>
</customdata>
</#if>
</attendee>
</#list>
</attendees>
<#assign m = meetingDetail.meeting.getMetadata()>
<metadata>
<#list m?keys as prop>
<${prop}><![CDATA[${m[prop]}]]></${prop}>
</#list>
</metadata>
<#if meetingDetail.meeting.isBreakout()>
<breakout>
<parentMeetingID>${meetingDetail.meeting.getParentMeetingId()}</parentMeetingID>
<sequence>${meetingDetail.meeting.getSequence()}</sequence>
</breakout>
</#if>
<#list meetingDetail.meeting.getBreakoutRooms()>
<breakoutRooms>
<#items as room>
<breakout>${room}</breakout>
</#items>
</breakoutRooms>
</#list>
</meeting>
</#items>
</meetings>
</#list>
</response>

View File

@ -0,0 +1,68 @@
<#-- GET_RECORDINGS FreeMarker XML template -->
<#compress>
<response>
<#-- Where code is a 'SUCCESS' or 'FAILED' String -->
<returncode>${returnCode}</returncode>
<recordings>
<#list recordings as r>
<recording>
<recordID>${r.getId()}</recordID>
<meetingID>${r.getMeeting().getId()?html}</meetingID>
<externalMeetingID>${r.getMeeting().getExternalId()?html}</externalMeetingID>
<name><![CDATA[${r.getMeeting().getName()}]]></name>
<isBreakout>${r.getMeeting().isBreakout()?c}</isBreakout>
<published>${r.getPublished()?string}</published>
<state>${r.getState()?string}</state>
<startTime><#if r.getStartTime()?? && r.getStartTime() != "">${r.getStartTime()}</#if></startTime>
<endTime><#if r.getEndTime()?? && r.getEndTime() != "">${r.getEndTime()}</#if></endTime>
<participants><#if r.getParticipants()??>${r.getParticipants()}</#if></participants>
<#if r.getBreakout()??>
<#assign breakout = r.getBreakout()>
<breakout>
<parentId>${breakout.getParentMeetingId()}</parentId>
<sequence>${breakout.getSequence()?c}</sequence>
</breakout>
</#if>
<#if r.getBreakoutRooms()??>
<#list r.getBreakoutRooms()>
<breakoutRooms>
<#items as broom>
<breakoutRoom>${broom.getValue()}</breakoutRoom>
</#items>
</breakoutRooms>
</#list>
</#if>
<#assign m = r.getMeta().get()>
<metadata>
<#list m?keys as prop>
<${prop}><![CDATA[${m[prop]}]]></${prop}>
</#list>
</metadata>
<#assign pb = r.getPlayback()>
<playback>
<format>${pb.getFormat()}</format>
<link>${pb.getLink()}</link>
<processingTime>${pb.getProcessingTime()?c}</processingTime>
<duration>${pb.getDuration()?c}</duration>
<#if pb.getExtensions()??>
<extensions>
<#if pb.getExtensions().getPreview()??>
<#assign prev = pb.getExtensions().getPreview()>
<preview>
<#list prev.getImages()>
<images>
<#items as image>
<image width="${image.getWidth()}" height="${image.getHeight()}" alt="${image.getAlt()}">${image.getValue()}</image>
</#items>
</images>
</#list>
</preview>
</#if>
</extensions>
</#if>
</playback>
</recording>
</#list>
</recordings>
</response>
</#compress>

View File

@ -0,0 +1,22 @@
<recording>
<id>f3ffe06acedf425565cc024c8ebe89a6552e8782-1489172964374</id>
<state>published</state>
<published>true</published>
<start_time>1489172964374</start_time>
<end_time>1489173221647</end_time>
<breakoutRooms>
<breakoutRoom>32ee8bcccfad34f85c58a12f87fc4268130a4fd3-1489173065780</breakoutRoom>
<breakoutRoom>721d83a3907548734d4a505992ebb94ec1454a91-1489173065780</breakoutRoom>
</breakoutRooms>
<meta>
<isBreakout>false</isBreakout>
<meetingId>random-2810069</meetingId>
<meetingName>random-2810069</meetingName>
</meta>
<playback>
<format>presentation</format>
<link>http://192.168.23.22/playback/presentation/0.9.0/playback.html?meetingId=f3ffe06acedf425565cc024c8ebe89a6552e8782-1489172964374</link>
<processing_time>87598</processing_time>
<duration>226417</duration>
</playback>
</recording>

View File

@ -0,0 +1,18 @@
<recording>
<id>f3ffe06acedf425565cc024c8ebe89a6552e8782-1489095153417</id>
<state>published</state>
<published>true</published>
<start_time>1489095153417</start_time>
<end_time>1489095772224</end_time>
<meta>
<meetingId>random-2810069</meetingId>
<meetingName>random-2810069</meetingName>
<isBreakout>false</isBreakout>
</meta>
<playback>
<format>presentation</format>
<link>http://192.168.23.22/playback/presentation/0.9.0/playback.html?meetingId=f3ffe06acedf425565cc024c8ebe89a6552e8782-1489095153417</link>
<processing_time>191283</processing_time>
<duration>545949</duration>
</playback>
</recording>

View File

@ -0,0 +1,7 @@
<playback>
<format>presentation</format>
<link>http://192.168.23.22/playback/presentation/0.9.0/playback.html?meetingId=f3ffe06acedf425565cc024c8ebe89a6552e8782-1489095153417</link>
<processing_time>191283</processing_time>
<duration>545949</duration>
</playback>

View File

@ -0,0 +1,31 @@
<recording>
<id>f3ffe06acedf425565cc024c8ebe89a6552e8782-1489435015784</id>
<state>foo</state>
<published>false</published>
<start_time>1489435015784</start_time>
<end_time>1489435076055</end_time>
<meeting id="f3ffe06acedf425565cc024c8ebe89a6552e8782-1489435015784" externalId="random-2810069" name="random-2810069" breakout="false"/>
<breakout parentMeetingId="f3ffe06acedf425565cc024c8ebe89a6552e8782-1489172964374" sequence="2" meetingId="f2041d123b6a4b994e7ad87ee9d348496a73472c-1489173065780"/>
<breakoutRooms>
<breakoutRoom>32ee8bcccfad34f85c58a12f87fc4268130a4fd3-1489173065780</breakoutRoom>
<breakoutRoom>721d83a3907548734d4a505992ebb94ec1454a91-1489173065780</breakoutRoom>
</breakoutRooms>
<meta>
<meetingName>random-2810069</meetingName>
<meetingId>random-2810069</meetingId>
<isBreakout>false</isBreakout>
</meta>
<playback>
<format>presentation</format>
<link>http://192.168.23.22/playback/presentation/0.9.0/playback.html?meetingId=f3ffe06acedf425565cc024c8ebe89a6552e8782-1489435015784</link>
<processing_time>28627</processing_time>
<duration>32213</duration>
<extensions>
<preview>
<images>
<image width="176" height="136" alt="Welcome to">http://192.168.23.22/presentation/32ee8bcccfad34f85c58a12f87fc4268130a4fd3-1489173065780/presentation/743dd59a958334b4cdcdaa302846d0c0eadcf9ff-1489173070800/thumbnails/thumb-1.png</image>
</images>
</preview>
</extensions>
</playback>
</recording>

View File

@ -0,0 +1,12 @@
<?xml version="1.0"?>
<recording id="panzoom_events">
<event timestamp="0.0" orig="2440467878.0">
<viewBox>0.0 0.0 1600.0 1200.0</viewBox>
</event>
<event timestamp="19.1" orig="2440504635.0">
<viewBox>0.0 0.0 1600.0 1200.0</viewBox>
</event>
<event timestamp="22.6" orig="2440508157.0">
<viewBox>0.0 0.0 1600.0 1200.0</viewBox>
</event>
</recording>

View File

@ -0,0 +1,2 @@
<?xml version="1.0"?>
<popcorn/>

View File

@ -0,0 +1,23 @@
package org.bigbluebutton.api.util
import org.scalatest._
class ParamsUtilTest extends UnitSpec {
it should "strip out control chars from text" in {
val text = "a\u0000b\u0007c\u008fd"
val cleaned = ParamsUtil.stripControlChars(text)
assert("abcd" == cleaned)
}
it should "complain about invalid chars in meetingId" in {
val meetingId = "Demo , Meeting"
assert(ParamsUtil.isValidMeetingId(meetingId) == false)
}
it should "accept valid chars in meetingId" in {
val meetingId = "Demo Meeting - 123"
assert(ParamsUtil.isValidMeetingId(meetingId) == true)
}
}

View File

@ -0,0 +1,79 @@
package org.bigbluebutton.api.util
import java.io.{File, FileInputStream}
import javax.xml.stream.{XMLInputFactory, XMLStreamReader}
import com.fasterxml.jackson.dataformat.xml.{JacksonXmlModule, XmlMapper}
import org.bigbluebutton.api.domain.{RecordingMetadata, RecordingMetadataPlayback}
class RecordingMetadataReaderHelperTest extends UnitSpec {
it should "deserialize playback part of metadata.xml" in {
val factory: XMLInputFactory = XMLInputFactory.newInstance();
val xml = new File("src/test/resources/playback-metadata.xml")
val module: JacksonXmlModule = new JacksonXmlModule();
// and then configure, for example:
module.setDefaultUseWrapper(false);
val mapper: XmlMapper = new XmlMapper(module)
//Reading from xml file and creating XMLStreamReader
val reader: XMLStreamReader = factory.createXMLStreamReader(new FileInputStream(xml))
val playback: RecordingMetadataPlayback = mapper.readValue(reader, classOf[RecordingMetadataPlayback])
println("***** FOOO =" + mapper.writeValueAsString(playback))
assert(playback.getDuration == 545949)
}
it should "deserialize metadata.xml" in {
val factory: XMLInputFactory = XMLInputFactory.newInstance();
val xml = new File("src/test/resources/breakout-room-metadata.xml")
val module: JacksonXmlModule = new JacksonXmlModule();
// and then configure, for example:
module.setDefaultUseWrapper(false);
val mapper: XmlMapper = new XmlMapper(module)
//Reading from xml file and creating XMLStreamReader
val reader: XMLStreamReader = factory.createXMLStreamReader(new FileInputStream(xml))
val recMeta: RecordingMetadata = mapper.readValue(reader, classOf[RecordingMetadata])
println("***** FOOO =" + mapper.writeValueAsString(recMeta))
assert(recMeta.getPlayback.getDuration == 126376)
}
it should "save metadata.xml" in {
val factory: XMLInputFactory = XMLInputFactory.newInstance();
val xml = new File("src/test/resources/breakout-room-metadata.xml")
val module: JacksonXmlModule = new JacksonXmlModule();
// and then configure, for example:
module.setDefaultUseWrapper(false);
val mapper: XmlMapper = new XmlMapper(module)
//Reading from xml file and creating XMLStreamReader
val reader: XMLStreamReader = factory.createXMLStreamReader(new FileInputStream(xml))
val recMeta: RecordingMetadata = mapper.readValue(reader, classOf[RecordingMetadata])
recMeta.getMeta().set("FOO", "BAR");
val metadataXml = RecordingMetadataReaderHelper.getMetadataXmlLocation("target")
if (metadataXml.exists()) metadataXml.delete()
RecordingMetadataReaderHelper.saveRecordingMetadata(metadataXml, recMeta)
assert(metadataXml.exists())
}
}

View File

@ -0,0 +1,77 @@
package org.bigbluebutton.api.util
import java.io.File
import java.util
import org.apache.commons.io.FileUtils
import org.bigbluebutton.api.RecordingService
class RecordingServiceTest extends UnitSpec {
it should "deserialize playback part of metadata.xml" in {
val srcXml = new File("src/test/resources/breakout-room-metadata.xml")
val metaParams: util.Map[String, String] = new util.TreeMap[String, String]()
metaParams.put("foo", "bar")
metaParams.put("bar", "baz")
val destXml = new File("target/updated-metadata.xml")
RecordingService.updateRecordingMetadata(srcXml, metaParams, destXml)
}
it should "publish recording" in {
// Make a copy of our sample recording
val destDir = new File("target/sample-recording/publish")
if (destDir.exists()) FileUtils.deleteDirectory(destDir)
val srcDir = new File("src/test/resources/sample-recording")
FileUtils.copyDirectory(srcDir, destDir)
val recordingId = "foo"
val recordingDir = new File(destDir.getPath() + File.separatorChar + recordingId)
val publishedDir = new File("target/recording/published")
RecordingService.publishRecording(publishedDir, recordingId, recordingDir)
assert(true)
}
it should "unpublish recording" in {
// Make a copy of our sample recording
val destDir = new File("target/sample-recording/unpublish")
if (destDir.exists()) FileUtils.deleteDirectory(destDir)
val srcDir = new File("src/test/resources/sample-recording")
FileUtils.copyDirectory(srcDir, destDir)
val recordingId = "foo"
val recordingDir = new File(destDir.getPath() + File.separatorChar + recordingId)
val unpublishedDir = new File("target/recording/unpublished")
if (unpublishedDir.exists()) FileUtils.deleteDirectory(unpublishedDir)
RecordingService.unpublishRecording(unpublishedDir, recordingId, recordingDir)
assert(unpublishedDir.exists())
}
it should "delete recording" in {
// Make a copy of our sample recording
val destDir = new File("target/sample-recording/delete")
if (destDir.exists()) FileUtils.deleteDirectory(destDir)
val srcDir = new File("src/test/resources/sample-recording")
FileUtils.copyDirectory(srcDir, destDir)
val recordingId = "foo"
val recordingDir = new File(destDir.getPath() + File.separatorChar + recordingId)
val deletedDir = new File("target/recording/deleted")
if (deletedDir.exists()) FileUtils.deleteDirectory(deletedDir)
RecordingService.deleteRecording(deletedDir, recordingId, recordingDir)
assert(deletedDir.exists())
}
}

View File

@ -0,0 +1,199 @@
package org.bigbluebutton.api.util
import java.io.File
import java.util
import org.bigbluebutton.api.domain.{Meeting, RecordingMetadata, User}
import org.scalatest._
class ResponseBuilderTest extends UnitSpec {
it should "find template" in {
val current = new java.io.File( "." ).getCanonicalPath()
println("Current dir:"+current)
val meetingInfo = new util.TreeMap[String, String]()
meetingInfo.put("foo", "foo")
meetingInfo.put("bar", "baz")
val meeting: Meeting = new Meeting.Builder("extMid", "intMid", System.currentTimeMillis())
.withName("Demo Meeting").withMaxUsers(25)
.withModeratorPass("mp").withViewerPass("ap")
.withRecording(true).withDuration(600)
.withLogoutUrl("/logoutUrl").withTelVoice("85115").withWebVoice("85115")
.withDialNumber("6135551234").withDefaultAvatarURL("/avatar")
.withAutoStartRecording(false).withAllowStartStopRecording(true)
.withWebcamsOnlyForModerator(false).withMetadata(meetingInfo)
.withWelcomeMessageTemplate("hello").withWelcomeMessage("hello")
.isBreakout(false).build
meeting.setParentMeetingId("ParentMeetingId")
meeting.setSequence(0);
meeting.addBreakoutRoom("breakout-room-id-1")
meeting.addBreakoutRoom("breakout-room-id-2")
meeting.addBreakoutRoom("breakout-room-id-3")
val user: User = new User("uid1", "extuid1", "Richard", "moderator", "/aygwapo")
meeting.userJoined(user)
val user2: User = new User("uid2", "extuid2", "Richard 2", "moderator", "/aygwapo")
meeting.userJoined(user2)
val user3: User = new User("uid3", "extuid3", "Richard 3", "moderator", "/aygwapo")
meeting.userJoined(user2)
val custData = new util.HashMap[String, String]()
custData.put("gwapo", "totoo")
meeting.addUserCustomData("extuid1", custData)
val templateLoc = new File("src/test/resources")
val builder = new ResponseBuilder(templateLoc)
def response = builder.buildGetMeetingInfoResponse(meeting, "success")
// println(response)
assert(templateLoc.exists())
}
it should "return meetings" in {
val meetingInfo1 = new util.TreeMap[String, String]()
meetingInfo1.put("foo", "foo")
meetingInfo1.put("bar", "baz")
val meeting1: Meeting = new Meeting.Builder("extMid1", "intMid1", System.currentTimeMillis())
.withName("Demo Meeting 1").withMaxUsers(25)
.withModeratorPass("mp").withViewerPass("ap")
.withRecording(true).withDuration(600)
.withLogoutUrl("/logoutUrl").withTelVoice("85115").withWebVoice("85115")
.withDialNumber("6135551234").withDefaultAvatarURL("/avatar")
.withAutoStartRecording(false).withAllowStartStopRecording(true)
.withWebcamsOnlyForModerator(false).withMetadata(meetingInfo1)
.withWelcomeMessageTemplate("hello").withWelcomeMessage("hello")
.isBreakout(false).build
meeting1.setParentMeetingId("ParentMeetingId")
meeting1.setSequence(0);
meeting1.addBreakoutRoom("breakout-room-id-1")
meeting1.addBreakoutRoom("breakout-room-id-2")
meeting1.addBreakoutRoom("breakout-room-id-3")
val userm11: User = new User("uid1", "extuid1", "Richard", "moderator", "/aygwapo")
meeting1.userJoined(userm11)
val userm12: User = new User("uid2", "extuid2", "Richard 2", "moderator", "/aygwapo")
meeting1.userJoined(userm12)
val userm13: User = new User("uid3", "extuid3", "Richard 3", "moderator", "/aygwapo")
meeting1.userJoined(userm13)
val custDatam1 = new util.HashMap[String, String]()
custDatam1.put("gwapo", "totoo")
meeting1.addUserCustomData("extuid1", custDatam1)
val meetingInfo2 = new util.TreeMap[String, String]()
meetingInfo2.put("foo", "foo")
meetingInfo2.put("bar", "baz")
val meeting2: Meeting = new Meeting.Builder("extMid2", "intMid2", System.currentTimeMillis())
.withName("Demo Meeting 2").withMaxUsers(25)
.withModeratorPass("mp").withViewerPass("ap")
.withRecording(true).withDuration(600)
.withLogoutUrl("/logoutUrl").withTelVoice("85115").withWebVoice("85115")
.withDialNumber("6135551234").withDefaultAvatarURL("/avatar")
.withAutoStartRecording(false).withAllowStartStopRecording(true)
.withWebcamsOnlyForModerator(false).withMetadata(meetingInfo2)
.withWelcomeMessageTemplate("hello").withWelcomeMessage("hello")
.isBreakout(false).build
meeting2.setParentMeetingId("ParentMeetingId")
meeting2.setSequence(0);
meeting2.addBreakoutRoom("breakout-room-id-1")
meeting2.addBreakoutRoom("breakout-room-id-2")
meeting2.addBreakoutRoom("breakout-room-id-3")
val userm21: User = new User("uid1", "extuid1", "Richard", "moderator", "/aygwapo")
meeting2.userJoined(userm21)
val userm22: User = new User("uid2", "extuid2", "Richard 2", "moderator", "/aygwapo")
meeting2.userJoined(userm22)
val userm23: User = new User("uid3", "extuid3", "Richard 3", "moderator", "/aygwapo")
meeting2.userJoined(userm23)
val custDatam2 = new util.HashMap[String, String]()
custDatam2.put("gwapo", "totoo")
meeting2.addUserCustomData("extuid1", custDatam2)
val meetingInfo3 = new util.TreeMap[String, String]()
meetingInfo3.put("foo", "foo")
meetingInfo3.put("bar", "baz")
val meeting3: Meeting = new Meeting.Builder("extMid", "intMid", System.currentTimeMillis())
.withName("Demo Meeting").withMaxUsers(25)
.withModeratorPass("mp").withViewerPass("ap")
.withRecording(true).withDuration(600)
.withLogoutUrl("/logoutUrl").withTelVoice("85115").withWebVoice("85115")
.withDialNumber("6135551234").withDefaultAvatarURL("/avatar")
.withAutoStartRecording(false).withAllowStartStopRecording(true)
.withWebcamsOnlyForModerator(false).withMetadata(meetingInfo3)
.withWelcomeMessageTemplate("hello").withWelcomeMessage("hello")
.isBreakout(false).build
meeting3.setParentMeetingId("ParentMeetingId")
meeting3.setSequence(0);
meeting3.addBreakoutRoom("breakout-room-id-1")
meeting3.addBreakoutRoom("breakout-room-id-2")
meeting3.addBreakoutRoom("breakout-room-id-3")
val user: User = new User("uid1", "extuid1", "Richard", "moderator", "/aygwapo")
meeting3.userJoined(user)
val user2: User = new User("uid2", "extuid2", "Richard 2", "moderator", "/aygwapo")
meeting3.userJoined(user2)
val user3: User = new User("uid3", "extuid3", "Richard 3", "moderator", "/aygwapo")
meeting3.userJoined(user2)
val custData = new util.HashMap[String, String]()
custData.put("gwapo", "totoo")
meeting3.addUserCustomData("extuid1", custData)
val meetings = new util.ArrayList[Meeting]()
meetings.add(meeting1)
meetings.add(meeting2)
meetings.add(meeting3)
val templateLoc = new File("src/test/resources")
val builder = new ResponseBuilder(templateLoc)
def response = builder.buildGetMeetingsResponse(meetings, "success")
// println(response)
assert(templateLoc.exists())
}
it should "reply to getRecordings api call" in {
val templateLoc = new File("src/test/resources")
val builder = new ResponseBuilder(templateLoc)
val metadataXml = new File("src/test/resources/breakout-room-metadata.xml")
val recMeta = RecordingMetadataReaderHelper.getRecordingMetadata(metadataXml);
val recList = new util.ArrayList[RecordingMetadata]()
recList.add(recMeta)
def response = builder.buildGetRecordingsResponse(recList, "success")
println(response)
assert(templateLoc.exists())
}
}

View File

@ -0,0 +1,8 @@
package org.bigbluebutton.api.util
import org.scalatest.FlatSpec
import org.scalatest.BeforeAndAfterAll
import org.scalatest.WordSpec
import org.scalatest.Matchers
abstract class UnitSpec extends FlatSpec with Matchers with BeforeAndAfterAll

View File

@ -16,8 +16,7 @@ dependencies {
//redis
compile 'redis.clients:jedis:2.7.2'
compile 'org.apache.commons:commons-pool2:2.3'
compile 'commons-lang:commons-lang:2.5'
compile 'commons-io:commons-io:2.4'
compile 'commons-codec:commons-codec:1.10'
compile 'com.google.code.gson:gson:1.7.1'
@ -26,6 +25,7 @@ dependencies {
compile 'com.zaxxer:nuprocess:1.1.0'
compile 'org.bigbluebutton:bbb-common-message:0.0.18-SNAPSHOT'
compile 'org.bigbluebutton:bbb-common-web:0.0.1-SNAPSHOT'
// Logging
// Commenting out as it results in build failure (ralam - may 11, 2014)

View File

@ -19,6 +19,8 @@
package org.bigbluebutton.web.controllers
import com.google.gson.Gson
import org.bigbluebutton.api.domain.RecordingMetadata
import org.bigbluebutton.api.util.ResponseBuilder
import javax.servlet.ServletRequest;
@ -53,9 +55,10 @@ import org.bigbluebutton.web.services.turn.StunTurnService;
import org.bigbluebutton.web.services.turn.TurnEntry;
import org.json.JSONArray;
import org.json.JSONObject;
import org.bigbluebutton.api.util.ResponseBuilder
import freemarker.template.Configuration;
import freemarker.cache.WebappTemplateLoader;
import java.io.File;
class ApiController {
private static final Integer SESSION_TIMEOUT = 14400 // 4 hours
@ -75,6 +78,8 @@ class ApiController {
PresentationUrlDownloadService presDownloadService
StunTurnService stunTurnService
/* general methods */
def index = {
log.debug CONTROLLER_NAME + "#index"
@ -791,7 +796,15 @@ class ApiController {
return;
}
respondWithConferenceDetails(meeting, null, null, null);
def templateLoc = getServletContext().getRealPath("/WEB-INF/freemarker")
ResponseBuilder responseBuilder = new ResponseBuilder(new File(templateLoc))
def xmlText = responseBuilder.buildGetMeetingInfoResponse(meeting, "success")
withFormat {
xml {
render(text: xmlText, contentType: "text/xml")
}
}
}
/************************************
@ -850,41 +863,14 @@ class ApiController {
}
} else {
response.addHeader("Cache-Control", "no-cache")
def templateLoc = getServletContext().getRealPath("/WEB-INF/freemarker")
ResponseBuilder responseBuilder = new ResponseBuilder(new File(templateLoc))
def xmlText = responseBuilder.buildGetMeetingsResponse(mtgs, "success")
withFormat {
xml {
render(contentType:"text/xml") {
response() {
returncode(RESP_CODE_SUCCESS)
meetings {
for (m in mtgs) {
meeting {
meetingID() { mkp.yield(m.getExternalId()) }
internalMeetingID() { mkp.yield(m.getInternalId()) }
if (m.isBreakout()) {
parentMeetingID() { mkp.yield(m.getParentMeetingId()) }
sequence(m.getSequence())
}
isBreakout() { mkp.yield(m.isBreakout()) }
meetingName() { mkp.yield(m.getName()) }
createTime(m.getCreateTime())
createDate(formatPrettyDate(m.getCreateTime()))
voiceBridge() { mkp.yield(m.getTelVoice()) }
dialNumber() { mkp.yield(m.getDialNumber()) }
attendeePW() { mkp.yield(m.getViewerPassword()) }
moderatorPW() { mkp.yield(m.getModeratorPassword()) }
hasBeenForciblyEnded(m.isForciblyEnded() ? "true" : "false")
running(m.isRunning() ? "true" : "false")
participantCount(m.getNumUsers())
listenerCount(m.getNumListenOnly())
voiceParticipantCount(m.getNumVoiceJoined())
videoCount(m.getNumVideos())
duration(m.duration)
hasUserJoined(m.hasUserJoined())
}
}
}
}
}
render(text: xmlText, contentType: "text/xml")
}
}
}
@ -1750,6 +1736,8 @@ class ApiController {
return
}
log.debug request.getQueryString()
// Do we agree on the checksum? If not, complain.
if (! paramsProcessorUtil.isChecksumSame(API_CALL, params.checksum, request.getQueryString())) {
errors.checksumError()
@ -1779,8 +1767,12 @@ class ApiController {
internalRecordIds = paramsProcessorUtil.convertToInternalMeetingId(externalMeetingIds);
}
Map<String,Recording> recs = meetingService.getRecordings(internalRecordIds, states);
recs = meetingService.filterRecordingsByMetadata(recs, ParamsProcessorUtil.processMetaParam(params));
for(String intRecId : internalRecordIds){
log.debug intRecId
}
List<RecordingMetadata> recsList = meetingService.getRecordingsMetadata(internalRecordIds, states);
List<RecordingMetadata> recs = meetingService.filterRecordingsByMetadata(recsList, ParamsProcessorUtil.processMetaParam(params));
if (recs.isEmpty()) {
response.addHeader("Cache-Control", "no-cache")
@ -1798,18 +1790,14 @@ class ApiController {
}
return;
}
def cfg = new Configuration()
// Load the XML template
// TODO: Maybe there is a better way to define the templates path
def wtl = new WebappTemplateLoader(getServletContext(), "/WEB-INF/freemarker")
cfg.setTemplateLoader(wtl)
def ftl = cfg.getTemplate("get-recordings.ftl")
def xmlText = new StringWriter()
ftl.process([code:RESP_CODE_SUCCESS, recs:recs.values()], xmlText)
def templateLoc = getServletContext().getRealPath("/WEB-INF/freemarker")
ResponseBuilder responseBuilder = new ResponseBuilder(new File(templateLoc))
def xmlText = responseBuilder.buildGetRecordingsResponse(recs, "success")
withFormat {
xml {
render(text: xmlText.toString(), contentType: "text/xml")
render(text: xmlText, contentType: "text/xml")
}
}
}

View File

@ -68,6 +68,10 @@ public class ApiErrors {
errors.add(new String[] {"maxParticipantsReached", "The number of participants allowed for this meeting has been reached."});
}
public void addError(String[] error) {
errors.add(error);
}
public boolean hasErrors() {
return errors.size() > 0;
}

View File

@ -37,11 +37,8 @@ import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import org.bigbluebutton.api.domain.Meeting;
import org.bigbluebutton.api.domain.Playback;
import org.bigbluebutton.api.domain.Recording;
import org.bigbluebutton.api.domain.User;
import org.bigbluebutton.api.domain.UserSession;
import org.bigbluebutton.api.domain.*;
import org.bigbluebutton.api.messaging.MessageListener;
import org.bigbluebutton.api.messaging.MessagingService;
import org.bigbluebutton.api.messaging.messages.CreateBreakoutRoom;
@ -302,6 +299,14 @@ public class MeetingService implements MessageListener {
}
private void handleCreateMeeting(Meeting m) {
if (m.isBreakout()){
Meeting parent = meetings.get(m.getParentMeetingId());
parent.addBreakoutRoom(m.getExternalId());
if (parent.isRecord()) {
messagingService.addBreakoutRoom(parent.getInternalId(), m.getInternalId());
}
}
if (m.isRecord()) {
Map<String, String> metadata = new TreeMap<String, String>();
metadata.putAll(m.getMetadata());
@ -310,13 +315,15 @@ public class MeetingService implements MessageListener {
metadata.put("meetingName", m.getName());
metadata.put("isBreakout", m.isBreakout().toString());
Map<String, String> breakoutMetadata = new TreeMap<String, String>();
breakoutMetadata.put("meetingId", m.getExternalId());
if (m.isBreakout()){
messagingService.recordMeetingInfo(m.getInternalId(), metadata);
if (m.isBreakout()) {
Map<String, String> breakoutMetadata = new TreeMap<String, String>();
breakoutMetadata.put("meetingId", m.getExternalId());
breakoutMetadata.put("sequence", m.getSequence().toString());
breakoutMetadata.put("parentMeetingId", m.getParentMeetingId());
messagingService.recordBreakoutInfo(m.getInternalId(), breakoutMetadata);
}
messagingService.recordMeetingInfo(m.getInternalId(), metadata, breakoutMetadata);
}
Map<String, Object> logData = new HashMap<String, Object>();
@ -415,18 +422,30 @@ public class MeetingService implements MessageListener {
return null;
}
public List<RecordingMetadata> getRecordingsMetadata(List<String> idList, List<String> states) {
List<RecordingMetadata> recsList = recordingService.getRecordingsMetadata(idList, states);
return recsList;
}
public Map<String, Recording> getRecordings(List<String> idList, List<String> states) {
List<Recording> recsList = recordingService.getRecordings(idList, states);
Map<String, Recording> recs = reorderRecordings(recsList);
return recs;
}
public Map<String, Recording> filterRecordingsByMetadata(
Map<String, Recording> recordings,
Map<String, String> metadataFilters) {
public List<RecordingMetadata> filterRecordingsByMetadata(List<RecordingMetadata> recsList,
Map<String, String> metadataFilters) {
return recordingService.filterRecordingsByMetadata(recsList, metadataFilters);
}
public Map<String, Recording> filterRecordingsByMetadata(Map<String, Recording> recordings,
Map<String, String> metadataFilters) {
return recordingService.filterRecordingsByMetadata(recordings, metadataFilters);
}
public Map<String, Recording> reorderRecordings(List<Recording> olds) {
Map<String, Recording> map = new HashMap<String, Recording>();
for (Recording r : olds) {
@ -498,8 +517,7 @@ public class MeetingService implements MessageListener {
}
}
public void updateRecordings(List<String> idList,
Map<String, String> metaParams) {
public void updateRecordings(List<String> idList, Map<String, String> metaParams) {
recordingService.updateMetaParams(idList, metaParams);
}

View File

@ -37,6 +37,7 @@ import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.commons.lang.StringUtils;
import org.bigbluebutton.api.domain.Meeting;
import org.bigbluebutton.api.util.ParamsUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.commons.httpclient.*;
@ -103,9 +104,14 @@ public class ParamsProcessorUtil {
// Do we have a meeting id? If not, complain.
if(!StringUtils.isEmpty(params.get("meetingID"))) {
if (StringUtils.isEmpty(StringUtils.strip(params.get("meetingID")))) {
String meetingId = StringUtils.strip(params.get("meetingID"));
if (StringUtils.isEmpty(meetingId)) {
errors.missingParamError("meetingID");
}
} else {
if (! ParamsUtil.isValidMeetingId(meetingId)) {
errors.addError(new String[] {"invalidFormat", "Meeting id contains invalid characters."});
}
}
} else {
errors.missingParamError("meetingID");
}
@ -290,10 +296,14 @@ public class ParamsProcessorUtil {
}
public Meeting processCreateParams(Map<String, String> params) {
String meetingName = params.get("name");
if (meetingName == null) {
meetingName = "";
}
meetingName = ParamsUtil.stripControlChars(meetingName);
String externalMeetingId = params.get("meetingID");
String viewerPass = processPassword(params.get("attendeePW"));

View File

@ -27,7 +27,9 @@ import java.util.Map;
import java.util.Set;
public interface MessagingService {
void recordMeetingInfo(String meetingId, Map<String, String> info, Map<String, String> breakoutInfo);
void recordMeetingInfo(String meetingId, Map<String, String> info);
void recordBreakoutInfo(String meetingId, Map<String, String> breakoutInfo);
void addBreakoutRoom(String parentId, String breakoutId);
void destroyMeeting(String meetingID);
void createMeeting(String meetingID, String externalMeetingID,
String parentMeetingID, String meetingName, Boolean recorded,

View File

@ -49,8 +49,16 @@ public class RedisMessagingService implements MessagingService {
private MessageSender sender;
private ToJsonEncoder encoder = new ToJsonEncoder();
public void recordMeetingInfo(String meetingId, Map<String, String> info, Map<String, String> breakoutInfo) {
storeService.recordMeetingInfo(meetingId, info, breakoutInfo);
public void recordMeetingInfo(String meetingId, Map<String, String> info) {
storeService.recordMeetingInfo(meetingId, info);
}
public void recordBreakoutInfo(String meetingId, Map<String, String> breakoutInfo) {
storeService.recordBreakoutInfo(meetingId, breakoutInfo);
}
public void addBreakoutRoom(String parentId, String breakoutId) {
storeService.addBreakoutRoom(parentId, breakoutId);
}
public void destroyMeeting(String meetingID) {

View File

@ -30,8 +30,7 @@ public class RedisStorageService {
Protocol.DEFAULT_DATABASE, "BbbRed5AppsPub");
}
public void recordMeetingInfo(String meetingId, Map<String, String> info,
Map<String, String> breakoutInfo) {
public void recordMeetingInfo(String meetingId, Map<String, String> info) {
Jedis jedis = redisPool.getResource();
try {
for (String key : info.keySet()) {
@ -40,20 +39,37 @@ public class RedisStorageService {
log.debug("Saving metadata in {}", meetingId);
jedis.hmset("meeting:info:" + meetingId, info);
for (String breakoutKey : breakoutInfo.keySet()) {
log.debug("Storing breakout metadata {} = {}", breakoutKey,
breakoutInfo.get(breakoutKey));
}
log.debug("Saving breakout metadata in {}", meetingId);
jedis.hmset("meeting:breakout:" + meetingId, breakoutInfo);
} catch (Exception e) {
log.warn("Cannot record the info meeting:" + meetingId, e);
} finally {
jedis.close();
}
}
public void recordBreakoutInfo(String meetingId, Map<String, String> breakoutInfo) {
Jedis jedis = redisPool.getResource();
try {
log.debug("Saving breakout metadata in {}", meetingId);
jedis.hmset("meeting:breakout:" + meetingId, breakoutInfo);
} catch (Exception e) {
log.warn("Cannot record the info meeting:" + meetingId, e);
} finally {
jedis.close();
}
}
public void addBreakoutRoom(String parentId, String breakoutId) {
Jedis jedis = redisPool.getResource();
try {
log.debug("Saving breakout room for meeting {}", parentId);
jedis.sadd("meeting:breakout:rooms:" + parentId, breakoutId);
} catch (Exception e) {
log.warn("Cannot record the info meeting:" + parentId, e);
} finally {
jedis.close();
}
}
public void removeMeeting(String meetingId){
Jedis jedis = redisPool.getResource();

View File

@ -0,0 +1,78 @@
<#-- GET_RECORDINGS FreeMarker XML template -->
<response>
<#-- Where code is a 'SUCCESS' or 'FAILED' String -->
<returncode>${returnCode}</returncode>
<meetingName>${meeting.getName()}</meetingName>
<meetingID>${meeting.getExternalId()}</meetingID>
<internalMeetingID>${meeting.getInternalId()}</internalMeetingID>
<createTime>${meeting.getCreateTime()?c}</createTime>
<createDate>${createdOn}</createDate>
<voiceBridge>${meeting.getTelVoice()}</voiceBridge>
<dialNumber>${meeting.getDialNumber()}</dialNumber>
<attendeePW>${meeting.getViewerPassword()}</attendeePW>
<moderatorPW>${meeting.getModeratorPassword()}</moderatorPW>
<running>${meeting.isRunning()?c}</running>
<duration>${meeting.getDuration()}</duration>
<hasUserJoined>${meeting.hasUserJoined()?c}</hasUserJoined>
<recording>${meeting.isRecord()?c}</recording>
<hasBeenForciblyEnded>${meeting.isForciblyEnded()?c}</hasBeenForciblyEnded>
<startTime>${meeting.getStartTime()?c}</startTime>
<endTime>${meeting.getEndTime()}</endTime>
<participantCount>${meeting.getNumUsers()}</participantCount>
<listenerCount>${meeting.getNumListenOnly()}</listenerCount>
<voiceParticipantCount>${meeting.getNumVoiceJoined()}</voiceParticipantCount>
<videoCount>${meeting.getNumVideos()}</videoCount>
<maxUsers>${meeting.getMaxUsers()}</maxUsers>
<moderatorCount>${meeting.getNumModerators()}</moderatorCount>
<attendees>
<#list meeting.getUsers() as att>
<attendee>
<userID>${att.getInternalUserId()}</userID>
<fullName>${att.getFullname()}</fullName>
<role>${att.getRole()}</role>
<isPresenter>${att.isPresenter()?c}</isPresenter>
<isListeningOnly>${att.isListeningOnly()?c}</isListeningOnly>
<hasJoinedVoice>${att.isVoiceJoined()?c}</hasJoinedVoice>
<hasVideo>${att.hasVideo()?c}</hasVideo>
<#if meeting.getUserCustomData(att.getExternalUserId())??>
<#assign ucd = meeting.getUserCustomData(att.getExternalUserId())>
<customdata>
<#list ucd?keys as prop>
<${prop}><![CDATA[${ucd[prop]}]]></${prop}>
</#list>
</customdata>
</#if>
</attendee>
</#list>
</attendees>
<#assign m = meeting.getMetadata()>
<metadata>
<#list m?keys as prop>
<${prop}><![CDATA[${m[prop]}]]></${prop}>
</#list>
</metadata>
<#if messageKey?has_content>
<messageKey>${messageKey}</messageKey>
</#if>
<#if message?has_content>
<message>${message}</message>
</#if>
<#if meeting.isBreakout()>
<breakout>
<parentMeetingID>${meeting.getParentMeetingId()}</parentMeetingID>
<sequence>${meeting.getSequence()}</sequence>
</breakout>
</#if>
<#list meeting.getBreakoutRooms()>
<breakoutRooms>
<#items as room>
<breakout>${room}</breakout>
</#items>
</breakoutRooms>
</#list>
</response>

View File

@ -0,0 +1,78 @@
<#-- GET_RECORDINGS FreeMarker XML template -->
<response>
<#-- Where code is a 'SUCCESS' or 'FAILED' String -->
<returncode>${returnCode}</returncode>
<#list meetingDetailsList>
<meetings>
<#items as meetingDetail>
<#assign meeting = meetingDetail.getMeeting()>
<meeting>
<meetingName>${meeting.getName()}</meetingName>
<meetingID>${meeting.getExternalId()}</meetingID>
<internalMeetingID>${meeting.getInternalId()}</internalMeetingID>
<createTime>${meeting.getCreateTime()?c}</createTime>
<createDate>${meetingDetail.getCreatedOn()}</createDate>
<voiceBridge>${meeting.getTelVoice()}</voiceBridge>
<dialNumber>${meeting.getDialNumber()}</dialNumber>
<attendeePW>${meeting.getViewerPassword()}</attendeePW>
<moderatorPW>${meeting.getModeratorPassword()}</moderatorPW>
<running>${meeting.isRunning()?c}</running>
<duration>${meeting.getDuration()}</duration>
<hasUserJoined>${meeting.hasUserJoined()?c}</hasUserJoined>
<recording>${meeting.isRecord()?c}</recording>
<hasBeenForciblyEnded>${meeting.isForciblyEnded()?c}</hasBeenForciblyEnded>
<startTime>${meeting.getStartTime()?c}</startTime>
<endTime>${meeting.getEndTime()}</endTime>
<participantCount>${meeting.getNumUsers()}</participantCount>
<listenerCount>${meeting.getNumListenOnly()}</listenerCount>
<voiceParticipantCount>${meeting.getNumVoiceJoined()}</voiceParticipantCount>
<videoCount>${meeting.getNumVideos()}</videoCount>
<maxUsers>${meeting.getMaxUsers()}</maxUsers>
<moderatorCount>${meeting.getNumModerators()}</moderatorCount>
<attendees>
<#list meetingDetail.meeting.getUsers() as att>
<attendee>
<userID>${att.getInternalUserId()}</userID>
<fullName>${att.getFullname()}</fullName>
<role>${att.getRole()}</role>
<isPresenter>${att.isPresenter()?c}</isPresenter>
<isListeningOnly>${att.isListeningOnly()?c}</isListeningOnly>
<hasJoinedVoice>${att.isVoiceJoined()?c}</hasJoinedVoice>
<hasVideo>${att.hasVideo()?c}</hasVideo>
<#if meeting.getUserCustomData(att.getExternalUserId())??>
<#assign ucd = meetingDetail.meeting.getUserCustomData(att.getExternalUserId())>
<customdata>
<#list ucd?keys as prop>
<${prop}><![CDATA[${ucd[prop]}]]></${prop}>
</#list>
</customdata>
</#if>
</attendee>
</#list>
</attendees>
<#assign m = meetingDetail.meeting.getMetadata()>
<metadata>
<#list m?keys as prop>
<${prop}><![CDATA[${m[prop]}]]></${prop}>
</#list>
</metadata>
<#if meetingDetail.meeting.isBreakout()>
<breakout>
<parentMeetingID>${meetingDetail.meeting.getParentMeetingId()}</parentMeetingID>
<sequence>${meetingDetail.meeting.getSequence()}</sequence>
</breakout>
</#if>
<#list meetingDetail.meeting.getBreakoutRooms()>
<breakoutRooms>
<#items as room>
<breakout>${room}</breakout>
</#items>
</breakoutRooms>
</#list>
</meeting>
</#items>
</meetings>
</#list>
</response>

View File

@ -2,62 +2,64 @@
<#compress>
<response>
<#-- Where code is a 'SUCCESS' or 'FAILED' String -->
<returncode>${code}</returncode>
<returncode>${returnCode}</returncode>
<recordings>
<#-- Where recs is a String -> Recording HashMap -->
<#list recs as r>
<#list recordings as r>
<recording>
<recordID>${r.getId()}</recordID>
<meetingID><#if r.getMeetingID()?? && r.getMeetingID() != "">${r.getMeetingID()?html}</#if></meetingID>
<name><#if r.getName()?? && r.getName() != ""><![CDATA[${r.getName()}]]></#if></name>
<published>${r.isPublished()?string}</published>
<meetingID>${r.getMeeting().getId()?html}</meetingID>
<externalMeetingID>${r.getMeeting().getExternalId()?html}</externalMeetingID>
<name><![CDATA[${r.getMeeting().getName()}]]></name>
<isBreakout>${r.getMeeting().isBreakout()?c}</isBreakout>
<published>${r.getPublished()?string}</published>
<state>${r.getState()?string}</state>
<startTime><#if r.getStartTime()?? && r.getStartTime() != "">${r.getStartTime()}</#if></startTime>
<endTime><#if r.getEndTime()?? && r.getEndTime() != "">${r.getEndTime()}</#if></endTime>
<participants><#if r.getNumParticipants()??>${r.getNumParticipants()}</#if></participants>
<#assign m = r.getMetadata()>
<participants><#if r.getParticipants()??>${r.getParticipants()}</#if></participants>
<#if r.getBreakout()??>
<#assign breakout = r.getBreakout()>
<breakout>
<parentId>${breakout.getParentMeetingId()}</parentId>
<sequence>${breakout.getSequence()?c}</sequence>
</breakout>
</#if>
<#if r.getBreakoutRooms()??>
<#list r.getBreakoutRooms()>
<breakoutRooms>
<#items as broom>
<breakoutRoom>${broom.getValue()}</breakoutRoom>
</#items>
</breakoutRooms>
</#list>
</#if>
<#assign m = r.getMeta().get()>
<metadata>
<#list m?keys as prop>
<${prop}><![CDATA[${m[prop]}]]></${prop}>
</#list>
</metadata>
<#assign pb = r.getPlayback()>
<playback>
<#if r.getPlaybacks()??>
<#list r.getPlaybacks() as p>
<#if p?? && p.getFormat()??>
<format>
<type>${p.getFormat()}</type>
<url>${p.getUrl()}</url>
<length>${p.getLength()}</length>
<#if p.getExtensions()??>
<#list p.getExtensions() as extension>
<${extension.getType()}>
<#assign properties = extension.getProperties()>
<#if extension.getType() == "preview">
<#list properties?keys as property>
<#if property == "images">
<${property}>
<#if properties[property]["image"]?? && properties[property]["image"]?is_hash>
<#assign image = properties[property]["image"]>
<image <#if image["attributes"]?? && image["attributes"]["width"]??>width="${image["attributes"]["width"]}"</#if> <#if image["attributes"]?? && image["attributes"]["height"]??>height="${image["attributes"]["height"]}"</#if> <#if image["attributes"]?? && image["attributes"]["alt"]??>alt="<#escape x as x?xml>${image["attributes"]["alt"]}</#escape>"</#if>>${image["text"]}</image>
<#elseif properties[property]["image"]?? && properties[property]["image"]?is_enumerable>
<#list properties[property]["image"] as image>
<image <#if image["attributes"]?? && image["attributes"]["width"]??>width="${image["attributes"]["width"]}"</#if> <#if image["attributes"]?? && image["attributes"]["height"]??>height="${image["attributes"]["height"]}"</#if> <#if image["attributes"]?? && image["attributes"]["alt"]??>alt="<#escape x as x?xml>${image["attributes"]["alt"]}</#escape>"</#if>>${image["text"]}</image>
</#list>
</#if>
</${property}>
<#else>
<${property} />
</#if>
</#list>
</#if>
</${extension.getType()}>
</#list>
</#if>
</format>
</#if>
</#list>
</#if>
<format>${pb.getFormat()}</format>
<link>${pb.getLink()}</link>
<processingTime>${pb.getProcessingTime()?c}</processingTime>
<duration>${pb.getDuration()?c}</duration>
<#if pb.getExtensions()??>
<extensions>
<#if pb.getExtensions().getPreview()??>
<#assign prev = pb.getExtensions().getPreview()>
<preview>
<#list prev.getImages()>
<images>
<#items as image>
<image width="${image.getWidth()}" height="${image.getHeight()}" alt="${image.getAlt()}">${image.getValue()}</image>
</#items>
</images>
</#list>
</preview>
</#if>
</extensions>
</#if>
</playback>
</recording>
</#list>

View File

@ -50,10 +50,22 @@ module BigBlueButton
@redis.hgetall("meeting:info:#{meeting_id}")
end
def has_breakout_metadata_for(meeting_id)
@redis.exists("meeting:breakout:#{meeting_id}")
end
def breakout_metadata_for(meeting_id)
@redis.hgetall("meeting:breakout:#{meeting_id}")
end
def has_breakout_rooms_for(meeting_id)
@redis.exists("meeting:breakout:rooms:#{meeting_id}")
end
def breakout_rooms_for(meeting_id)
@redis.smembers("meeting:breakout:rooms:#{meeting_id}")
end
def num_events_for(meeting_id)
@redis.llen("meeting:#{meeting_id}:recordings")
end
@ -82,6 +94,10 @@ module BigBlueButton
@redis.del("meeting:breakout:#{meeting_id}")
end
def delete_breakout_rooms_for(meeting_id)
@redis.del("meeting:breakout:rooms:#{meeting_id}")
end
def build_header(message_type)
return {
"timestamp" => BigBlueButton.monotonic_clock, #
@ -175,6 +191,8 @@ module BigBlueButton
MODULE = 'module'
EVENTNAME = 'eventName'
MEETINGID = 'meetingId'
MEETINGNAME = 'meetingName'
ISBREAKOUT = 'isBreakout'
def initialize(redis)
@redis = redis
@ -186,13 +204,29 @@ module BigBlueButton
result = xml.instruct! :xml, :version => "1.0", :encoding=>"UTF-8"
meeting_metadata = @redis.metadata_for(meeting_id)
breakout_metadata = @redis.breakout_metadata_for(meeting_id)
version = YAML::load(File.open('../../core/scripts/bigbluebutton.yml'))["bbb_version"]
if (meeting_metadata != nil)
xml.recording(:meeting_id => meeting_id, :bbb_version => version) {
xml.meeting(:id => meeting_id, :externalId => meeting_metadata[MEETINGID], :name => meeting_metadata[MEETINGNAME], :breakout => meeting_metadata[ISBREAKOUT])
xml.metadata(meeting_metadata)
xml.breakout(breakout_metadata)
if (@redis.has_breakout_metadata_for(meeting_id))
breakout_metadata = @redis.breakout_metadata_for(meeting_id)
xml.breakout(breakout_metadata)
end
if (@redis.has_breakout_rooms_for(meeting_id))
breakout_rooms = @redis.breakout_rooms_for(meeting_id)
if (breakout_rooms != nil)
xml.breakoutRooms() {
breakout_rooms.each do |breakout_room|
xml.breakoutRoom(breakout_room)
end
}
end
end
msgs = @redis.events_for(meeting_id)
msgs.each do |msg|
res = @redis.event_info_for(meeting_id, msg)
@ -231,6 +265,7 @@ module BigBlueButton
end
@redis.delete_metadata_for(meeting_id)
@redis.delete_breakout_metadata_for(meeting_id)
@redis.delete_breakout_rooms_for(meeting_id)
end
def save_events_to_file(directory, result)

View File

@ -1,4 +1,4 @@
# Set encoding to utf-8
# Set encoding to utf-8
# encoding: UTF-8
#
@ -109,6 +109,24 @@ if not FileTest.directory?(target_dir)
end_time = recording.at_xpath("end_time")
end_time.content = real_end_time
## Copy the breakout and breakout rooms node from
## events.xml if present.
breakout_xpath = @doc.xpath("//breakout")
breakout_rooms_xpath = @doc.xpath("//breakoutRooms")
meeting_xpath = @doc.xpath("//meeting")
if (meeting_xpath != nil)
recording << meeting_xpath
end
if (breakout_xpath != nil)
recording << breakout_xpath
end
if (breakout_rooms_xpath != nil)
recording << breakout_rooms_xpath
end
participants = recording.at_xpath("participants")
participants.content = BigBlueButton::Events.get_num_participants("#{target_dir}/events.xml")